node-autotask 0.43.0 → 0.44.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 (53) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/MCP_TOOL_CATALOG.json +27 -8
  3. package/MCP_TOOL_MANIFEST.md +138 -137
  4. package/N8N_MCP_MANIFEST.json +17 -17
  5. package/N8N_MCP_MANIFEST.md +18 -18
  6. package/README.md +6 -19
  7. package/capabilities.json +1815 -1432
  8. package/dist/{base-CVx2VMSj.d.cts → base-CftidiyZ.d.cts} +2 -1
  9. package/dist/{base-CVx2VMSj.d.ts → base-CftidiyZ.d.ts} +2 -1
  10. package/dist/capabilities.cjs +2 -2
  11. package/dist/capabilities.d.cts +2 -1
  12. package/dist/capabilities.d.ts +2 -1
  13. package/dist/capabilities.js +1 -1
  14. package/dist/{chunk-TH7B5H4P.cjs → chunk-6WLGHKIU.cjs} +285 -285
  15. package/dist/{chunk-KTEDPS4X.js → chunk-7MCKJTSA.js} +2 -2
  16. package/dist/{chunk-5VHSLJMR.js → chunk-A6LHCJQP.js} +30 -9
  17. package/dist/{chunk-4EFXIE53.js → chunk-BKTOMNT2.js} +2 -2
  18. package/dist/{chunk-VFLAMMIJ.cjs → chunk-EMYDC5SG.cjs} +151 -120
  19. package/dist/{chunk-SK3GYRNA.cjs → chunk-KWQSGWY5.cjs} +143 -143
  20. package/dist/{chunk-Q23JULNN.js → chunk-LYI5FRPX.js} +55 -24
  21. package/dist/{chunk-JY2BWYXY.js → chunk-N6IWPGHB.js} +1814 -1431
  22. package/dist/{chunk-5BDUMDAQ.cjs → chunk-PH3ESYAF.cjs} +1814 -1431
  23. package/dist/{chunk-MJTKK3QG.cjs → chunk-SN4BMQ2N.cjs} +31 -10
  24. package/dist/{index-C7rMJ4IV.d.ts → index-D8FtzUGP.d.ts} +54 -41
  25. package/dist/{index-C3KFMnRT.d.cts → index-DgBA1JMH.d.cts} +131 -130
  26. package/dist/{index-C3KFMnRT.d.ts → index-DgBA1JMH.d.ts} +131 -130
  27. package/dist/{index-Cm_Xpt7X.d.cts → index-YbEk0awP.d.cts} +54 -41
  28. package/dist/index.cjs +23 -23
  29. package/dist/index.d.cts +8 -7
  30. package/dist/index.d.ts +8 -7
  31. package/dist/index.js +5 -5
  32. package/dist/operations/index.cjs +337 -244
  33. package/dist/operations/index.d.cts +53 -45
  34. package/dist/operations/index.d.ts +53 -45
  35. package/dist/operations/index.js +121 -28
  36. package/dist/pool.cjs +9 -9
  37. package/dist/pool.d.cts +4 -3
  38. package/dist/pool.d.ts +4 -3
  39. package/dist/pool.js +5 -5
  40. package/dist/resources/index.cjs +5 -5
  41. package/dist/resources/index.d.cts +2 -2
  42. package/dist/resources/index.d.ts +2 -2
  43. package/dist/resources/index.js +4 -4
  44. package/dist/types/index.d.cts +1 -1
  45. package/dist/types/index.d.ts +1 -1
  46. package/dist/untrusted.cjs +2 -2
  47. package/dist/untrusted.d.cts +14 -5
  48. package/dist/untrusted.d.ts +14 -5
  49. package/dist/untrusted.js +1 -1
  50. package/docs/API.md +126 -37
  51. package/examples/mcp/schemas.ts +2 -0
  52. package/examples/mcp/tools-read.ts +3 -2
  53. package/package.json +10 -9
package/docs/API.md CHANGED
@@ -685,6 +685,22 @@ carry paging parameters.
685
685
  when the limit or the guard truncated the walk and more records exist past
686
686
  the stop point; `pageDetails.count` is never surfaced (it is the current
687
687
  page's record count — `<resource>.count` is the total).
688
+ - **`walk: { maxRows: N }` — an explicit export cap (issue #350).**
689
+ `N` is a positive integer and replaces the total `limit` cap, so
690
+ `{ walk: { maxRows: 10_000 } }` can return 10,000 rows. `limit` still
691
+ validates as 1..2500 (default 500) and only sizes pages in this mode:
692
+ `min(500, limit)`. `truncated` and `hasMore` are true when the final
693
+ page was sliced, a next cursor remains, or the 1000-page guard stopped
694
+ the walk, even below `maxRows`; an exhausted exact-cap result reports
695
+ false. No extra page is fetched to decide completeness. With no `walk`,
696
+ the ordinary cap and result shape remain unchanged (`truncated` is
697
+ omitted). `walk` and `latest` cannot be combined. Through `invoke`, the
698
+ unscoped-walk guard estimates the explicit `maxRows` cap independently
699
+ of `limit`; narrow the filter or pass `acknowledgeUnscoped: true` when
700
+ a large unscoped walk is intended. When names are enabled (per call or
701
+ by client labels), explicit walks apply the existing label budget per
702
+ fetched vendor page, matching the iterator; invoke does not label the
703
+ combined result again. Ordinary invoked queries keep their per-call budget.
688
704
  - **`latest: N` (issue #59) — the N highest-`id` rows without caller paging.**
689
705
  The vendor has NO sort parameter (the vendored spec's `QueryModel` is
690
706
  exactly `maxRecords` / `includeFields` / `filter`), so `latest` is a
@@ -780,23 +796,28 @@ Vendor filter structure (swagger `Filter`, recursive):
780
796
  `project.<field>` is resolved before the query: the parent entity is read
781
797
  (`IncludeFields: ['id']`, at most 300 rows) with the relation's conditions,
782
798
  and the leaves become `<relation>ID in [ids]`. Supported by `queryBounded`
783
- (and the invoke `query` op), `count` and `operations.aggregate`; the raw
784
- `query()`/`queryAll()` walks do not rewrite.
799
+ (and the invoke `query` op), `count`, `operations.aggregate`, and the raw
800
+ `query()`/`queryAll()` walks.
785
801
  - Inside an `and` (and at the top level) all leaves of one relation share ONE
786
802
  parent read; inside an `or` each relation leaf is its own read. Direct
787
803
  conditions are untouched and may be mixed freely.
788
804
  - No parent match: an `and` matches nothing; an `or` drops that branch. When
789
805
  the whole filter matches nothing the child call is not issued: `queryBounded`
790
806
  returns `{ items: [], fetched: 0, hasMore: false, pagesWalked: 0,
791
- relationNote }`, `count` returns `0`, aggregate group/sum carries
807
+ relationNote }` (plus `truncated: false` for an explicit `walk`),
808
+ `count` returns `0`, aggregate group/sum carries
792
809
  `relationNote`.
793
810
  - More than 300 matching parents: typed `AutotaskConfigError`, child call not
794
811
  issued (a capped parent set would be vendor-ordered, so partial totals would
795
812
  be biased). Narrow the relation conditions (e.g. a ticket date or status).
796
813
  - Pre-request (zero wire): unknown relation, a parent field outside the
797
- parent's vendored field set, multi-hop (`ticket.company.x`) and `udf: true`
798
- relation leaves are refused. The invoke field-name check skips relation
814
+ parent's vendored field set and multi-hop (`ticket.company.x`) are
815
+ refused. The invoke field-name check skips relation
799
816
  leaves (they are checked against the parent instead).
817
+ - Any dotted `udf: true` field is refused before metadata, parent queries,
818
+ scope counts or child requests, including self prefixes and unknown
819
+ prefixes (issue #351). Use an undotted tenant UDF name with `udf: true`;
820
+ `fieldCheck: false` does not bypass this rule.
800
821
 
801
822
  ```ts
802
823
  // company 123's time in October, hours per ticket: 2 calls (tickets ids, then the walk)
@@ -829,11 +850,20 @@ Vendor filter structure (swagger `Filter`, recursive):
829
850
  the cap). A GET path exists (`{ via: 'get' }`) and the SDK measures the
830
851
  serialized GET URL — over 2048 it throws `QueryTooLongError` (never a
831
852
  silent degradation).
832
- - `includeFields` passes through vendor column selection; the SDK never
833
- invents column selection — one exception: when `includeFields` is set the
834
- SDK always appends `id` (the vendor omits `id` from a row unless it is
835
- named in the selection, and every caller needs the id to act on the row —
836
- issue #59).
853
+ - **Own-field aliases (issue #352):** a single self prefix — the resource
854
+ key, or a relation name targeting that resource — is stripped from
855
+ ordinary filter fields, `includeFields`, and aggregate `groupBy` / `sum`.
856
+ On tickets, `ticket.title` and `tickets.title` mean `title`; on time
857
+ entries, `time_entries.hoursWorked` means `hoursWorked`, while
858
+ `ticket.companyID` remains a parent relation filter. Aggregate date
859
+ suffixes are preserved (`time_entries.dateWorked@month` becomes
860
+ `dateWorked@month`), and result keys use the canonical unprefixed names.
861
+ Foreign and multi-hop names are never converted into own-field aliases.
862
+ Projection normalization applies on the first page and every cursor
863
+ continuation, without mutating the caller's input.
864
+ - `includeFields` selects vendor columns after own-field normalization; the
865
+ SDK always appends `id` when omitted (the vendor omits `id` unless it is
866
+ selected, and callers need it to act on the row — issue #59).
837
867
 
838
868
  ## 6. Mutation safety
839
869
 
@@ -962,7 +992,48 @@ candidates and the error code when a resource could not resolve (empty
962
992
  the primary text field, exact match only; with `resolution.scanOnMiss` the
963
993
  bounded client-scan re-check (and its truncation cap) applies per resource. Returns
964
994
  `{ identifier, results: [{ resource, value, resolutionCost, scanned,
965
- scanTruncated, candidates, error? }], matches }`.
995
+ scanTruncated, candidates, error? }], matches, matched }`.
996
+
997
+ `match` defaults to `'exact'`. With `match: 'auto'`, every requested
998
+ resource finishes its exact lookup before a contains search can start. Only
999
+ a clean miss everywhere permits the contains sweep; an exact hit,
1000
+ ambiguity, truncated scan, unsupported resolver or lookup error suppresses
1001
+ it. Numeric ids and digit strings never fall back to contains. Auth,
1002
+ rate-limit, server, network, timeout and policy faults propagate.
1003
+
1004
+ The contains sweep defaults to the client’s `resolution.maxScanRecords`,
1005
+ capped at 500. Optional `limit` must be an integer 1..500 and can only
1006
+ narrow that configured cap; invalid values refuse before lookup wire.
1007
+ Exact lookup sizes are unchanged. A singleton below the cap resolves; multiple matches
1008
+ report candidate ids, and a cap-full result reports `scanTruncated: true`
1009
+ and `RESOLUTION_TRUNCATED`, even if the cap is one. `matched` reports
1010
+ `'exact'` or `'contains'`; only the latter includes the full sweep in
1011
+ `contains`, including an empty sweep.
1012
+
1013
+ **`resolveStaff(client, identifier, opts?)`** looks up Autotask resources
1014
+ by exact email, userName or name. Text matches require `isActive: true`
1015
+ and exclude API users by default, using tenant picklist labels on
1016
+ `licenseType` / `userType` rather than fixed numeric values. Numeric ids
1017
+ fetch directly and bypass active/API-user exclusions. `emailOnly: true`
1018
+ treats even digit strings as email; `includeApiUsers: true` includes API
1019
+ users in text matches. Options form a closed plain-object bag: flags must be
1020
+ booleans when present and limit must be an integer 1..10. Unknown keys, nulls
1021
+ and malformed options refuse with CONFIG_ERROR before metadata or lookup wire.
1022
+
1023
+ Returns `{ identifier, lookup: 'id' | 'text', matches: [{ record, apiUser?,
1024
+ licenseTypeLabel? }], matchedCount, truncated, metadataState }`. `limit`
1025
+ is 1..10 (default 10); the text lookup reads at most 50 records.
1026
+ `matchedCount` is after exclusions and before the candidate limit, and is
1027
+ a lower bound when truncated. A live cursor or a candidate-limit cut sets
1028
+ `truncated: true`; a truncated singleton never proves uniqueness. Text
1029
+ misses return an empty `matches`; a numeric miss propagates `NOT_FOUND`.
1030
+ `metadataState` is `ok` when API-user labels were found, `none` when labels
1031
+ exist without an API label, `absent` when no labels are exposed, or `error`
1032
+ when metadata could not be read. Lookup continues if metadata is
1033
+ unavailable, and API-user classification is omitted when unproven. Text
1034
+ records honor the existing client `labels` setting through the shared row-label
1035
+ pass; optional `names` reports incomplete label coverage. Numeric gets keep
1036
+ their ordinary record shape.
966
1037
 
967
1038
  ### 7.3 `invoke(client, operation, args?, opts?)` — the write governor
968
1039
 
@@ -989,7 +1060,8 @@ the mode pinned (§7.4). The governor rules (pinned):
989
1060
  `queryBounded`** (server-side pagination): ONE invoke call returns a
990
1061
  bounded, complete-enough result — the vendor cursor walk happens inside
991
1062
  the SDK; the caller passes `limit` as the TOTAL cap (default 500, max
992
- 2500) and never owns a stream. `queryAll` keeps its raw behavior (the
1063
+ 2500), or an independent `walk.maxRows` export cap (section 4), and never
1064
+ owns a stream. `queryAll` keeps its raw behavior (the
993
1065
  unbounded walk; the catalog text steers the caller away for normal work);
994
1066
  - a query's `includeFields` NAMES and its `filter` condition FIELD NAMES are
995
1067
  validated **pre-request** (issues #119b + #125): the authority is the
@@ -1059,25 +1131,9 @@ be cross-mode-refused.
1059
1131
  Pinned by the full-surface suite (`test/operations/invoke/modes.test.ts`:
1060
1132
  all 1281 ops × 3 modes, zero wire).
1061
1133
 
1062
- **Harness approval policy (recommended).** Require approval on every tool
1063
- whose MCP annotation is `readOnlyHint !== true` — today exactly **267
1064
- tools**: the 265 dedicated write/delete tools (209 `effect=write` + 56
1065
- `effect=destructive`, every one of them has a dedicated tool) +
1066
- `autotask_write` + `autotask_delete`. The remaining **635 tools** (the 622 dedicated read tools + the 10 core read tools + `autotask_read` +
1067
- `autotask_catalog` + `autotask_describe`) all carry `readOnlyHint: true`
1068
- and are safe ungated: every op they can route is `effect=read` (zero
1069
- mutating requests — pinned by the test inventory, §6 of the split design).
1070
- Refined class for the highest-risk ops: the **57 tools with
1071
- `destructiveHint: true`** (56 dedicated delete tools + `autotask_delete`)
1072
- additionally require `confirm === <operation key>` even inside
1073
- `autotask_delete`, and the 3 approval-gated writes
1074
- (`configuration_items.moveConfigurationItem`, `contacts.moveToCompany`,
1075
- `resources.transferOwnership`) require the same confirm inside
1076
- `autotask_write`. Migration: `autotask_invoke` is REPLACED by
1077
- `autotask_read`/`autotask_write`/`autotask_delete` — update harness config
1078
- (approve the 267 above) and remove any `autotask_invoke` entry. The rule is
1079
- derivable from the registry `effect` column (and the mode tools' fixed
1080
- annotations) — it never needs a hand-maintained name list.
1134
+ <!-- BEGIN GENERATED:approval-policy -->
1135
+ **Harness approval policy (recommended).** Require approval on every tool whose MCP annotation is `readOnlyHint !== true` — today exactly **267 tools**: the 265 dedicated write/delete tools (209 `effect=write` + 56 `effect=destructive`, every one of them has a dedicated tool) + `autotask_write` + `autotask_delete`. The remaining **636 tools** (the 623 dedicated read tools + the 10 core read tools + `autotask_read` + `autotask_catalog` + `autotask_describe`) all carry `readOnlyHint: true` and are safe ungated: every op they can route is `effect=read` (zero mutating requests). Refined class for the highest-risk ops: the **57 tools with `destructiveHint: true`** (56 dedicated delete tools + `autotask_delete`) additionally require `confirm === <operation key>` even inside `autotask_delete`. The approval-gated writes (`configuration_items.moveConfigurationItem`, `contacts.moveToCompany`, `resources.transferOwnership`) require the same confirm inside `autotask_write`. Migration: `autotask_invoke` is REPLACED by `autotask_read`/`autotask_write`/`autotask_delete` — update harness config and remove any `autotask_invoke` entry. The rule is derivable from the registry `effect` column and the mode tools’ fixed annotations; it never needs a hand-maintained name list.
1136
+ <!-- END GENERATED:approval-policy -->
1081
1137
 
1082
1138
  ### 7.5 `recovery(client, input, opts?)` — the one-call recovery/cleanup op (issue #9)
1083
1139
 
@@ -1282,10 +1338,12 @@ wire):
1282
1338
  vendor casing via `operations.fieldCatalog`). An empty array / absent = the
1283
1339
  unfiltered call (the vendor-accepted no-filter shape rides the wire as
1284
1340
  `filter: []`; that is also the blind-predicate's stamp case).
1285
- - **`groupBy` / `sum`** = exact vendor field names (validated against the
1341
+ - **`groupBy` / `sum`** = vendor field names, accepting the single own-field
1342
+ prefix aliases of section 5 (normalized before validation and folding;
1343
+ result keys are unprefixed). Names are validated against the
1286
1344
  resource's vendored field set pre-request — a hallucinated field is a shape
1287
1345
  error, not a silent fold-miss; the complete problem list in one typed error,
1288
- zero wire). Non-numeric/absent `sum` values contribute 0 (documented).
1346
+ zero wire. Non-numeric/absent `sum` values contribute 0 (documented).
1289
1347
  - **`top`** (default 25, clamped 1..100) keeps the top-N groups (descending
1290
1348
  count; the deterministic group-key order breaks ties). **`limit`** (default
1291
1349
  2500 = the house `BOUNDED_QUERY_MAX_LIMIT`, clamped down to it) is the
@@ -1709,7 +1767,7 @@ const lob = await revenueRollup(client, { tenantWide: true, dateFrom: '2025-07-0
1709
1767
  ## 8. Resource surface
1710
1768
 
1711
1769
  <!-- BEGIN GENERATED:api-per-entity -->
1712
- 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 16). 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).
1770
+ Standard methods (every resource, `BaseResource`): `query` (stream, 1000-page guarded), `queryBounded` (server-side pagination — one call walks the cursor until the total cap: `limit` (default 500 / max 2500), or independent `walk.maxRows` for exports (pages stay `min(500, limit)`); `hasMore` reports incomplete walks, and explicit walks also carry `truncated`; 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 16). 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).
1713
1771
 
1714
1772
  | resource key | apiEntity | kind | tier | parent | standard ops present | special ops | mcpTool |
1715
1773
  |--------------|-----------|------|------|--------|----------------------|-------------|---------|
@@ -2678,7 +2736,8 @@ metadata is (the `autotask_describe` channel, `capabilities.json`,
2678
2736
  `dateWorked` window (`from`/`to` are `YYYY-MM-DD` date strings — the S1c
2679
2737
  guard forbids Date positionals), one bounded read (`limit` 1-500, default
2680
2738
  500 — the hard cap, issue #128: a larger window needs `time_entries.query`
2681
- with the same `dateWorked` filter, its 2500-row walk cap) returning
2739
+ with the same `dateWorked` filter and a suitable `limit` or explicit
2740
+ `walk.maxRows` cap, section 4) returning
2682
2741
  `{ items, postedCount, unpostedCount, window, nextCursor, unconfirmed, truncated }`; the counts come
2683
2742
  from the billing-item join (any billing item referencing the entry), `opts.posted` narrows client-side after the join
2684
2743
  (true = posted only, false = unposted only, omitted = both); the counts still split every entry read (before the narrowing), so `items.length` can differ from their sum and `ticketID` /
@@ -3381,8 +3440,9 @@ additive, and mirrors the family:
3381
3440
  - **Shape and wire** — each op is exactly ONE bounded query (`MaxRecords`
3382
3441
  1-500, default 500 — the hard cap, issue #128: a call without `limit` takes
3383
3442
  the first 500 rows in id order; a window with more than 500 matching rows
3384
- needs the bounded query op with the same date-window filter — its 2500-row
3385
- walk cap; the `itemDate` / `invoiceDateTime` `gte`/`lte` filter rides the
3443
+ needs the bounded query op with the same date-window filter and a suitable
3444
+ `limit` or explicit `walk.maxRows` cap (section 4); the `itemDate` /
3445
+ `invoiceDateTime` `gte`/`lte` filter rides the
3386
3446
  wire), and the result is the plain `items[]` array — no counts
3387
3447
  (the time-entry family shape).
3388
3448
 
@@ -4011,6 +4071,35 @@ truncates first, then wraps) — so the explicit `… [+N chars truncated]`
4011
4071
  marker survives INSIDE the wrapped value and the closing marker is never
4012
4072
  cut off.
4013
4073
 
4074
+ **`sanitiseResult(client, result, operation)`** exposes that field projection
4075
+ as a zero-wire operations helper. Pass the registry key of the read that
4076
+ produced the result, for example `tickets.query`; an unknown key is a typed
4077
+ `CONFIG_ERROR`. It walks plain objects and arrays, wrapping strings in the
4078
+ operation's `metadata.untrustedFields`, including strings in arrays under
4079
+ those fields. `userDefinedFields[].value` strings are fenced even when the
4080
+ operation has no field stamp. Deeply nested containers are capped at 64
4081
+ levels with `[DEPTH]`.
4082
+ Inputs are never mutated, wrapping is idempotent, and non-string values
4083
+ and `[REDACTED]` pass through. SDK read primitives still return raw values.
4084
+
4085
+ This helper supplies field/UDF fencing. The host retains text cleaning,
4086
+ free-text caps and trusted formatting exemptions; apply those policies in
4087
+ the host before projection where needed. The bounded-query `noteChars`
4088
+ option remains a separate SDK cap applied before wrapping.
4089
+
4090
+ For hosts that need to clean or cap text before wrapping, the
4091
+ `node-autotask/untrusted` subpath exports
4092
+ `deepMarkUntrusted(value, fields, projector?)` and the callback type
4093
+ `UntrustedStringProjector = (text: string, target: object, key: string | number) => string`.
4094
+ The optional callback runs only for marked string values, including UDF
4095
+ values and strings in named arrays. `target` is the copied output
4096
+ container; `key` is its property name or array index, assigned after the
4097
+ callback returns. A host can record provenance in a `WeakMap` keyed by
4098
+ `target` and return the raw text, then clean, cap and wrap those marked
4099
+ values later. Containers exposed to the callback remain copies even if
4100
+ the callback returns unchanged text. Unmarked strings and non-string
4101
+ values do not invoke it; omitting the callback keeps the default wrapping.
4102
+
4014
4103
  ## 13. Doctor preflight (issue #2)
4015
4104
 
4016
4105
  `runDoctor(client)` (section 1.10) runs the bounded, sequential preflight
@@ -160,6 +160,8 @@ export function hitsOutput(): z.ZodType {
160
160
  /** `resolveAny`: one outcome per requested resource; misses do not throw. */
161
161
  export function resolveAnyOutput(): z.ZodType {
162
162
  return z.object({
163
+ matched: z.enum(['exact', 'contains']),
164
+ contains: hitsOutput().optional(),
163
165
  identifier: z.union([z.string(), z.number()]),
164
166
  results: z.array(
165
167
  crossDataField('Per-resource outcome: value (compact or null), resolutionCost, scanned, candidates/error on a miss.'),
@@ -84,7 +84,7 @@ export function registerReadTools({ server, client, dry, untrustedWrap }: ToolCo
84
84
  // R3 — find a record it cannot name.
85
85
  registerCoreRead(
86
86
  coreSpec('autotask_resolve_any')!,
87
- z.object({ identifier: IDENTIFIER, resources: RESOURCES }),
87
+ z.object({ identifier: IDENTIFIER, resources: RESOURCES, match: z.enum(['exact', 'auto']).optional(), limit: z.number().int().min(1).max(500).optional() }),
88
88
  resolveAnyOutput,
89
89
  (v) => `${(v as { matches: string[] }).matches.length} resource(s) matched the identifier.`,
90
90
  );
@@ -163,6 +163,7 @@ export function projectUntrustedResult(opId: string, value: unknown, fields: rea
163
163
  case 'operations.resolveAny':
164
164
  return {
165
165
  ...v,
166
+ ...(v.contains === undefined ? {} : { contains: projectUntrustedResult('operations.searchAcrossResources', v.contains, fields) }),
166
167
  results: (Array.isArray(v.results) ? v.results : []).map((row) =>
167
168
  row !== null && typeof row === 'object' && !Array.isArray(row) && 'value' in row
168
169
  ? { ...(row as Record<string, unknown>), value: deepMarkUntrusted((row as Record<string, unknown>).value, fields) }
@@ -212,7 +213,7 @@ export async function coreReadCall(client: AutotaskClient, opId: string, args: R
212
213
  case 'threshold_information.get':
213
214
  return { model: await client.thresholdInformation.get() };
214
215
  case 'operations.resolveAny':
215
- return resolveAny(client, args.identifier as number | string, { resources: args.resources as string[] | undefined });
216
+ return resolveAny(client, args.identifier as number | string, { resources: args.resources as string[] | undefined, match: args.match as 'exact' | 'auto' | undefined, limit: args.limit as number | undefined });
216
217
  case 'operations.searchAcrossResources':
217
218
  return searchAcrossResources(client, args.query as string, {
218
219
  resources: args.resources as string[] | undefined,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-autotask",
3
- "version": "0.43.0",
3
+ "version": "0.44.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,
@@ -161,18 +161,19 @@
161
161
  },
162
162
  "homepage": "https://github.com/msoukhomlinov/node-autotask#readme",
163
163
  "devDependencies": {
164
- "@eslint/js": "^9.17.0",
164
+ "@eslint/js": "^10.0.1",
165
165
  "@modelcontextprotocol/server": "^2.2.0",
166
- "@types/node": "^22.10.0",
167
- "@vitest/coverage-v8": "^4.1.12",
168
- "eslint": "^9.17.0",
169
- "globals": "^15.14.0",
166
+ "@types/node": "^26.6.6",
167
+ "@vitest/coverage-v8": "^5.0.5",
168
+ "eslint": "^10.13.0",
169
+ "globals": "^17.13.0",
170
170
  "rimraf": "^6.0.1",
171
- "tsup": "^8.3.0",
171
+ "tsup": "^9.0.0",
172
172
  "tsx": "^4.23.15",
173
- "typescript": "^5.7.0",
173
+ "typescript": "^6.0.3",
174
174
  "typescript-eslint": "^8.18.0",
175
- "vitest": "^4.1.12",
175
+ "vite": "^8.3.5",
176
+ "vitest": "^5.0.5",
176
177
  "zod": "^4.4.3"
177
178
  },
178
179
  "overrides": {