iit-mcp-autotask 0.35.1 → 0.38.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.
- package/README.md +41 -32
- package/capabilities.json +10 -44
- package/dist/mocks/fixture-ops.d.ts +2 -16
- package/dist/mocks/fixture-ops.js +0 -2
- package/dist/tools/aggregate.d.ts +15 -3
- package/dist/tools/aggregate.js +39 -35
- package/dist/tools/capabilities-manifest.js +0 -1
- package/dist/tools/catalog-data.d.ts +12 -5
- package/dist/tools/catalog-data.js +17 -16
- package/dist/tools/context/ops.js +3 -3
- package/dist/tools/context/types.d.ts +2 -14
- package/dist/tools/dispatch.d.ts +15 -0
- package/dist/tools/dispatch.js +90 -16
- package/dist/tools/lists.js +38 -169
- package/dist/tools/meta.d.ts +0 -2
- package/dist/tools/meta.js +105 -82
- package/dist/tools/provenance-scrub.d.ts +20 -0
- package/dist/tools/provenance-scrub.js +209 -0
- package/dist/tools/rank.d.ts +7 -1
- package/dist/tools/rank.js +5 -7
- package/dist/tools/reasons.d.ts +2 -1
- package/dist/tools/reasons.js +10 -0
- package/dist/tools/resolves.d.ts +14 -1
- package/dist/tools/resolves.js +121 -38
- package/dist/tools/results/envelope.d.ts +2 -1
- package/dist/tools/results/envelope.js +2 -2
- package/dist/tools/results/errors.js +2 -0
- package/dist/tools/results/untrusted.d.ts +10 -5
- package/dist/tools/results/untrusted.js +9 -5
- package/dist/tools/schemas.d.ts +35 -72
- package/dist/tools/schemas.js +58 -108
- package/dist/tools/scope-guard.js +4 -2
- package/dist/tools/server-version.d.ts +1 -1
- package/dist/tools/server-version.js +1 -1
- package/dist/tools/server.d.ts +2 -2
- package/dist/tools/server.js +9 -8
- package/dist/tools/surface.js +1 -4
- package/dist/tools/write-guidance.d.ts +6 -1
- package/dist/tools/write-guidance.js +21 -2
- package/package.json +2 -2
- package/server.json +2 -2
- package/skills/company-360.md +12 -5
- package/skills/contract-profitability.md +3 -3
- package/skills/filter-semantics.md +5 -5
- package/skills/slim-reads.md +3 -3
- package/skills/unbilled-work-review.md +5 -5
package/README.md
CHANGED
|
@@ -8,10 +8,10 @@ context into one call, a generic read layer for the long tail, one result envelo
|
|
|
8
8
|
packaged playbooks, strong security boundaries, and predictable per-tenant in-process state (pooled
|
|
9
9
|
client + SDK-owned TTL caches).
|
|
10
10
|
|
|
11
|
-
> **Package status (2026-10-
|
|
12
|
-
> at `0.
|
|
11
|
+
> **Package status (2026-10-29):** the runtime dependency `node-autotask` is published to public npm
|
|
12
|
+
> at `0.38.0`; the committed `pnpm-lock.yaml` pins that
|
|
13
13
|
> release with its integrity hash, so installs resolve deterministically. The dependency is always npm
|
|
14
|
-
> (node-autotask pinned exactly at `0.
|
|
14
|
+
> (node-autotask pinned exactly at `0.38.0`) — never a `file:`, `git:`, `link:` or workspace reference
|
|
15
15
|
> to a local checkout.
|
|
16
16
|
|
|
17
17
|
> **0.6.0 is a breaking release** (unreleased): tool names, result shapes and configuration all
|
|
@@ -19,15 +19,16 @@ client + SDK-owned TTL caches).
|
|
|
19
19
|
|
|
20
20
|
## Tool surface
|
|
21
21
|
|
|
22
|
-
The **core** toolset (
|
|
23
|
-
`finance` (or `all`,
|
|
24
|
-
listed below. Every other operation in the SDK's 1,
|
|
22
|
+
The **core** toolset (10 tools) is always served; `AUTOTASK_TOOLSETS` adds `service`, `records` and
|
|
23
|
+
`finance` (or `all`, 17 tools); the operator profile adds two. The whole tool surface (19 tools) is
|
|
24
|
+
listed below. Every other operation in the SDK's 1,281-operation registry stays
|
|
25
25
|
reachable through the generic layer: `autotask_catalog` finds the key, `autotask_describe` serves
|
|
26
|
-
its contract,
|
|
26
|
+
its contract (slim by default: transport and retry detail dropped, long text cut, the first two
|
|
27
|
+
examples; `full: true` serves the complete record, `summary: true` about 2 KB), `autotask_read` runs it.
|
|
27
28
|
|
|
28
29
|
| Toolset | Tools |
|
|
29
30
|
|---------|-------|
|
|
30
|
-
| core (always) | `autotask_guide` · `autotask_catalog` · `autotask_describe` · `autotask_read` · `autotask_resolve` · `autotask_query` · `
|
|
31
|
+
| core (always) | `autotask_guide` · `autotask_catalog` · `autotask_describe` · `autotask_read` · `autotask_resolve` · `autotask_query` · `autotask_tenant_status` · `autotask_ticket_full_context` · `autotask_company_full_profile` · `autotask_resource_workload` |
|
|
31
32
|
| `service` | `autotask_project_get_context` · `autotask_service_call_full_profile` |
|
|
32
33
|
| `records` | `autotask_contact_full_profile` · `autotask_configuration_item_profile` · `autotask_contract_full_profile` |
|
|
33
34
|
| `finance` | `autotask_quote_get` · `autotask_invoice_get` (`format`: `record`, `html`, `xml`, `pdf`) |
|
|
@@ -35,12 +36,24 @@ its contract, `autotask_read` runs it.
|
|
|
35
36
|
|
|
36
37
|
- `autotask_resolve` turns any identifier into a record: an id or name, `by: "ticket_number"`,
|
|
37
38
|
`"serial"` or `"email"`, in one `resource` or swept across the five workflow resources;
|
|
38
|
-
`match: "
|
|
39
|
+
the default `match: "auto"` tries exact, then contains on a miss for companies, tickets (title) or a
|
|
40
|
+
sweep (one hit: `ok` with `matched: "contains"`; several: `ambiguous`), `match: "exact"` has no
|
|
41
|
+
fallback, `match: "contains"` searches only. `resource: "resources"` finds staff by
|
|
39
42
|
email, user name or name (active only; API users, which carry their owner's email, are excluded unless
|
|
40
43
|
`include_api_users: true`; `by: "email"` narrows to the email field; a numeric identifier is looked up
|
|
41
|
-
as a Resource id); compact rows carry `apiUser` and the licence label when the tenant's labels tell it, never an email. Every match carries `ref`, its human reference. A sweep returns compact matches (`resource`, `ref`, `id`, the human fields) unless `expand: true` (the
|
|
42
|
-
- `autotask_query` is the bounded filtered list over `resource`:
|
|
43
|
-
`
|
|
44
|
+
as a Resource id); compact rows carry `apiUser` and the licence label when the tenant's labels tell it, never an email. Every match carries `ref`, its human reference, as safe text: control and markup characters (`< > { } [ ] \` " | \\`) dropped, whitespace collapsed, at most 80 characters (a cut ends with `…`), never wrapped; still vendor-authored data (#230). A sweep returns compact matches (`resource`, `ref`, `id`, the human fields) unless `expand: true` (the record under `record`, without its `[REDACTED]`, null and empty fields; `expand: "full"` keeps them); `limit` caps the TOTAL matches across resources (resource order, then the SDK's order) and a contains sweep reports `truncated` and per-resource `counts {returned, found}`.
|
|
45
|
+
- `autotask_query` is the bounded filtered list over `resource`: any resource with a
|
|
46
|
+
`<resource>.query` read (`tickets`, `time_entries`, `service_calls`, `contracts`,
|
|
47
|
+
`time_off_requests`, `companies`, `billing_items`, ...; limit default 50, max 2500). With
|
|
48
|
+
`group_by`, `sum` or `count: true` it answers totals instead of rows (the SDK's
|
|
49
|
+
`operations.aggregate`; `top` 1-100 groups, `limit` up to 2,500 scanned rows; `hasMore` /
|
|
50
|
+
`groupsTruncated` mean partial). Records by id: `filter: [{field: "id", op: "in", value: "1,2,3"}]`
|
|
51
|
+
(`in` / `notIn` take an array or a comma string). Time entries have no `companyID`: the relation
|
|
52
|
+
filter `{field: "ticket.companyID", op: "eq", value: "<id>"}` (also `task.<field>`; `project.<field>`
|
|
53
|
+
where the entity has the FK) scopes them through one parent read of at most 300 ids (more is a typed
|
|
54
|
+
refusal; no parent match is an empty result with a `relationNote`), in rows, counts and totals.
|
|
55
|
+
The separate `autotask_aggregate` and `autotask_fetch_many` tools are removed (#228, 0.37.0); a
|
|
56
|
+
mixed-resource batch by id is `autotask_read` `operations.fetchByIds`. The served filter
|
|
44
57
|
schema declares each condition as `{field, op, value}` strings, so a client that builds one type
|
|
45
58
|
per array column writes `"29"`, `"true"`, `"2026-10-22"`; numbers, `in` arrays and and/or groups are
|
|
46
59
|
still accepted and passed through unchanged.
|
|
@@ -51,11 +64,10 @@ its contract, `autotask_read` runs it.
|
|
|
51
64
|
fields, because form-filling clients (Copilot Studio) fill every optional input they are shown. A
|
|
52
65
|
client that cannot send a mixed-type array asks for a time entry window over a resource with
|
|
53
66
|
`autotask_query` `time_entries` (`resourceID` eq, `dateWorked` gte/lte, every value a string: the
|
|
54
|
-
served filter schema types `value` as a string). `autotask_read`
|
|
67
|
+
served filter schema types `value` as a string). `autotask_read` answers ONE envelope (the next minor,
|
|
55
68
|
breaking): `data = { results: Slot[], ok, failed, budget?, budgetWarning? }`, an array in input
|
|
56
69
|
order even for a single read (`data.results[0].result`). A slot is `{ ok: true, operation, result,
|
|
57
|
-
windowNote?, scopeNote? }` or `{ ok: false, operation, error }
|
|
58
|
-
(`operation` is `<resource>.get`; a miss is `ok: false` with `NOT_FOUND`). A single read that fails
|
|
70
|
+
windowNote?, scopeNote? }` or `{ ok: false, operation, error }`. A single read that fails
|
|
59
71
|
still answers the whole-call error envelope.
|
|
60
72
|
- `autotask_guide` serves the packaged playbooks — the server's skill layer (below).
|
|
61
73
|
|
|
@@ -96,7 +108,7 @@ part miss never fails the call (typed `absent`); only a missing root record fail
|
|
|
96
108
|
(`src/tools/reasons.ts`). Vendor free text is wrapped in `<untrusted_content>`,
|
|
97
109
|
stripped of every control (except newline and tab), format and default-ignorable character (bidi,
|
|
98
110
|
zero-width, tag characters, variation selectors) and capped at 500 characters
|
|
99
|
-
(`
|
|
111
|
+
(`textCapped: true` on the envelope; row cuts are `hasMore`); the wrapper is the injection control — the angle-bracket
|
|
100
112
|
escape earlier builds added was removed in 0.9.0 (issue #70): it was pure cost for LLM consumers
|
|
101
113
|
and broke read-modify-write round-trips, so wrapped text carries raw `&`/`<`/`>` and is data,
|
|
102
114
|
never instructions. User-defined field values get the same treatment; `output_path` files keep the
|
|
@@ -336,18 +348,15 @@ decoded bytes for `pdf`) under the same root rules — never a JSON envelope, ne
|
|
|
336
348
|
to the exact vendor markup); a `pdf` past the vendor 1 MiB inline limit is truncated and carries no
|
|
337
349
|
data (and no file).
|
|
338
350
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
`
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
`
|
|
348
|
-
parent-scoped resources (`contacts` is one — its get needs (parentId, id)) before the request,
|
|
349
|
-
after the query wire is spent; contact names stay reachable via `autotask_resolve` and the
|
|
350
|
-
composite tools, and a scoped contact read is a follow-up.
|
|
351
|
+
**Labels come with the rows** (node-autotask 0.38.0, #227): query rows, `group_by` keys and
|
|
352
|
+
fetched records carry SDK label siblings by default: picklists as `<field>Label` (`statusLabel`),
|
|
353
|
+
ids as `<field>Name` (`companyIDName`, `contractIDName`, `resourceIDName`, `ticketIDName`, ...;
|
|
354
|
+
`assignedResourceName` stays as a deprecated alias for one release). Each name is capped at 200
|
|
355
|
+
characters and wrapped once as untrusted content; since node-autotask 0.39.0 the sibling key is always present, `null` when the id is null, 0 or unresolved (or the picklist value is null); a `names` note says when
|
|
356
|
+
the SDK's cold-read cap left something unlabelled; `names: false` turns labels off. The server-side
|
|
357
|
+
`enrich` parameter is removed (0.37.0): a call that still sends it is refused as an unknown input; read
|
|
358
|
+
the SDK label instead (`companyIDName` for the old `companyName`, `resourceIDName` for `resourceName`,
|
|
359
|
+
`ticketIDName` for `ticketName`).
|
|
351
360
|
|
|
352
361
|
## Transports
|
|
353
362
|
|
|
@@ -763,7 +772,7 @@ independent review with its resolutions, [`docs/reviews/0.6.0.md`](docs/reviews/
|
|
|
763
772
|
|
|
764
773
|
## SDK dependency
|
|
765
774
|
|
|
766
|
-
`iit-mcp-autotask` depends on **`node-autotask` from npm at exactly `0.
|
|
775
|
+
`iit-mcp-autotask` depends on **`node-autotask` from npm at exactly `0.38.0`** (an exact pin —
|
|
767
776
|
no range operator; subpaths `.` / `/capabilities` / `/operations` / `/resources` / `/types` /
|
|
768
777
|
`/pool`) — the only **vendor** dependency; the other two runtime dependencies are
|
|
769
778
|
`@modelcontextprotocol/server` (the MCP framework) and `zod` (schema validation). It never
|
|
@@ -773,7 +782,7 @@ user — the published tarball ships no lockfile) must resolve to the release th
|
|
|
773
782
|
reviewed against, or the plan-hash boot check refuses to start.
|
|
774
783
|
|
|
775
784
|
**Publishing `node-autotask` is the maintainer's action, done manually.** It is published on the
|
|
776
|
-
public registry at `0.
|
|
785
|
+
public registry at `0.37.0` (published 2026-10-28 UTC); the
|
|
777
786
|
exact pin and the committed `pnpm-lock.yaml` (with `pnpm-workspace.yaml`) pin the real published
|
|
778
787
|
tarball with its integrity hash, so installs resolve to exactly that release — the lockfile
|
|
779
788
|
freezes the pnpm/git/Docker paths, the pin freezes the npm/npx paths. The docs are written against this
|
|
@@ -867,9 +876,9 @@ physical assets (Autotask models them as Configuration Items — covered).
|
|
|
867
876
|
|
|
868
877
|
| # | Item | Status |
|
|
869
878
|
|---|------|--------|
|
|
870
|
-
| B1 | **Project phase & task lists** (W7 sub-capability): no `project_tasks`/`project_phases` operations in the SDK's 1,
|
|
879
|
+
| B1 | **Project phase & task lists** (W7 sub-capability): no `project_tasks`/`project_phases` operations in the SDK's 1,281-op registry. W7 ships with project status + context; phase/task lists excluded and documented. Classification (SDK gap vs vendor API-version limit) is decided by recon's spec-side check (A2). | blocked — documented |
|
|
871
880
|
| B2 | **Time-entry writes on ww29/australia-nz for API-only users**: vendor 500 (live-verified). Time-entry creation works where the API user is a real resource, through the operator profile (`log-time-entry`); on ww29 the write is classified `failed` (a 500 with a vendor `errors[]` body), never `unknown`. | zone limitation |
|
|
872
|
-
| B3 | **`time_entries.getPosted`/`getUnposted`: the zone's TimeEntry entity has no `postedDate`**, so SDK 0.31 and earlier 500ed (and `postedWithin`/`dateWorkedWindow` silently counted every entry unposted). SDK 0.32.0 derives posted state from a `billing_items.timeEntryID` join and returns `{ items, hasMore, window, approvedCount, unapprovedCount }` (optional date-only `dateFrom`/`dateTo`; no dates reads only the first 500 entries). Needs BillingItems read permission (one extra request per call); `approvedCount`/`unapprovedCount` are not posted state. Reachable via the generic layer. | resolved in SDK 0.32.0 (not re-verified live) |
|
|
881
|
+
| B3 | **`time_entries.getPosted`/`getUnposted`: the zone's TimeEntry entity has no `postedDate`**, so SDK 0.31 and earlier 500ed (and `postedWithin`/`dateWorkedWindow` silently counted every entry unposted). SDK 0.32.0 derives posted state from a `billing_items.timeEntryID` join and returns `{ items, hasMore, window, approvedCount, unapprovedCount }` (optional date-only `dateFrom`/`dateTo`; no dates reads only the first 500 entries). Needs BillingItems read permission (one extra request per call); `approvedCount`/`unapprovedCount` are not posted state. SDK 0.37.0 adds `cursor`/`nextCursor` (page past 500 entries), `unconfirmed` (entries whose posted state is unknown, kept out of both counts) and `countOnly: true` on the four posted-state ops. Reachable via the generic layer. | resolved in SDK 0.32.0 (not re-verified live) |
|
|
873
882
|
| B4 | **CI create on tenants without products** (ww29 product wall: `products.count = 0`): `configuration_items.create` requires an existing `productID`. No dedicated CI-create in v1; the wall is documented on the generic side. | zone limitation |
|
|
874
883
|
| B5 | **Blind count/query surfaces on ww29** (companies not owned by the caller; tickets): count/query return 0/empty even when data exists. Not fixable client-side; the surface carries the typed blind-surface sentinel — `get(id)`/full-profile are the authorities. | zone limitation |
|
|
875
884
|
| B6 | **`node-autotask` npm publish**: the Phase-C install gate re-checks the registry; if still 404 the build pauses and escalates (SCOPING-resolved: the user publishes — no git/file dependency fallback). | open — user action |
|
package/capabilities.json
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
"domain": "autotask",
|
|
4
4
|
"description": "Machine-readable capability catalogue for iit-mcp-autotask: the tools the server can serve, with the toolset and the lowest profile that serves each. Everything else in the registry is reachable through the generic layer (autotask_catalog / autotask_describe / autotask_read) and documented in docs/capability-matrix.md. Generated from src/tools/surface.ts by scripts/build-capabilities.mjs.",
|
|
5
5
|
"exposedToolCount": {
|
|
6
|
-
"readonlyCore":
|
|
7
|
-
"readonlyAll":
|
|
8
|
-
"operatorCore":
|
|
9
|
-
"operatorAll":
|
|
6
|
+
"readonlyCore": 10,
|
|
7
|
+
"readonlyAll": 17,
|
|
8
|
+
"operatorCore": 12,
|
|
9
|
+
"operatorAll": 19
|
|
10
10
|
},
|
|
11
11
|
"profiles": {
|
|
12
12
|
"readonly": {
|
|
@@ -24,8 +24,6 @@
|
|
|
24
24
|
"autotask_read",
|
|
25
25
|
"autotask_resolve",
|
|
26
26
|
"autotask_query",
|
|
27
|
-
"autotask_aggregate",
|
|
28
|
-
"autotask_fetch_many",
|
|
29
27
|
"autotask_tenant_status",
|
|
30
28
|
"autotask_ticket_full_context",
|
|
31
29
|
"autotask_company_full_profile",
|
|
@@ -69,7 +67,7 @@
|
|
|
69
67
|
},
|
|
70
68
|
{
|
|
71
69
|
"id": "autotask.platform.catalog",
|
|
72
|
-
"description": "Find the exact operation key for anything without a dedicated tool: ranked search (q) over the 1,
|
|
70
|
+
"description": "Find the exact operation key for anything without a dedicated tool: ranked search (q) over the 1,281 registry operations, with effect and whether this deployment serves it. No vendor call.",
|
|
73
71
|
"mcpPrimitive": "tool",
|
|
74
72
|
"toolName": "autotask_catalog",
|
|
75
73
|
"type": "read",
|
|
@@ -101,7 +99,7 @@
|
|
|
101
99
|
},
|
|
102
100
|
{
|
|
103
101
|
"id": "autotask.generic.read",
|
|
104
|
-
"description": "Run reads by exact registry key, or up to 10 in one call: unbilled work, profitability and margin, SLA health. *.query: 50 rows; scope by company/contract ids (unscoped = tenant walk);
|
|
102
|
+
"description": "Run reads by exact registry key, or up to 10 in one call: unbilled work, profitability and margin, SLA health. *.query: 50 rows; scope by company/contract ids (unscoped = tenant walk); totals: autotask_query group_by; slim: includeFields, noteChars.",
|
|
105
103
|
"mcpPrimitive": "tool",
|
|
106
104
|
"toolName": "autotask_read",
|
|
107
105
|
"type": "read",
|
|
@@ -117,7 +115,7 @@
|
|
|
117
115
|
},
|
|
118
116
|
{
|
|
119
117
|
"id": "autotask.discovery.resolve",
|
|
120
|
-
"description": "Find the record
|
|
118
|
+
"description": "Find the record for an identifier: id or name, ticket number, CI serial, contact email (by), staff by email, user name or name (resource resources, include_api_users); one resource or a sweep. Exact, then contains on a miss.",
|
|
121
119
|
"mcpPrimitive": "tool",
|
|
122
120
|
"toolName": "autotask_resolve",
|
|
123
121
|
"type": "read",
|
|
@@ -133,45 +131,13 @@
|
|
|
133
131
|
},
|
|
134
132
|
{
|
|
135
133
|
"id": "autotask.discovery.query",
|
|
136
|
-
"description": "List records with a filter: tickets, time entries, service calls, contracts
|
|
134
|
+
"description": "List records or totals with a filter: tickets, time entries, service calls, contracts, time off, more. Totals, no rows: group_by, sum, count (hours logged by ticket, how many match). By id: id in. Scope by company ids (unscoped = tenant walk).",
|
|
137
135
|
"mcpPrimitive": "tool",
|
|
138
136
|
"toolName": "autotask_query",
|
|
139
137
|
"type": "read",
|
|
140
138
|
"toolset": "core",
|
|
141
139
|
"profile": "readonly",
|
|
142
|
-
"backingOperation": "tickets.query | time_entries.query | service_calls.query | contracts.query | time_off_requests.query",
|
|
143
|
-
"annotations": {
|
|
144
|
-
"readOnly": true,
|
|
145
|
-
"idempotent": true,
|
|
146
|
-
"destructive": false,
|
|
147
|
-
"openWorld": true
|
|
148
|
-
}
|
|
149
|
-
},
|
|
150
|
-
{
|
|
151
|
-
"id": "autotask.core.aggregate",
|
|
152
|
-
"description": "Totals, counts and group-by sums (hours by ticket) over any <resource>.query table without reading rows. No groupBy or sum = one count call. hasMore or groupsTruncated = a partial answer, never a tenant total.",
|
|
153
|
-
"mcpPrimitive": "tool",
|
|
154
|
-
"toolName": "autotask_aggregate",
|
|
155
|
-
"type": "read",
|
|
156
|
-
"toolset": "core",
|
|
157
|
-
"profile": "readonly",
|
|
158
|
-
"backingOperation": "operations.aggregate",
|
|
159
|
-
"annotations": {
|
|
160
|
-
"readOnly": true,
|
|
161
|
-
"idempotent": true,
|
|
162
|
-
"destructive": false,
|
|
163
|
-
"openWorld": true
|
|
164
|
-
}
|
|
165
|
-
},
|
|
166
|
-
{
|
|
167
|
-
"id": "autotask.discovery.fetch_many",
|
|
168
|
-
"description": "Fetch up to 500 records by numeric id across mixed resources in one call: items: [{resource, id, fields?}]. Answers data.results[{ok, operation, result|error}], ok, failed (same as autotask_read); a miss is a typed NOT_FOUND slot.",
|
|
169
|
-
"mcpPrimitive": "tool",
|
|
170
|
-
"toolName": "autotask_fetch_many",
|
|
171
|
-
"type": "read",
|
|
172
|
-
"toolset": "core",
|
|
173
|
-
"profile": "readonly",
|
|
174
|
-
"backingOperation": "operations.fetchByIds",
|
|
140
|
+
"backingOperation": "tickets.query | time_entries.query | service_calls.query | contracts.query | time_off_requests.query | operations.aggregate",
|
|
175
141
|
"annotations": {
|
|
176
142
|
"readOnly": true,
|
|
177
143
|
"idempotent": true,
|
|
@@ -395,7 +361,7 @@
|
|
|
395
361
|
"autotask_describe",
|
|
396
362
|
"autotask_read"
|
|
397
363
|
],
|
|
398
|
-
"backingRegistryOperations":
|
|
364
|
+
"backingRegistryOperations": 1281,
|
|
399
365
|
"note": "autotask_catalog finds the exact operation key (ranked search), autotask_describe serves its contract, autotask_read runs any effect=read key. Writes go through the operator profile (autotask_write then autotask_apply); authenticate.* is never served."
|
|
400
366
|
},
|
|
401
367
|
"schemas": {}
|
|
@@ -20,7 +20,7 @@ import type { InvokeOptions } from 'node-autotask/operations';
|
|
|
20
20
|
import type { HourlyBudget, QueryWalkParams, QueryWalkResult, ToolClient, ToolOps } from '../tools/context.js';
|
|
21
21
|
/** One recorded ops invocation (the handler-driven vendor-call view). */
|
|
22
22
|
export interface OpCall {
|
|
23
|
-
method: 'invokeRead' | 'invokeWrite' | 'invokeDelete' | 'resolveAny' | 'searchAcrossResources' | '
|
|
23
|
+
method: 'invokeRead' | 'invokeWrite' | 'invokeDelete' | 'resolveAny' | 'searchAcrossResources' | 'relationshipMap' | 'queryWalk' | 'hourlyBudget';
|
|
24
24
|
client: ToolClient;
|
|
25
25
|
/** The operation key (invoke*) or the helper's primary argument. */
|
|
26
26
|
target: string;
|
|
@@ -35,7 +35,7 @@ export type FixtureBehaviour = {
|
|
|
35
35
|
kind: 'error';
|
|
36
36
|
error: () => unknown;
|
|
37
37
|
}
|
|
38
|
-
/** The helper-shaped outcomes (resolveAny / searchAcrossResources
|
|
38
|
+
/** The helper-shaped outcomes (resolveAny / searchAcrossResources). */
|
|
39
39
|
| {
|
|
40
40
|
kind: 'helper';
|
|
41
41
|
value: () => unknown;
|
|
@@ -83,20 +83,6 @@ export declare class MockToolOps implements ToolOps {
|
|
|
83
83
|
resources?: readonly string[];
|
|
84
84
|
limit?: number;
|
|
85
85
|
}, signal?: AbortSignal) => Promise<unknown>;
|
|
86
|
-
fetchMany: (client: ToolClient, items: readonly Readonly<{
|
|
87
|
-
resource: string;
|
|
88
|
-
id: number;
|
|
89
|
-
fields?: string[];
|
|
90
|
-
}>[], opts?: {
|
|
91
|
-
maxItems?: number;
|
|
92
|
-
}, signal?: AbortSignal) => Promise<unknown>;
|
|
93
|
-
fetchByIds: (client: ToolClient, items: readonly Readonly<{
|
|
94
|
-
resource: string;
|
|
95
|
-
id: number | string;
|
|
96
|
-
fields?: string[];
|
|
97
|
-
}>[], opts?: {
|
|
98
|
-
maxItems?: number;
|
|
99
|
-
}, signal?: AbortSignal) => Promise<unknown>;
|
|
100
86
|
relationshipMap: (client: ToolClient, resourceKey: string) => Promise<unknown>;
|
|
101
87
|
/** The `output_path` walk (target = the query operation key). */
|
|
102
88
|
queryWalk: (client: ToolClient, operation: string, params: QueryWalkParams, maxRows: number, signal?: AbortSignal) => Promise<QueryWalkResult | undefined>;
|
|
@@ -108,8 +108,6 @@ export class MockToolOps {
|
|
|
108
108
|
invokeDelete = (client, operation, args, opts, signal) => this.answer(client, 'invokeDelete', operation, args, opts, signal);
|
|
109
109
|
resolveAny = (client, identifier, opts, signal) => this.answer(client, 'resolveAny', 'resolveAny', [identifier, opts?.resources], undefined, signal);
|
|
110
110
|
searchAcrossResources = (client, query, opts, signal) => this.answer(client, 'searchAcrossResources', 'searchAcrossResources', [query, opts?.resources, opts?.limit], undefined, signal);
|
|
111
|
-
fetchMany = (client, items, opts, signal) => this.answer(client, 'fetchMany', 'fetchMany', [items, opts?.maxItems], undefined, signal);
|
|
112
|
-
fetchByIds = (client, items, opts, signal) => this.answer(client, 'fetchByIds', 'fetchByIds', [items, opts?.maxItems], undefined, signal);
|
|
113
111
|
relationshipMap = (client, resourceKey) => this.answer(client, 'relationshipMap', 'relationshipMap', [resourceKey], undefined, undefined);
|
|
114
112
|
/** The `output_path` walk (target = the query operation key). */
|
|
115
113
|
queryWalk = (client, operation, params, maxRows, signal) => this.answer(client, 'queryWalk', operation, [params, maxRows], undefined, signal);
|
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
import type { ToolContext } from './context.js';
|
|
2
|
-
import type
|
|
3
|
-
/**
|
|
4
|
-
export
|
|
2
|
+
import { type ToolReturn } from './results.js';
|
|
3
|
+
/** The SDK aggregate opts this server passes (the SDK's own names). */
|
|
4
|
+
export interface AggregateOpts {
|
|
5
|
+
groupBy?: string[];
|
|
6
|
+
sum?: string[];
|
|
7
|
+
top?: number;
|
|
8
|
+
limit?: number;
|
|
9
|
+
/** false: no label siblings on the group keys (SDK default on). */
|
|
10
|
+
names?: boolean;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Counts, totals and group-by sums over one `<resource>.query` surface: `autotask_query` with
|
|
14
|
+
* group_by / sum / count (#228). `extra` rides the data.
|
|
15
|
+
*/
|
|
16
|
+
export declare function runAggregate(ctx: ToolContext, operation: string, rawFilter: unknown, opts: AggregateOpts, signal: AbortSignal | undefined, extra?: Record<string, unknown>): Promise<ToolReturn>;
|
package/dist/tools/aggregate.js
CHANGED
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* tools/aggregate.ts — `
|
|
3
|
-
* `operations.aggregate`,
|
|
4
|
-
* call to a tool
|
|
2
|
+
* tools/aggregate.ts — the totals mode of `autotask_query` (`group_by` / `sum` / `count`; #197, #228):
|
|
3
|
+
* a thin wrapper over the SDK's `operations.aggregate`, so a roll-up question (totals, counts, group-by
|
|
4
|
+
* sums) is one call to a core tool, not a ~50 KB discovery trip through catalog, describe and read.
|
|
5
5
|
*
|
|
6
6
|
* Nothing is reimplemented: the SDK validates the operation, the field names and the caps (top 1-100,
|
|
7
7
|
* limit at most 2,500), folds the rows and reports its own truncation honesty (`hasMore`,
|
|
8
|
-
* `groupsTruncated`, `totalScanned`, `blind`); the
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* `groupsTruncated`, `totalScanned`, `blind`); the answer passes through unchanged. What this adds is
|
|
9
|
+
* the gates every read passes (the deployment policy on both the helper and the target surface, the
|
|
10
|
+
* inbound untrusted-wrapper strip, the filter shape) and the untrusted marking of the answer by the
|
|
11
|
+
* TARGET resource's field stamps, the same list `autotask_read` of `<resource>.query` uses.
|
|
12
12
|
*/
|
|
13
13
|
import { AutotaskConfigError } from 'node-autotask';
|
|
14
14
|
import { stripUntrustedDeep } from 'node-autotask/untrusted';
|
|
15
|
-
import {
|
|
15
|
+
import { assertFilterShape } from './schemas.js';
|
|
16
16
|
import { abortedAnswer } from './dispatch.js';
|
|
17
17
|
import { markResult, ok } from './results.js';
|
|
18
18
|
import { assertOperationPermitted } from '../policy.js';
|
|
19
|
+
import { scopedCatalogCall } from './field-pointer.js';
|
|
19
20
|
/** The server line for an SDK aggregate answer: numbers only, and the partial marker whenever the SDK flags truncation. */
|
|
20
21
|
function summaryLine(operation, result) {
|
|
21
22
|
const r = (result ?? {});
|
|
@@ -28,32 +29,35 @@ function summaryLine(operation, result) {
|
|
|
28
29
|
const blindNote = r.blindSurface !== undefined && r.blindSurface !== false ? ' blindSurface: the surface could not be read, an empty result is not a true zero.' : '';
|
|
29
30
|
return `${operation}: ${scanned}${groups}.${blindNote}${partial ? ' PARTIAL: the scan or the group list was cut short (hasMore / groupsTruncated); these are not tenant-wide totals, narrow the filter.' : ''}`;
|
|
30
31
|
}
|
|
31
|
-
/**
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
};
|
|
32
|
+
/**
|
|
33
|
+
* Counts, totals and group-by sums over one `<resource>.query` surface: `autotask_query` with
|
|
34
|
+
* group_by / sum / count (#228). `extra` rides the data.
|
|
35
|
+
*/
|
|
36
|
+
export async function runAggregate(ctx, operation, rawFilter, opts, signal, extra = {}) {
|
|
37
|
+
const helper = 'operations.aggregate';
|
|
38
|
+
assertOperationPermitted(helper, ctx.record(helper), ctx.config, ctx.log);
|
|
39
|
+
const target = ctx.record(operation);
|
|
40
|
+
if (target === undefined) {
|
|
41
|
+
throw new AutotaskConfigError(`aggregate: unknown operation "${operation}": it is not in the capability registry (find the exact key with autotask_catalog)`, { operation: helper });
|
|
42
|
+
}
|
|
43
|
+
assertOperationPermitted(operation, target, ctx.config, ctx.log);
|
|
44
|
+
assertFilterShape(rawFilter, helper);
|
|
45
|
+
// the conditions array (a params bag's own filter when the caller sent the bag form)
|
|
46
|
+
const conditions = Array.isArray(rawFilter) ? rawFilter : (rawFilter.filter ?? []);
|
|
47
|
+
const filter = stripUntrustedDeep(conditions);
|
|
48
|
+
// The SDK's positional form: [operation, filter] and the opts bag only when a fold or cap was set.
|
|
49
|
+
const bag = {};
|
|
50
|
+
for (const key of ['groupBy', 'sum', 'top', 'limit', 'names'])
|
|
51
|
+
if (opts[key] !== undefined)
|
|
52
|
+
bag[key] = opts[key];
|
|
53
|
+
const answer = await ctx.ops.invokeRead(ctx.client, helper, Object.keys(bag).length > 0 ? [operation, filter, bag] : [operation, filter], undefined, signal).catch((err) => {
|
|
54
|
+
// node-autotask #280: a bad filter / groupBy / sum name is a zero-wire CONFIG_ERROR; point at the scoped field catalog
|
|
55
|
+
if (!(err instanceof AutotaskConfigError) || !err.message.includes('is not a field of'))
|
|
56
|
+
throw err;
|
|
57
|
+
throw new AutotaskConfigError(err.message, { operation: helper, suggestedAction: `Use exact vendor field names: ${scopedCatalogCall(operation.slice(0, operation.indexOf('.')))}.` });
|
|
58
|
+
});
|
|
59
|
+
if (answer === undefined)
|
|
60
|
+
return abortedAnswer(ctx.correlationId);
|
|
61
|
+
return ok(summaryLine(operation, answer), { ...extra, operation, result: markResult(answer, ctx.untrustedFields(operation)) });
|
|
58
62
|
}
|
|
59
63
|
//# sourceMappingURL=aggregate.js.map
|
|
@@ -9,7 +9,6 @@ const CAPABILITY_IDS = {
|
|
|
9
9
|
autotask_read: 'autotask.generic.read',
|
|
10
10
|
autotask_resolve: 'autotask.discovery.resolve',
|
|
11
11
|
autotask_query: 'autotask.discovery.query',
|
|
12
|
-
autotask_fetch_many: 'autotask.discovery.fetch_many',
|
|
13
12
|
autotask_tenant_status: 'autotask.identity.tenant_status',
|
|
14
13
|
autotask_ticket_full_context: 'autotask.ticket.full_context',
|
|
15
14
|
autotask_company_full_profile: 'autotask.company.full_profile',
|
|
@@ -19,6 +19,11 @@ export interface CatalogRow {
|
|
|
19
19
|
reach: string | null;
|
|
20
20
|
reachReason: string | null;
|
|
21
21
|
boundedAlternative: string | null;
|
|
22
|
+
/** node-autotask 0.38.0 (#277): a repeated date-window helper, left out of default listings (still callable by key). */
|
|
23
|
+
hidden?: boolean;
|
|
24
|
+
hiddenReason?: string;
|
|
25
|
+
/** node-autotask 0.38.0: superseded (time_entries.recentTickets). */
|
|
26
|
+
deprecated?: boolean;
|
|
22
27
|
}
|
|
23
28
|
export interface MetaToolSpec {
|
|
24
29
|
name: string;
|
|
@@ -56,6 +61,8 @@ export declare function recordById(operation: string): CapabilityRecord | undefi
|
|
|
56
61
|
* name the deployment does not serve.
|
|
57
62
|
*/
|
|
58
63
|
export declare const TOOL_NAME_BY_OP: Readonly<Record<string, string>>;
|
|
64
|
+
/** The tool that serves one registry key: the table above, else `autotask_query` for any `<resource>.query` read (#228). */
|
|
65
|
+
export declare function toolForOp(operation: string): string | undefined;
|
|
59
66
|
/** The served tool for this row's operation in a deployment serving `served`, or `null`. */
|
|
60
67
|
export declare function servedToolName(row: CatalogRow, served: ReadonlySet<string>): string | null;
|
|
61
68
|
/** The registry keys projected onto an MCP tool name (the reverse of {@link TOOL_NAME_BY_OP}); empty when none. */
|
|
@@ -65,10 +72,10 @@ export declare const UNTRUSTED_NOTE = "Externally-sourced free text in the resul
|
|
|
65
72
|
/** The record's plan-derived untrusted field list (the registry stamp). */
|
|
66
73
|
export declare function untrustedFieldsOf(rec: CapabilityRecord): string[];
|
|
67
74
|
/**
|
|
68
|
-
* The resources that carry user-defined fields (#183 S8),
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
75
|
+
* The resources that carry user-defined fields (#183 S8), read from the registry with zero wire:
|
|
76
|
+
* each record's published `udfCapable` flag (node-autotask 0.37.0 #254; the plan's entities whose SDK
|
|
77
|
+
* resource class normalises `userDefinedFields`). Not the `getUdfInfo` set (every vendor entity has
|
|
78
|
+
* that metadata read, UDFs or not). A test pins the exact set.
|
|
72
79
|
*/
|
|
73
80
|
export declare const UDF_CAPABLE: ReadonlySet<string>;
|
|
74
81
|
/** The per-tool description budget (A0 slimming: every served description <= 250 chars). */
|
|
@@ -109,7 +116,7 @@ export declare function cacheOptionsForPreset(preset: 'on' | 'off', fieldCacheTt
|
|
|
109
116
|
* fails the boot (or the CI test) instead of quietly shipping a tool surface that no longer
|
|
110
117
|
* describes the SDK behind it.
|
|
111
118
|
*/
|
|
112
|
-
export declare const BUILT_AGAINST_PLAN_HASH = "
|
|
119
|
+
export declare const BUILT_AGAINST_PLAN_HASH = "9bb87de7bcbda60f6c44955dba52b8e61e9c5547750ea8acacfcb6f261d7734f";
|
|
113
120
|
/**
|
|
114
121
|
* Fail fast when the installed SDK's plan has moved out from under this surface.
|
|
115
122
|
* Called at boot (main.ts) and safe to call repeatedly (pure comparison).
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Two sources, both shipped inside the `node-autotask` package:
|
|
5
5
|
* - `MCP_TOOL_CATALOG.json` — the machine catalog: one row per registry operation (projected
|
|
6
6
|
* tool, effect, dry-run/confirm, required args, tier, reach) and the plan hash;
|
|
7
|
-
* - the compiled capability registry (`node-autotask/capabilities`) — the 1,
|
|
7
|
+
* - the compiled capability registry (`node-autotask/capabilities`) — the 1,281-operation
|
|
8
8
|
* records the SDK's own invoke governor resolves against.
|
|
9
9
|
*
|
|
10
10
|
* The JSON file is NOT behind the package's `exports` map, so it is read from the installed
|
|
@@ -51,7 +51,6 @@ export const TOOL_NAME_BY_OP = {
|
|
|
51
51
|
'threshold_information.get': 'autotask_tenant_status',
|
|
52
52
|
'operations.resolveAny': 'autotask_resolve',
|
|
53
53
|
'operations.searchAcrossResources': 'autotask_resolve',
|
|
54
|
-
'operations.fetchByIds': 'autotask_fetch_many',
|
|
55
54
|
'operations.relationshipMap': 'autotask_describe',
|
|
56
55
|
'companies.resolve': 'autotask_resolve',
|
|
57
56
|
'companies.search': 'autotask_resolve',
|
|
@@ -67,7 +66,7 @@ export const TOOL_NAME_BY_OP = {
|
|
|
67
66
|
'service_calls.query': 'autotask_query',
|
|
68
67
|
'contracts.query': 'autotask_query',
|
|
69
68
|
'time_off_requests.query': 'autotask_query',
|
|
70
|
-
'operations.aggregate': '
|
|
69
|
+
'operations.aggregate': 'autotask_query',
|
|
71
70
|
'tickets.fullContext': 'autotask_ticket_full_context',
|
|
72
71
|
'companies.fullProfile': 'autotask_company_full_profile',
|
|
73
72
|
'projects.getContext': 'autotask_project_get_context',
|
|
@@ -82,9 +81,13 @@ export const TOOL_NAME_BY_OP = {
|
|
|
82
81
|
'invoices.markupXml': 'autotask_invoice_get',
|
|
83
82
|
'invoices.pdf': 'autotask_invoice_get',
|
|
84
83
|
};
|
|
84
|
+
/** The tool that serves one registry key: the table above, else `autotask_query` for any `<resource>.query` read (#228). */
|
|
85
|
+
export function toolForOp(operation) {
|
|
86
|
+
return TOOL_NAME_BY_OP[operation] ?? (operation.endsWith('.query') && recordById(operation)?.effect === 'read' ? 'autotask_query' : undefined);
|
|
87
|
+
}
|
|
85
88
|
/** The served tool for this row's operation in a deployment serving `served`, or `null`. */
|
|
86
89
|
export function servedToolName(row, served) {
|
|
87
|
-
const tool =
|
|
90
|
+
const tool = toolForOp(row.operation) ?? (row.effect === 'read' ? undefined : 'autotask_write');
|
|
88
91
|
return tool !== undefined && served.has(tool) ? tool : null;
|
|
89
92
|
}
|
|
90
93
|
/** The registry keys projected onto an MCP tool name (the reverse of {@link TOOL_NAME_BY_OP}); empty when none. */
|
|
@@ -102,12 +105,12 @@ export function untrustedFieldsOf(rec) {
|
|
|
102
105
|
return Array.isArray(list) ? list : [];
|
|
103
106
|
}
|
|
104
107
|
/**
|
|
105
|
-
* The resources that carry user-defined fields (#183 S8),
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
108
|
+
* The resources that carry user-defined fields (#183 S8), read from the registry with zero wire:
|
|
109
|
+
* each record's published `udfCapable` flag (node-autotask 0.37.0 #254; the plan's entities whose SDK
|
|
110
|
+
* resource class normalises `userDefinedFields`). Not the `getUdfInfo` set (every vendor entity has
|
|
111
|
+
* that metadata read, UDFs or not). A test pins the exact set.
|
|
109
112
|
*/
|
|
110
|
-
export const UDF_CAPABLE = new Set(RECORDS.filter((r) =>
|
|
113
|
+
export const UDF_CAPABLE = new Set(RECORDS.filter((r) => r.udfCapable).map((r) => r.resource));
|
|
111
114
|
/** The per-tool description budget (A0 slimming: every served description <= 250 chars). */
|
|
112
115
|
export const MAX_DESCRIPTION_CHARS = 250;
|
|
113
116
|
/**
|
|
@@ -117,13 +120,11 @@ export const MAX_DESCRIPTION_CHARS = 250;
|
|
|
117
120
|
*/
|
|
118
121
|
export const TOOL_DESCRIPTIONS = {
|
|
119
122
|
autotask_guide: 'Step-by-step playbooks for multi-step Autotask tasks. No args: the index; {skill} returns one playbook, {skill, section} one section. Static text, no vendor call. Use it first when a task needs several tools.',
|
|
120
|
-
autotask_catalog: 'Find the exact operation key for anything without a dedicated tool: ranked search (q) over the 1,
|
|
123
|
+
autotask_catalog: 'Find the exact operation key for anything without a dedicated tool: ranked search (q) over the 1,281 registry operations, with effect and whether this deployment serves it. No vendor call.',
|
|
121
124
|
autotask_describe: 'One operation\'s (registry key or tool name) exact input/output contract, examples, errors, policy verdict and playbooks (operation), or one resource\'s relationship map: parent, children, lookup keys (resource). No vendor call.',
|
|
122
|
-
autotask_read: 'Run reads by exact registry key, or up to 10 in one call: unbilled work, profitability and margin, SLA health. *.query: 50 rows; scope by company/contract ids (unscoped = tenant walk);
|
|
123
|
-
autotask_resolve: 'Find the record
|
|
124
|
-
autotask_query: 'List records with a filter: tickets, time entries, service calls, contracts
|
|
125
|
-
autotask_aggregate: 'Totals, counts and group-by sums (hours by ticket) over any <resource>.query table without reading rows. No groupBy or sum = one count call. hasMore or groupsTruncated = a partial answer, never a tenant total.',
|
|
126
|
-
autotask_fetch_many: 'Fetch up to 500 records by numeric id across mixed resources in one call: items: [{resource, id, fields?}]. Answers data.results[{ok, operation, result|error}], ok, failed (same as autotask_read); a miss is a typed NOT_FOUND slot.',
|
|
125
|
+
autotask_read: 'Run reads by exact registry key, or up to 10 in one call: unbilled work, profitability and margin, SLA health. *.query: 50 rows; scope by company/contract ids (unscoped = tenant walk); totals: autotask_query group_by; slim: includeFields, noteChars.',
|
|
126
|
+
autotask_resolve: 'Find the record for an identifier: id or name, ticket number, CI serial, contact email (by), staff by email, user name or name (resource resources, include_api_users); one resource or a sweep. Exact, then contains on a miss.',
|
|
127
|
+
autotask_query: 'List records or totals with a filter: tickets, time entries, service calls, contracts, time off, more. Totals, no rows: group_by, sum, count (hours logged by ticket, how many match). By id: id in. Scope by company ids (unscoped = tenant walk).',
|
|
127
128
|
autotask_tenant_status: 'Identity (API user + impersonated user), Autotask API version, this server\'s package/SDK version and profile (server), request thresholds, and the time zone of the people asking, with local and UTC time (time). Use it first to check credentials.',
|
|
128
129
|
autotask_ticket_full_context: 'One ticket with its company, contact, assigned resource, project, notes, time entries and attachment metadata. Missing parts degrade to typed absent; a missing ticket is not found.',
|
|
129
130
|
autotask_company_full_profile: 'One company with locations, contacts, notes, open tickets and projects, 10 rows per part (capped: true = more exist; raise the limit). fields {part:[names]} trims columns. Open = not tenant-closed. Missing parts degrade to typed absent.',
|
|
@@ -173,7 +174,7 @@ export function cacheOptionsForPreset(preset, fieldCacheTtlMs) {
|
|
|
173
174
|
* fails the boot (or the CI test) instead of quietly shipping a tool surface that no longer
|
|
174
175
|
* describes the SDK behind it.
|
|
175
176
|
*/
|
|
176
|
-
export const BUILT_AGAINST_PLAN_HASH = '
|
|
177
|
+
export const BUILT_AGAINST_PLAN_HASH = '9bb87de7bcbda60f6c44955dba52b8e61e9c5547750ea8acacfcb6f261d7734f';
|
|
177
178
|
/**
|
|
178
179
|
* Fail fast when the installed SDK's plan has moved out from under this surface.
|
|
179
180
|
* Called at boot (main.ts) and safe to call repeatedly (pure comparison).
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** The default tool ops: the bounded query walk and the abort-signal wrapper (issue #91). */
|
|
2
2
|
import { AutotaskClient, camelKey } from 'node-autotask';
|
|
3
3
|
import { AutotaskConfigError } from 'node-autotask';
|
|
4
|
-
import {
|
|
4
|
+
import { invokeDelete, invokeRead, invokeWrite, relationshipMap, resolveAny, searchAcrossResources } from 'node-autotask/operations';
|
|
5
5
|
/** The real SDK bindings (the production default; tests substitute a fixture-backed bundle). */
|
|
6
6
|
/**
|
|
7
7
|
* The SDK operations bundle (the production default; tests substitute a fixture-backed bundle).
|
|
@@ -17,8 +17,6 @@ export const defaultToolOps = {
|
|
|
17
17
|
invokeDelete: (client, operation, args, opts, signal) => withSignal(client, signal, () => invokeDelete(client, operation, args, opts ?? {})),
|
|
18
18
|
resolveAny: (client, identifier, opts, signal) => withSignal(client, signal, () => resolveAny(client, identifier, opts ?? {})),
|
|
19
19
|
searchAcrossResources: (client, query, opts, signal) => withSignal(client, signal, () => searchAcrossResources(client, query, opts ?? {})),
|
|
20
|
-
fetchMany: (client, items, opts, signal) => withSignal(client, signal, () => fetchMany(client, items, opts ?? {})),
|
|
21
|
-
fetchByIds: (client, items, opts, signal) => withSignal(client, signal, () => fetchByIds(client, items, opts ?? {})),
|
|
22
20
|
relationshipMap: (client, resourceKey) => relationshipMap(client, resourceKey),
|
|
23
21
|
hourlyBudget: (client) => Promise.resolve(client.hourlyBudget()),
|
|
24
22
|
queryWalk: (client, operation, params, maxRows, signal) => withSignal(client, signal, () => walkQuery(client, operation, params, maxRows)),
|
|
@@ -45,6 +43,8 @@ export async function walkQuery(client, operation, params, maxRows) {
|
|
|
45
43
|
bag.includeFields = params.includeFields;
|
|
46
44
|
if (params.via !== undefined)
|
|
47
45
|
bag.via = params.via;
|
|
46
|
+
if (params.names !== undefined)
|
|
47
|
+
bag.names = params.names;
|
|
48
48
|
const items = [];
|
|
49
49
|
for await (const item of holder.query(bag)) {
|
|
50
50
|
if (items.length >= maxRows)
|