node-autotask 0.26.0 → 0.28.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 (49) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/MCP_TOOL_CATALOG.json +941 -14
  3. package/MCP_TOOL_MANIFEST.md +71 -16
  4. package/N8N_MCP_MANIFEST.json +91 -10
  5. package/N8N_MCP_MANIFEST.md +14 -11
  6. package/README.md +11 -9
  7. package/capabilities.json +13623 -7738
  8. package/capabilities.schema.json +125 -0
  9. package/dist/capabilities.cjs +2 -2
  10. package/dist/capabilities.js +1 -1
  11. package/dist/{chunk-QXWIQ67O.js → chunk-6EQSXYRW.js} +13622 -7737
  12. package/dist/{chunk-HQGE4WAV.cjs → chunk-D5IVS6GY.cjs} +1054 -235
  13. package/dist/{chunk-5OGJTS2E.cjs → chunk-GUY2KK7U.cjs} +157 -136
  14. package/dist/{chunk-SSUECUVN.js → chunk-IRVZGW5K.js} +947 -128
  15. package/dist/{chunk-7DOSJS6S.js → chunk-O2D7KDDD.js} +23 -2
  16. package/dist/{chunk-VVQ6NJYZ.js → chunk-SYX6EWMT.js} +694 -76
  17. package/dist/{chunk-SG4LIVMG.cjs → chunk-WYKGD5UD.cjs} +1189 -571
  18. package/dist/{chunk-WM6E5O55.cjs → chunk-ZEGMVEF3.cjs} +13622 -7737
  19. package/dist/{index-CKYr0unq.d.cts → index-CORcZUNm.d.cts} +457 -50
  20. package/dist/{index-1zBdJIEy.d.cts → index-DQ9NgraW.d.cts} +281 -1
  21. package/dist/{index-1zBdJIEy.d.ts → index-DQ9NgraW.d.ts} +281 -1
  22. package/dist/{index-C8QH3BkP.d.ts → index-DXuV04zM.d.ts} +457 -50
  23. package/dist/index.cjs +44 -24
  24. package/dist/index.d.cts +11 -5
  25. package/dist/index.d.ts +11 -5
  26. package/dist/index.js +26 -6
  27. package/dist/operations/index.cjs +147 -120
  28. package/dist/operations/index.d.cts +136 -7
  29. package/dist/operations/index.d.ts +136 -7
  30. package/dist/operations/index.js +33 -6
  31. package/dist/pool.cjs +42 -9
  32. package/dist/pool.d.cts +14 -3
  33. package/dist/pool.d.ts +14 -3
  34. package/dist/pool.js +38 -5
  35. package/dist/resources/index.cjs +19 -5
  36. package/dist/resources/index.d.cts +2 -2
  37. package/dist/resources/index.d.ts +2 -2
  38. package/dist/resources/index.js +18 -4
  39. package/dist/types/index.cjs +1 -1
  40. package/dist/types/index.d.cts +1 -1
  41. package/dist/types/index.d.ts +1 -1
  42. package/dist/types/index.js +1 -1
  43. package/docs/API.md +246 -10
  44. package/examples/mcp/schemas.ts +16 -7
  45. package/package.json +1 -1
  46. /package/dist/{chunk-S643FDOR.cjs → chunk-IXF7AKOJ.cjs} +0 -0
  47. /package/dist/{chunk-2TWKEB2L.js → chunk-RMG55JTF.js} +0 -0
  48. /package/dist/{chunk-W3K5BEVY.cjs → chunk-SDYRQFGL.cjs} +0 -0
  49. /package/dist/{chunk-EBCHCNPK.js → chunk-WLTLB2ND.js} +0 -0
package/docs/API.md CHANGED
@@ -930,6 +930,29 @@ rejects the batch once in-flight items settle; `signal` stops new items starting
930
930
  `node-autotask/resources` (resource classes) and `node-autotask/types` (entity
931
931
  types, type-only) alongside `./capabilities` and `./operations`.
932
932
 
933
+ ### Pooled clients and the shared read cache (`./pool` subpath, issue #219 N6)
934
+
935
+ `createClientPool({ maxSize?, idleTtlMs?, factory?, sharedReadCache? })` keeps
936
+ one `AutotaskClient` per credential tuple (`{ get, delete, clear, size }`) so a
937
+ multi-tenant gateway reuses each tenant's limiter state. Each pooled client has
938
+ its own read cache, so a write through one client never cleared what another
939
+ client of the SAME tenant would serve — a post-write read-back routed to a
940
+ re-created client (a `delete(config)` on a 401, an LRU eviction, an idle
941
+ expiry) could return the pre-write snapshot for up to the resource TTL
942
+ (contacts: 5 min).
943
+
944
+ **Default (shared):** the pool OWNS one `ReadCache` per credential tuple and
945
+ hands it to every client it builds for that tuple (via the `cache.store`
946
+ config seam), so any write through any pooled client of that tenant voids the
947
+ read cache for all of them and a read-back is never served the pre-write
948
+ snapshot. The shared cache outlives a client entry's `delete`/eviction (its
949
+ entries still expire on their own TTL), is pruned at `maxSize` and dropped by
950
+ `clear()`. `sharedReadCache: false` restores per-client caches (then route a
951
+ post-write read-back through the writing client). `cache: false` opts the tuple
952
+ out entirely. `cache.store` is otherwise a plain advanced seam: a supplied
953
+ `ReadCache` instance is used as-is, and `ttlMs` / `perResourceTtlMs` beside it
954
+ refuse (the instance owns its TTLs).
955
+
933
956
  ## 7. Operations surface (`./operations` subpath)
934
957
 
935
958
  Cross-resource helpers live in `src/operations/` (placement locked —
@@ -1058,13 +1081,13 @@ be cross-mode-refused.
1058
1081
  and the `confirm === <op key>` gate (all 54 deletes; the 3 approval-gated
1059
1082
  writes — inside `autotask_write` too) apply exactly as in `invoke`.
1060
1083
  Pinned by the full-surface suite (`test/operations/invoke/modes.test.ts`:
1061
- all 1214 ops × 3 modes, zero wire).
1084
+ all 1275 ops × 3 modes, zero wire).
1062
1085
 
1063
1086
  **Harness approval policy (recommended).** Require approval on every tool
1064
- whose MCP annotation is `readOnlyHint !== true` — today exactly **256
1065
- tools**: the 254 dedicated write/delete tools (198 `effect=write` + 56
1087
+ whose MCP annotation is `readOnlyHint !== true` — today exactly **267
1088
+ tools**: the 265 dedicated write/delete tools (209 `effect=write` + 56
1066
1089
  `effect=destructive`, every one of them has a dedicated tool) +
1067
- `autotask_write` + `autotask_delete`. The remaining **608 tools** (the 595
1090
+ `autotask_write` + `autotask_delete`. The remaining **631 tools** (the 618
1068
1091
  dedicated read tools + the 10 core read tools + `autotask_read` +
1069
1092
  `autotask_catalog` + `autotask_describe`) all carry `readOnlyHint: true`
1070
1093
  and are safe ungated: every op they can route is `effect=read` (zero
@@ -1077,7 +1100,7 @@ additionally require `confirm === <operation key>` even inside
1077
1100
  `resources.transferOwnership`) require the same confirm inside
1078
1101
  `autotask_write`. Migration: `autotask_invoke` is REPLACED by
1079
1102
  `autotask_read`/`autotask_write`/`autotask_delete` — update harness config
1080
- (approve the 256 above) and remove any `autotask_invoke` entry. The rule is
1103
+ (approve the 267 above) and remove any `autotask_invoke` entry. The rule is
1081
1104
  derivable from the registry `effect` column (and the mode tools' fixed
1082
1105
  annotations) — it never needs a hand-maintained name list.
1083
1106
 
@@ -1418,7 +1441,77 @@ notes cannot be deleted, so they never use create-then-delete.
1418
1441
  calendar dates the window covers in that zone and the bounds are whole UTC days of those dates. (Before 0.24.0 a `tz`
1419
1442
  behind UTC, e.g. `America/Los_Angeles`, selected the NEXT UTC day; live-verified 2026-10-23.)
1420
1443
 
1421
- ## 8. Resource surface (128 resources)
1444
+ ### 7.12 `timeEntries.approve` — time-entry approval levels (2026-10-26)
1445
+
1446
+ `timeEntries.approve(id, opts?)` records one approval level for a time entry.
1447
+ The vendor keeps approvals in a separate entity (`BillingItemApprovalLevels`)
1448
+ and, by its own documentation, does not write the approval fields on the time
1449
+ entry when that record is created — so the helper is a composite:
1450
+
1451
+ 1. reads the time entry (also the whole dry run: one GET, zero writes),
1452
+ 2. `POST /V1.0/BillingItemApprovalLevels` with
1453
+ `{ timeEntryID, approvalResourceID, approvalLevel, approvalDateTime }`,
1454
+ 3. updates the time entry with the vendor's own workaround —
1455
+ `billingApprovalDateTime` + `billingApprovalResourceID`, one
1456
+ collection-merge `PUT /V1.0/TimeEntries`,
1457
+ 4. reads the entry back and compares the two fields.
1458
+
1459
+ Defaults: `approvalResourceID` = the entry's own `resourceID`, `approvalLevel` =
1460
+ 1, `approvalDateTime` = now. An entry with no `resourceID` and no caller pin is
1461
+ refused before any write.
1462
+
1463
+ The result is `status: 'approved'` when the read-back matches, and
1464
+ `status: 'approval_fields_not_persisted'` (`persisted: false`) when the level
1465
+ record was created but the follow-up fields did not stick. The level id is
1466
+ returned either way: the vendor defines no DELETE for
1467
+ `BillingItemApprovalLevels`, so the record cannot be rolled back.
1468
+ `dryRun: true` returns the pinned envelope for the POST plus the same warning,
1469
+ with the single GET as the only wire call.
1470
+
1471
+ Posting is not part of the operation (`/V1.0/BillingItems` exposes PUT/PATCH
1472
+ only), and the typed TimeEntry model omits the live-only `postedDate` /
1473
+ `postedOnTime` fields, so "approved and posted" is only detectable as a vendor
1474
+ error from the follow-up update.
1475
+
1476
+ ### 7.13 Ticket status semantics and per-resource tickets (issues #226 / #225)
1477
+
1478
+ `tickets.statusSemantics(opts?)` resolves **which status ids mean closed** in the
1479
+ tenant. The closed set is a tenant picklist, not the constant `5`: the resolver
1480
+ reads `GET /V1.0/Tickets/entityInformation/fields` through the field-metadata
1481
+ cache (`tickets:fields`, the same entry `operations.fieldCatalog` warms, so a
1482
+ warm cache is zero wire) and reports
1483
+ `{ picklist, closedIds, source: 'picklist' | 'assumed', note }`. A status value
1484
+ is closed when its label is complete, completed, closed, cancelled, canceled or
1485
+ resolved. When the catalog cannot be read, or carries no recognisable closed
1486
+ label, the historical id `5` stands in with `source: 'assumed'` and a `note`
1487
+ that says so. Passing `excludeStatusIds` reports those ids instead
1488
+ (`source: 'picklist'` — an explicit caller choice). No module outside this
1489
+ resolver holds a status literal: the move engine's closed-target refusal,
1490
+ `companies.fullProfile`'s `openTickets` part and `tickets.forResource`'s
1491
+ `openOnly` all call it and carry the disclosure (`closedStatusIds` /
1492
+ `closedStatusSource`, or the part `note`) in their own output.
1493
+
1494
+ `tickets.forResource(resourceID, opts?)` lists the tickets assigned to one
1495
+ resource, as primary assignee, secondary resource, or both (the `scope` option,
1496
+ default `both`; the seam is `Tickets.assignedResourceID` for primary and the
1497
+ `TicketSecondaryResources` link table for secondary). It follows
1498
+ `time_entries.recentTickets`: two bounded reads (a `Tickets/query` on
1499
+ `assignedResourceID` and a `TicketSecondaryResources/query` on `resourceID`,
1500
+ both `id in [...]`-chunked when the link set is large) unioned client-side —
1501
+ deliberately not the cross-field grouped `or`, whose live semantics are
1502
+ unverified. Result: `{ resourceID, scope, openOnly, tickets, primaryIds,
1503
+ secondaryIds, bothIds, counts, capped, unresolvedIds, closedStatus }`, newest
1504
+ first by `id` desc, each ticket projected to `id`, `ticketNumber`, `title`,
1505
+ `status`, `assignedResourceID`, `companyID`. `openOnly: true` applies the
1506
+ resolved open filter server-side (`status notIn` the closed ids, or a single
1507
+ `not-eq` leaf) and re-applies it client-side so a server that ignores the leaf
1508
+ cannot widen the answer; `closedStatus` carries the disclosure above.
1509
+ `limit` (1-500, default 100) caps the returned tickets — `capped: true` means
1510
+ the cap bit, and the link read is capped at 500 rows (`capped` also reflects
1511
+ that). `unresolvedIds` lists ids the ticket read did not return (deleted or
1512
+ invisible). `excludeStatusIds` overrides the closed set for this call.
1513
+
1514
+ ## 8. Resource surface (135 resources)
1422
1515
 
1423
1516
  <!-- BEGIN GENERATED:api-per-entity -->
1424
1517
  Standard methods (every resource, `BaseResource`): `query` (stream, 1000-page guarded), `queryBounded` (server-side pagination — one call walks the cursor until `limit` TOTAL records, default 500 / max 2500, with a `hasMore` flag; the MCP `query` surface, routed by `invoke`), `queryAll` (collect → `T[]` — the unbounded raw walk), `count`, `get` (id → record; `item: null` → `NotFoundError`), `create` / `update` / `delete` (where the vendor defines them — mutations take a `MutationOpts` bag: `{ dryRun, lastModifiedDate?, maxRetries? }` (issue #105: `maxRetries` overrides the configured retry budget for every request the mutation issues; `0` = exactly one send per request)), plus the helper floor where the plan declares it: `resolve` (all 121), `search` (40 — bounded contains on the resource text field), `getContext` (5 workflow resources), `findBy<Field>` (3), `createIfNotExists` (14). Entity metadata on every resource: `getEntityInfo`, `getFieldInfo`, `getUdfInfo` (UDF-capable 15). Parent-scoped children take `(parentId, id)` on the CRUD methods and require `parentId` on `createIfNotExists` (the scoped create POSTs the scoped collection; the dedup query stays global); `query`/`count`/`resolve`/`search` stay global (narrow with a `parentField` filter). Scoped `update` PATCHes the collection (the vendor keys the record by the id in the body).
@@ -1427,6 +1520,7 @@ Standard methods (every resource, `BaseResource`): `query` (stream, 1000-page gu
1427
1520
  |--------------|-----------|------|------|--------|----------------------|-------------|---------|
1428
1521
  | `appointments` | Appointment | entity | extended | no | query, count, get, create, update, delete, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_appointment_get |
1429
1522
  | `billing_codes` | BillingCode | entity | extended | no | query, count, get, resolve, search | getEntityInfo, getFieldInfo, getUdfInfo | autotask_billingCode_get |
1523
+ | `billing_item_approval_levels` | BillingItemApprovalLevel | entity | extended | no | query, count, get, create | — | autotask_billingItemApprovalLevel_get |
1430
1524
  | `billing_items` | BillingItems | entity | extended | no | query, count, get, update, resolve | customRange, getEntityInfo, getFieldInfo, getUdfInfo, last14, last24h, last30, last7, last90, lastFullFortnight, lastFullMonth, lastFullQuarter, lastFullWeek, today, yesterday | autotask_billingItems_get |
1431
1525
  | `change_request_links` | ChangeRequestLink | entity | extended | no | query, count, get, create, delete, resolve | createIfNotExists, getEntityInfo, getFieldInfo, getUdfInfo | autotask_changeRequestLinks_get |
1432
1526
  | `checklist_libraries` | ChecklistLibrary | entity | extended | no | query, count, get, create, update, delete, resolve, search | getEntityInfo, getFieldInfo, getUdfInfo | autotask_checklistLibrary_get |
@@ -1482,11 +1576,15 @@ Standard methods (every resource, `BaseResource`): `query` (stream, 1000-page gu
1482
1576
  | `expense_reports` | ExpenseReport | entity | extended | no | query, count, get, create, update, resolve, search | getEntityInfo, getFieldInfo, getUdfInfo | autotask_expenseReport_get |
1483
1577
  | `holiday_sets` | HolidaySet | entity | advanced | no | query, count, get, create, update, delete, resolve | createIfNotExists, getEntityInfo, getFieldInfo, getUdfInfo | autotask_holidaySet_get |
1484
1578
  | `holidays` | Holiday | entity | advanced | yes (holiday_sets) | query, count, get, create, update, delete, resolve | createIfNotExists, getEntityInfo, getFieldInfo, getUdfInfo | autotask_holiday_get |
1579
+ | `invoice_templates` | InvoiceTemplate | entity | advanced | no | query, count, get | getEntityInfo, getFieldInfo, getUdfInfo | autotask_invoiceTemplate_get |
1485
1580
  | `invoices` | Invoice | entity | extended | no | query, count, get, update, resolve | customRange, getEntityInfo, getFieldInfo, getUdfInfo, last14, last24h, last30, last7, last90, lastFullFortnight, lastFullMonth, lastFullQuarter, lastFullWeek, markupHtml, markupXml, pdf, today, yesterday | autotask_invoice_get |
1486
1581
  | `notification_history` | NotificationHistory | entity | advanced | no | query, count, get, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_notificationHistory_get |
1487
1582
  | `opportunities` | Opportunity | entity | extended | no | query, count, get, create, update, resolve | createIfNotExists, getEntityInfo, getFieldInfo, getUdfInfo | autotask_opportunity_get |
1488
1583
  | `opportunity_attachments` | OpportunityAttachment | entity | extended | yes (opportunities) | query, count, get, create, delete, resolve | download, getEntityInfo, getFieldInfo, getUdfInfo | autotask_opportunityAttachment_get |
1489
1584
  | `opportunity_categories` | OpportunityCategory | entity | extended | no | query, count, get, update, resolve, search | getEntityInfo, getFieldInfo, getUdfInfo | autotask_opportunityCategories_get |
1585
+ | `organizational_level1s` | OrganizationalLevel1 | entity | advanced | no | query, count, get, create, update | getEntityInfo, getFieldInfo, getUdfInfo | autotask_organizationalLevel1_get |
1586
+ | `organizational_level2s` | OrganizationalLevel2 | entity | advanced | no | query, count, get, create, update | getEntityInfo, getFieldInfo, getUdfInfo | autotask_organizationalLevel2_get |
1587
+ | `organizational_level_associations` | OrganizationalLevelAssociation | entity | advanced | no | query, count, get, create, update | getEntityInfo, getFieldInfo, getUdfInfo | autotask_organizationalLevelAssociation_get |
1490
1588
  | `product_vendors` | ProductVendors | entity | extended | yes (products) | query, count, get, create, update, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_productVendor_get |
1491
1589
  | `products` | Product | entity | extended | no | query, count, get, create, update, resolve, search | getEntityInfo, getFieldInfo, getUdfInfo | autotask_product_get |
1492
1590
  | `project_charges` | ProjectCharge | entity | extended | yes (projects) | query, count, get, create, update, delete, resolve, search | createIfNotExists, getEntityInfo, getFieldInfo, getUdfInfo | autotask_projectCharge_get |
@@ -1510,6 +1608,7 @@ Standard methods (every resource, `BaseResource`): `query` (stream, 1000-page gu
1510
1608
  | `resource_time_off_balances` | ResourceTimeOffBalance | entity | extended | yes (resources) | get, resolve | getByResource, getByYear, getEntityInfo, getFieldInfo, getUdfInfo | autotask_resourceTimeOffBalance_get |
1511
1609
  | `resources` | Resource | entity | core | no | query, count, get, update, resolve, search | getEntityInfo, getFieldInfo, getUdfInfo, transferOwnership, whoAmI | autotask_resource_get |
1512
1610
  | `roles` | Role | entity | extended | no | query, count, get, create, update, resolve, search | getEntityInfo, getFieldInfo, getUdfInfo | autotask_role_get |
1611
+ | `sales_orders` | SalesOrder | entity | extended | no | query, count, get, update | getEntityInfo, getFieldInfo, getUdfInfo | autotask_salesOrder_get |
1513
1612
  | `service_call_task_resources` | ServiceCallTaskResource | entity | extended | yes (service_call_tasks) | query, count, get, create, delete, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_serviceCallTaskResource_get |
1514
1613
  | `service_call_tasks` | ServiceCallTask | entity | extended | yes (service_calls) | query, count, get, create, delete, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_serviceCallTask_get |
1515
1614
  | `service_call_ticket_resources` | ServiceCallTicketResource | entity | extended | yes (service_call_tickets) | query, count, get, create, delete, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_serviceCallTicketResource_get |
@@ -1526,6 +1625,7 @@ Standard methods (every resource, `BaseResource`): `query` (stream, 1000-page gu
1526
1625
  | `tag_groups` | TagGroup | entity | advanced | no | query, count, get, create, update, delete, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_tagGroup_get |
1527
1626
  | `tags` | Tag | entity | advanced | no | query, count, get, create, update, delete, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_tag_get |
1528
1627
  | `task_secondary_resources` | TaskSecondaryResource | entity | extended | yes (tasks) | query, count, get, create, delete, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_taskSecondaryResources_get |
1628
+ | `tax_regions` | TaxRegion | entity | advanced | no | query, count, get, create, update | getEntityInfo, getFieldInfo, getUdfInfo | autotask_taxRegion_get |
1529
1629
  | `ticket_additional_configuration_items` | TicketAdditionalConfigurationItem | entity | extended | yes (tickets) | query, count, get, create, delete, resolve | createIfNotExists, getEntityInfo, getFieldInfo, getUdfInfo | autotask_ticketAdditionalConfigurationItem_get |
1530
1630
  | `ticket_additional_contacts` | TicketAdditionalContact | entity | extended | yes (tickets) | query, count, get, create, delete, resolve | createIfNotExists, getEntityInfo, getFieldInfo, getUdfInfo | autotask_ticketAdditionalContact_get |
1531
1631
  | `ticket_attachments` | TicketAttachment | entity | extended | yes (tickets) | query, count, get, create, delete, resolve | download, getEntityInfo, getFieldInfo, getUdfInfo | autotask_ticketAttachment_get |
@@ -1541,8 +1641,8 @@ Standard methods (every resource, `BaseResource`): `query` (stream, 1000-page gu
1541
1641
  | `ticket_notes` | TicketNote | entity | extended | yes (tickets) | query, count, get, create, update, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_ticketNote_get |
1542
1642
  | `ticket_secondary_resources` | TicketSecondaryResource | entity | extended | yes (tickets) | query, count, get, create, delete, resolve | getEntityInfo, getFieldInfo, getUdfInfo | autotask_ticketSecondaryResources_get |
1543
1643
  | `ticket_webhooks` | TicketWebhook | entity | extended | yes (tickets) | query, count, get, create, update, delete, resolve, search | getEntityInfo, getFieldInfo, getUdfInfo | autotask_ticketWebhook_get |
1544
- | `tickets` | Ticket | entity | core | no | query, count, get, create, update, resolve, search, getContext | changeStatus, findByTicketNumber, fullContext, getEntityInfo, getFieldInfo, getUdfInfo, openWithNoteAndAssign, slaHealthCheck, summary, timeline | autotask_ticket_get |
1545
- | `time_entries` | TimeEntry | entity | extended | no | query, count, get, create, update, delete, resolve | createForResource, createIfNotExists, customRange, dateWorkedWindow, getEntityInfo, getFieldInfo, getPosted, getUdfInfo, getUnposted, last14, last24h, last30, last7, last90, lastFullFortnight, lastFullMonth, lastFullQuarter, lastFullWeek, postedWithin, today, yesterday | autotask_timeEntry_get |
1644
+ | `tickets` | Ticket | entity | core | no | query, count, get, create, update, resolve, search, getContext | changeStatus, findByTicketNumber, forResource, fullContext, getEntityInfo, getFieldInfo, getUdfInfo, openWithNoteAndAssign, slaHealthCheck, statusSemantics, summary, timeline | autotask_ticket_get |
1645
+ | `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, postedWithin, today, yesterday | autotask_timeEntry_get |
1546
1646
  | `time_entry_attachments` | TimeEntryAttachment | entity | extended | yes (time_entries) | query, count, get, create, delete, resolve | download, getEntityInfo, getFieldInfo, getUdfInfo | autotask_timeEntryAttachment_get |
1547
1647
  | `time_off_requests` | TimeOffRequest | entity | extended | no | query, count, get, resolve | approve, getEntityInfo, getFieldInfo, getUdfInfo, reject | autotask_timeOffRequest_get |
1548
1648
  | `inventory_products` | InventoryProduct | entity | extended | no | query, count, get, create, update, delete, resolve | getEntityInfo, getFieldInfo, getUdfInfo, stockedItemsAdd, stockedItemsRemove, stockedItemsTransfer | autotask_inventoryProduct_get |
@@ -1554,7 +1654,7 @@ Standard methods (every resource, `BaseResource`): `query` (stream, 1000-page gu
1554
1654
  | `threshold_information` | — | integration | core | no | get | — | autotask_threshold_information |
1555
1655
  | `zone_information` | — | integration | advanced | no | get | — | autotask_zone_information |
1556
1656
 
1557
- 128 resources (121 entity + 7 integration). Rows are generated from the registry — do not hand-type.
1657
+ 129 resources (122 entity + 7 integration). Rows are generated from the registry — do not hand-type.
1558
1658
  <!-- END GENERATED:api-per-entity -->
1559
1659
 
1560
1660
  **`companies.search` include_family (issue #59).** `companies.search` is
@@ -1831,6 +1931,28 @@ resource paths, of which **40** expose a create op (the stampable set; the
1831
1931
  (resource, role) pairs exist in the tenant (postedWithin rows: resourceID
1832
1932
  31131373 / 29869529 with roleID 29682834). The typed W-A3 pointer note
1833
1933
  carries this.
1934
+ - **Derived fields (issue #219 N8, live ww29 2026-10-26 — twelve time entries
1935
+ logged on ticket 530637)**: `hoursWorked` is recomputed from
1936
+ `startDateTime`/`endDateTime` (a create sending `1.43` for a 14:00-15:26
1937
+ block stored `1.5`; omitting it stored the same `1.5`; other entries stored
1938
+ the exact elapsed time — 40 min → 0.6667, 110 min → 1.8333, 15 min →
1939
+ 0.25; the rounding rule is **UNVERIFIED**) and a later PATCH returns HTTP
1940
+ success without persisting a correction (`FIELD_NOT_PERSISTED`) — the only
1941
+ remedy is delete + recreate. `timeEntryType`, `contractID` and
1942
+ `internalBillingCodeID` are inherited (from the ticket; the internal billing
1943
+ code is mirrored from `billingCodeID`); `timeEntryType` is flagged
1944
+ `readOnly`, which reads as "never send" but means "derived from the parent".
1945
+ `billingApprovalLevelMostRecent` is derived by a DIFFERENT entity: the REST
1946
+ approval write is a **create on `BillingItemApprovalLevels`** (create-only;
1947
+ `billing_item_approval_levels` is a plan resource since 0.27.0), while
1948
+ `billingApprovalResourceID` and `billingApprovalDateTime` are a documented
1949
+ follow-up UPDATE of the entry, not the approval itself — a caller told to
1950
+ "approve" must create the approval level row, not write two fields. The
1951
+ field is also not a usable signal: it stayed `0` for those twelve entries
1952
+ and for all 392,182 posted billing items in the tenant. The declaration is
1953
+ plan-pinned (`resources.time_entries.derivedFields`, tokens only — §11.14)
1954
+ and served on `operations.fieldCatalog` rows and the `time_entries`
1955
+ `createGuidance` row.
1834
1956
 
1835
1957
  ### 11.5 Company-create side effect
1836
1958
 
@@ -2095,7 +2217,7 @@ calls — generated plan data, drift-guarded by `capabilities:check` clause
2095
2217
  14), so an agent can read "what is blind on this zone" without burning
2096
2218
  budget or guessing:
2097
2219
 
2098
- - **`operations.blindSurfaces`** — the 116-surface inventory: per surface the
2220
+ - **`operations.blindSurfaces`** — the 124-surface inventory: per surface the
2099
2221
  kind (`unowned-blind` / `unfiltered-blind` / `unknown` — the 46
2100
2222
  data-with-rows surfaces ride `unknown` by design: the sweep's limit-2 read
2101
2223
  proves data, not the absence of it), the recorded `unfilteredRows` (0-2
@@ -2133,6 +2255,57 @@ W-A7 drift — the 200 that is not the specced envelope). An agent that reads
2133
2255
  the row before its create attempt converts a multi-iteration vendor ladder
2134
2256
  into one predicted-failure-free attempt.
2135
2257
 
2258
+ **Which required set wins (issue #219 N4).** Three sources answer "what does
2259
+ this create really require", at different coverage: the live-proven prefix
2260
+ (92 rows — the fields a live create actually demanded, in vendor-demand
2261
+ order), the hand-authored zone set `createRequiredSet` (6 resources), and the
2262
+ vendor field catalog's `isRequired` (the spec opinion — wrong for 68 of the
2263
+ 75 resources that carry a proven prefix). The precedence is FIXED:
2264
+ **live-proven > zone > spec**, and the source is named on the result.
2265
+ `effectiveRequiredFields(record, zoneFacts)` in `node-autotask/operations` is
2266
+ the only place in the SDK that compares the three; every
2267
+ `operations.createGuidance` row carries its result as
2268
+ `requiredFields: { fields, from: 'live-proven' | 'zone' | 'spec' }`
2269
+ (`contacts.create` → the four live-proven fields, not the empty spec list).
2270
+ A value-conditioned prefix (e.g. `contracts.create`'s type-1
2271
+ `estimatedRevenue`/`estimatedHours`/`estimatedCost`) is returned whole with
2272
+ `from: 'live-proven'` — the condition is typed on the row (see the next
2273
+ paragraph), so read it before treating the list as unconditional.
2274
+
2275
+ **Structured create requirements (issue #219 N3).** The blocker channel is a
2276
+ CLASS of wall, not a statement of what the vendor wants; and the campaign
2277
+ receipt that DID say what it wanted is 300-char truncated on disk
2278
+ (`ticket_charges.create` preserves only `"A productID or a billingCodeID must b"`).
2279
+ So the requirement is **declared** where the fact is known, and the receipt
2280
+ stays evidence — never reconstructed:
2281
+
2282
+ - `blocker.requires?: { anyOf?, allOf?, noneOf? }` — the vendor field names a
2283
+ caller must supply (or must not) for the create to be accepted. Optional and
2284
+ additive: a row without it keeps its exact shape.
2285
+ `ticket_charges.create` and `project_charges.create` serve
2286
+ `requires: { anyOf: ['productID', 'billingCodeID'] }`.
2287
+ - `createGuidance.requiresByValue?: [{ field, value, requires, values? }]` —
2288
+ requirements conditioned on a picklist value, so a caller sees the condition
2289
+ and can satisfy it before the attempt. `contracts.create` declares the
2290
+ recorded `contractType` gates: type 1 → `estimatedRevenue` +
2291
+ `estimatedHours` + `estimatedCost`; types 3, 4, 6, 8 → `estimatedRevenue`;
2292
+ type 7 → `contractPeriodType` (+ `values: { contractPeriodType: 2 }`) and
2293
+ `setupFee`. (Whether type 7 also needs `estimatedRevenue` is **UNVERIFIED**:
2294
+ the recorded message names `estimatedRevenue` for types 1/3/4/6/8 only, and
2295
+ this block reproduces that clause exactly rather than extending it.)
2296
+ - `createGuidance.rejectedValues?: [{ field, value }]` — picklist values the
2297
+ vendor refuses (`contractType: 5`).
2298
+
2299
+ The prose those fields used to live in is gone: the `contracts.create` message
2300
+ now POINTS at `requiresByValue` instead of restating the gates, so no consumer
2301
+ has to parse English. `check-capabilities` clause 20 holds the shape closed,
2302
+ checks every named field against the vendored model of the resource's
2303
+ `apiEntity`, and fails when a row's `vendorEvidence` states a one-of
2304
+ requirement (`"… or … must …"`) while the row declares no `requires` — the
2305
+ clause that stops this fact from being lost a second time. The create op's
2306
+ registry record carries the same typed block as `metadata.createRequirements`
2307
+ (the `autotask_describe` channel).
2308
+
2136
2309
  ### 11.13 The posted/unposted wall (W-D3) + `postedWithin`, and drifted-create marker recovery (issues #10/#13)
2137
2310
 
2138
2311
  - **W-D3 (live, ww29, 2026-10-04, re-verified)** — this zone's TimeEntry
@@ -2251,6 +2424,64 @@ the field catalog; the seven integration singletons are refused typed —
2251
2424
  a known key without a catalog → `CONFIG_ERROR`, an unknown key → typed
2252
2425
  `RESOURCE_UNKNOWN` carrying the count + the `autotask_catalog` pointer).
2253
2426
 
2427
+ **Reference targets (issue #219 N1).** Every projected field row carries
2428
+ `targetResource: string | null` and `targetEnumerable: boolean`: the plan
2429
+ resource key the vendor `referenceEntityType` names, and whether that
2430
+ resource has a read (`query`/`get`) path, i.e. whether a caller can
2431
+ enumerate valid ids with it. The join is DERIVED from the generated
2432
+ registry — `resourceForEntityType(entityType)` in
2433
+ `node-autotask/operations` reads each resource's plan `apiEntity` (plus
2434
+ the singular entity name where the plan's `apiEntity` is the plural path
2435
+ segment) and returns `null` when the plan carries no such resource, so a
2436
+ consumer never hand-maps `"CompanyLocation"` -> `company_locations`. A
2437
+ row for a target the plan cannot enumerate reads `null` + `false`; the
2438
+ row carries no remedy, label or exposure claim (`plan.referenceTargets`
2439
+ is the only home for those). The same helper serves reference UDF rows —
2440
+ there is no second entity-type table.
2441
+
2442
+ **User-defined fields (issue #219 N7).** `udfCapable` entities carry a
2443
+ parallel field system, and the served row states it rather than leaving it
2444
+ inferred. With the `udf` include, the row's `udf` list holds the UDF
2445
+ DEFINITIONS the vendor's `entityInformation/userDefinedFields` channel
2446
+ returns — `name`, `label`, `type`, `isRequired`, `isReadOnly`, `isPickList`,
2447
+ `isReference`, `referenceEntityType`, `picklistValues` (the values ride the
2448
+ `labels: true` opt, zero extra wire), `defaultValue`. A UDF definition never
2449
+ appears in the plan artifact (it is tenant-specific). The row also carries
2450
+ `udfFilter: 'udf-marker'` and `udfWriteShape: 'merge'` exactly when it is
2451
+ `udfCapable`: a filter on a UDF NAME is accepted by the vendor only with the
2452
+ `udf: true` marker, so a UDF field row serves `filterable: 'udf-marker'` and
2453
+ omits the vendor's `isQueryable` (true for UDFs, contradicted by the plain
2454
+ filter refusal), and a sent `userDefinedFields` list is MERGED by the vendor
2455
+ (a subset is correct). Object/array field rows declare
2456
+ `writeShape: 'merge' | 'replace-all'` (a scalar row carries none);
2457
+ `userDefinedFields` is `merge` at the row level. The row type
2458
+ (`FieldCatalogFieldRow`) is hand-written in `src/operations/fieldCatalog.ts`,
2459
+ not generated from the plan — the UDF vocabulary is a LIVE read.
2460
+
2461
+ **Derived fields (issue #219 N8).** Some fields are OUTPUTS: the vendor
2462
+ recomputes them or inherits them, so a submitted value is not authoritative.
2463
+ The plan pins the declaration on the resource
2464
+ (`resources.<key>.derivedFields: [{ field, rule, writableRoute? }]`) and the
2465
+ catalog serves it twice: the resource row carries the whole `derivedFields`
2466
+ list (present exactly when the plan declares one, never filtered by the
2467
+ `fields` opt) and each matching projected field row carries
2468
+ `derived: { rule, writableRoute? }`. `rule` is a closed token, never a
2469
+ sentence: `elapsed(<startField>,<endField>)` (the vendor recomputes the value
2470
+ from those two timestamps), `parent` (inherited from the parent record),
2471
+ `mirror(<field>)` (the vendor copies that field) and `external` (written by
2472
+ another entity's op — `writableRoute` names the plan write op). The tokens are
2473
+ validated by `check-capabilities` clause 21, which also pins the
2474
+ `time_entries` set (see §11.4/§11.12): `hoursWorked` is
2475
+ `elapsed(startDateTime,endDateTime)`, `timeEntryType` and `contractID` are
2476
+ `parent`, `internalBillingCodeID` is `mirror(billingCodeID)`, and
2477
+ `billingApprovalLevelMostRecent` is `external` with
2478
+ `writableRoute: billing_item_approval_levels.create`. **`derived` is not
2479
+ `readOnly`:** `isReadOnly` (the vendor flag) means never write the field at
2480
+ all; `derived` means the vendor overwrites or inherits it — a field can be
2481
+ derived and not read-only (`hoursWorked` accepts a sent value and discards
2482
+ it). The registry stamps the same list as `metadata.derivedFields` on every
2483
+ record of the resource, so `autotask_describe` and the catalog cannot drift.
2484
+
2254
2485
  **Honest scope (stated in the served text AND the result's `honesty`
2255
2486
  block):** the live catalog reports a field's SHAPE — existence,
2256
2487
  requiredness, type, `maxLength`, read-only, queryable, picklist-ness —
@@ -3355,6 +3586,11 @@ sentence (`UNTRUSTED_NOTE`, mirrored verbatim in
3355
3586
  `examples/mcp-register.ts` and `scripts/project-mcp-tools.mjs` so the
3356
3587
  served description equals the manifest's), and their WIRE `outputSchema`
3357
3588
  carries the field list as the `x-untrusted` extension (zod v4 `.meta`). The
3589
+ cross-resource tools (`resolveAny`, `searchAcrossResources`, the mode tools)
3590
+ carry `["*"]` instead of the union: the per-resource tools already list every
3591
+ field, the union only grows with each added resource, and the served surface has
3592
+ a hard token budget — `["*"]` states the stronger claim ("every text value here is
3593
+ vendor-authored") without repeating the catalog in five schemas. The
3358
3594
  reference server (`examples/mcp-register.ts`) wraps the affected fields in
3359
3595
  `<untrusted_content>` markers AFTER the SDK call (projection layer only —
3360
3596
  the SDK primitives stay raw), strips ONE outer wrapper from write/filter
@@ -6,7 +6,7 @@ import * as z from 'zod/v4';
6
6
  import type { StandardSchemaWithJSON } from '@modelcontextprotocol/server';
7
7
  import type { CapabilityRecord } from 'node-autotask/capabilities';
8
8
  import { WORKFLOW_RESOURCES } from 'node-autotask/operations';
9
- import { CROSS_RESOURCE_UNTRUSTED, untrustedFieldsOf } from './manifest.js';
9
+ import { untrustedFieldsOf } from './manifest.js';
10
10
 
11
11
  // ---------------------------------------------------------------------------
12
12
  // Input fragments (zod v4) — the shapes the manifest's `input` column states.
@@ -109,6 +109,18 @@ export function dataField(description: string, fields: readonly string[]): z.Zod
109
109
  return fields.length > 0 ? base.meta({ 'x-untrusted': [...fields] }) : base;
110
110
  }
111
111
 
112
+ /** The cross-resource wildcard result (search_across / resolve_any / a mode
113
+ * tool): its untrusted set is the union over every resource, which grows with
114
+ * each added resource and is already listed field-by-field on that resource's
115
+ * own tools. On the wire it is therefore marked `["*"]` — "every text value in
116
+ * this result is vendor-authored" — which is the stronger claim and does not
117
+ * repeat the catalog in five schemas (the served surface has a hard budget).
118
+ * The runtime redaction still uses the full union (`CROSS_RESOURCE_UNTRUSTED`
119
+ * in tools-mode.ts); only the wire annotation is compacted. */
120
+ export function crossDataField(description: string): z.ZodType {
121
+ return z.unknown().describe(description).meta({ 'x-untrusted': ['*'] });
122
+ }
123
+
112
124
  /** One resolved record: `found: false` is a complete answer, not an error.
113
125
  * The `record` field carries `x-untrusted` for the tool's resource. */
114
126
  export function oneOutput(rec: CapabilityRecord): z.ZodType {
@@ -139,7 +151,7 @@ export function hitsOutput(): z.ZodType {
139
151
  return z.object({
140
152
  query: z.string(),
141
153
  results: z.array(
142
- z.object({ resource: z.string(), matches: z.array(dataField('Compact summary rows.', CROSS_RESOURCE_UNTRUSTED)) }),
154
+ z.object({ resource: z.string(), matches: z.array(crossDataField('Compact summary rows.')) }),
143
155
  ),
144
156
  skipped: z.array(z.string().describe('Requested resources without a search helper — reported, never errored.')),
145
157
  });
@@ -150,10 +162,7 @@ export function resolveAnyOutput(): z.ZodType {
150
162
  return z.object({
151
163
  identifier: z.union([z.string(), z.number()]),
152
164
  results: z.array(
153
- dataField(
154
- 'Per-resource outcome: value (compact or null), resolutionCost, scanned, candidates/error on a miss.',
155
- CROSS_RESOURCE_UNTRUSTED,
156
- ),
165
+ crossDataField('Per-resource outcome: value (compact or null), resolutionCost, scanned, candidates/error on a miss.'),
157
166
  ),
158
167
  matches: z.array(z.string().describe('The resources that resolved to exactly one record.')),
159
168
  });
@@ -190,7 +199,7 @@ export function invokeOutput(): z.ZodType {
190
199
  return z.object({
191
200
  operation: z.string(),
192
201
  simulated: z.boolean(),
193
- result: dataField('The SDK result — the pinned DryRunResult envelope for a simulated write.', CROSS_RESOURCE_UNTRUSTED),
202
+ result: crossDataField('The SDK result — the pinned DryRunResult envelope for a simulated write.'),
194
203
  });
195
204
  }
196
205
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-autotask",
3
- "version": "0.26.0",
3
+ "version": "0.28.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,
File without changes
File without changes
File without changes
File without changes