node-autotask 0.21.0 → 0.24.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 (56) hide show
  1. package/ARCHITECTURE.md +44 -21
  2. package/CHANGELOG.md +44 -0
  3. package/MCP_TOOL_CATALOG.json +90 -10
  4. package/MCP_TOOL_MANIFEST.md +24 -20
  5. package/MCP_TOOL_OVERRIDES.json +6 -6
  6. package/N8N_MCP_MANIFEST.json +65 -10
  7. package/N8N_MCP_MANIFEST.md +10 -8
  8. package/README.md +9 -9
  9. package/capabilities.json +3247 -7575
  10. package/capabilities.schema.json +0 -12
  11. package/dist/capabilities.cjs +2 -2
  12. package/dist/capabilities.d.cts +0 -2
  13. package/dist/capabilities.d.ts +0 -2
  14. package/dist/capabilities.js +1 -1
  15. package/dist/{chunk-CBUY3MKB.cjs → chunk-4VAC2Z55.cjs} +3246 -7574
  16. package/dist/{chunk-KD6LOSAK.js → chunk-BBQBWVBE.js} +3496 -3082
  17. package/dist/{chunk-JRSU6PKP.js → chunk-ICCU2S62.js} +2 -2
  18. package/dist/{chunk-GBVUXAUA.js → chunk-IOVFTIW6.js} +2257 -1426
  19. package/dist/{chunk-QWAWNQEZ.cjs → chunk-LVBFLSPL.cjs} +3044 -2630
  20. package/dist/{chunk-PMPFQDVM.cjs → chunk-SOHQ6SDI.cjs} +135 -135
  21. package/dist/{chunk-HEW4WJLC.cjs → chunk-SW66OWM5.cjs} +2645 -1814
  22. package/dist/{chunk-DM67SXCO.js → chunk-WJD26CPN.js} +3246 -7574
  23. package/dist/{index-BXtKN1M5.d.cts → index-BVtycq-6.d.cts} +1 -1
  24. package/dist/{index-BXtKN1M5.d.ts → index-BVtycq-6.d.ts} +1 -1
  25. package/dist/{index-ByWYfNog.d.cts → index-C0OwQhon.d.cts} +486 -962
  26. package/dist/{index-DSnuyiTr.d.ts → index-CZ94y401.d.ts} +486 -962
  27. package/dist/index.cjs +91 -62
  28. package/dist/index.d.cts +360 -5
  29. package/dist/index.d.ts +360 -5
  30. package/dist/index.js +74 -45
  31. package/dist/operations/index.cjs +462 -462
  32. package/dist/operations/index.d.cts +78 -84
  33. package/dist/operations/index.d.ts +78 -84
  34. package/dist/operations/index.js +382 -382
  35. package/dist/pool.cjs +8 -8
  36. package/dist/pool.d.cts +2 -2
  37. package/dist/pool.d.ts +2 -2
  38. package/dist/pool.js +4 -4
  39. package/dist/resources/index.cjs +4 -4
  40. package/dist/resources/index.d.cts +2 -2
  41. package/dist/resources/index.d.ts +2 -2
  42. package/dist/resources/index.js +3 -3
  43. package/dist/types/index.d.cts +1 -1
  44. package/dist/types/index.d.ts +1 -1
  45. package/docs/API.md +170 -49
  46. package/docs/N8N-ADAPTER.md +3 -3
  47. package/examples/mcp/errors.ts +135 -0
  48. package/examples/mcp/manifest.ts +168 -0
  49. package/examples/mcp/register.ts +66 -0
  50. package/examples/mcp/schemas.ts +203 -0
  51. package/examples/mcp/tools-meta.ts +120 -0
  52. package/examples/mcp/tools-mode.ts +177 -0
  53. package/examples/mcp/tools-read.ts +264 -0
  54. package/examples/mcp/types.ts +34 -0
  55. package/examples/mcp-register.ts +9 -1074
  56. package/package.json +1 -1
@@ -24,8 +24,10 @@ import {
24
24
  RELATIONSHIP_MAP,
25
25
  RELATIONSHIP_ZONE,
26
26
  RecoveryTruncatedError,
27
+ UI_LINK_ENTITIES,
27
28
  WORKFLOW_RESOURCES,
28
29
  buildFilter,
30
+ buildUiLink,
29
31
  camelKey,
30
32
  clampPageSize,
31
33
  expandResourceNames,
@@ -34,13 +36,14 @@ import {
34
36
  fieldCatalog,
35
37
  isAutotaskError,
36
38
  isRecord,
37
- normalizedConditions,
38
39
  parseIdentifier,
39
- validateFetchManyItems
40
- } from "../chunk-KD6LOSAK.js";
40
+ uiLink,
41
+ validateFetchManyItems,
42
+ validateQueryFieldNames
43
+ } from "../chunk-BBQBWVBE.js";
41
44
  import {
42
45
  CAPABILITIES
43
- } from "../chunk-DM67SXCO.js";
46
+ } from "../chunk-WJD26CPN.js";
44
47
 
45
48
  // src/operations/searchAcrossResources.ts
46
49
  var DEFAULT_LIMIT = 25;
@@ -57,19 +60,35 @@ async function searchAcrossResources(client, query, opts = {}) {
57
60
  throw new AutotaskConfigError("searchAcrossResources: no resources requested");
58
61
  }
59
62
  const limit = clampPageSize(opts.limit ?? DEFAULT_LIMIT);
63
+ const totalLimit = opts.totalLimit;
64
+ if (totalLimit !== void 0 && !(Number.isInteger(totalLimit) && totalLimit >= 1)) {
65
+ throw new AutotaskConfigError("searchAcrossResources: totalLimit must be a positive integer");
66
+ }
60
67
  const results = [];
61
68
  const skipped = [];
69
+ const notSearched = [];
70
+ let remaining = totalLimit ?? Infinity;
71
+ let hitQuota = false;
62
72
  for (const key of keys) {
63
73
  const holder = resourceFor(client, key);
64
74
  if (holder === void 0 || typeof holder.search !== "function") {
65
75
  skipped.push(key);
66
76
  continue;
67
77
  }
68
- const matches = await holder.search(text, { limit });
78
+ if (remaining <= 0) {
79
+ notSearched.push(key);
80
+ continue;
81
+ }
82
+ const ask = Math.min(limit, remaining);
83
+ const matches = await holder.search(text, { limit: ask });
69
84
  const rows = Array.isArray(matches) ? matches.filter((m) => typeof m === "object" && m !== null).map((m) => m) : [];
85
+ if (totalLimit !== void 0) rows.length = Math.min(rows.length, ask);
70
86
  results.push({ resource: key, matches: rows });
87
+ remaining -= rows.length;
88
+ if (rows.length === ask) hitQuota = true;
71
89
  }
72
- return { query: text, results, skipped };
90
+ if (totalLimit === void 0) return { query: text, results, skipped };
91
+ return { query: text, results, skipped, notSearched, truncated: notSearched.length > 0 || hitQuota };
73
92
  }
74
93
 
75
94
  // src/operations/resolveAny.ts
@@ -213,39 +232,41 @@ var OP_RE = /^([a-z0-9_]+)\.query$/;
213
232
  function num(v) {
214
233
  return typeof v === "number" && Number.isFinite(v) ? v : 0;
215
234
  }
216
- function validateAggregate(operation, filter, opts) {
217
- const problems = [];
218
- let resourceKey;
235
+ function parseOperationKey(operation, problems) {
219
236
  if (typeof operation !== "string" || !OP_RE.test(operation)) {
220
237
  problems.push(
221
238
  `operation must be exactly "<planResourceKey>.query" (the resource's bounded query op \u2014 e.g. "billing_items.query"); a special op, a ".count" row, or any other verb is not an aggregate surface \u2014 see autotask_catalog for the exact plan keys`
222
239
  );
223
- } else {
224
- const key = OP_RE.exec(operation)[1];
225
- resourceKey = key;
226
- if (!QUERY_RESOURCE_KEYS.has(key)) {
227
- problems.push(
228
- `operation "${operation}": "${key}" has no query row in the capability registry (116 query surfaces) \u2014 see autotask_catalog for the exact plan keys`
229
- );
230
- }
240
+ return void 0;
241
+ }
242
+ const key = OP_RE.exec(operation)[1];
243
+ if (!QUERY_RESOURCE_KEYS.has(key)) {
244
+ problems.push(
245
+ `operation "${operation}": "${key}" has no query row in the capability registry (116 query surfaces) \u2014 see autotask_catalog for the exact plan keys`
246
+ );
231
247
  }
232
- let conditions = [];
248
+ return key;
249
+ }
250
+ function parseFilterConditions(filter, problems) {
233
251
  if (filter === void 0 || filter === null) {
234
- conditions = [];
235
- } else {
236
- const conds = Array.isArray(filter) ? filter : filter.filter;
237
- if (!Array.isArray(conds)) {
238
- problems.push("filter must be the query-bag filter (a conditions array, or { filter: [...] })");
239
- } else if (conds.length === 0) {
240
- conditions = [];
241
- } else {
242
- try {
243
- conditions = buildFilter(conds).filter;
244
- } catch (err) {
245
- problems.push(err instanceof AutotaskConfigError ? err.message : `filter must be the query-bag filter (13 operators; caps 500 OR-conditions / 1 UDF / 8000 chars); ${String(err)}`);
246
- }
247
- }
252
+ return [];
253
+ }
254
+ const conds = Array.isArray(filter) ? filter : filter.filter;
255
+ if (!Array.isArray(conds)) {
256
+ problems.push("filter must be the query-bag filter (a conditions array, or { filter: [...] })");
257
+ return [];
248
258
  }
259
+ if (conds.length === 0) {
260
+ return [];
261
+ }
262
+ try {
263
+ return buildFilter(conds).filter;
264
+ } catch (err) {
265
+ problems.push(err instanceof AutotaskConfigError ? err.message : `filter must be the query-bag filter (13 operators; caps 500 OR-conditions / 1 UDF / 8000 chars); ${String(err)}`);
266
+ return [];
267
+ }
268
+ }
269
+ function checkOptsBag(opts, problems) {
249
270
  if (opts !== void 0 && (typeof opts !== "object" || opts === null || Array.isArray(opts))) {
250
271
  throw new AutotaskConfigError(
251
272
  `aggregate: opts must be the options object (got ${Array.isArray(opts) ? "an array" : typeof opts})`,
@@ -257,58 +278,63 @@ function validateAggregate(operation, filter, opts) {
257
278
  problems.push(`unknown opts key "${key}" \u2014 the bag is closed: ${AGGREGATE_OPT_KEYS.join(", ")}`);
258
279
  }
259
280
  }
260
- const fieldSets = resourceKey !== void 0 ? MODEL_FIELD_SETS[resourceKey] : void 0;
261
- const dateSets = resourceKey !== void 0 ? MODEL_DATE_FIELDS[resourceKey] : void 0;
262
- const checkFields = (name, fields) => {
263
- if (fields === void 0) return [];
264
- if (!Array.isArray(fields) || fields.length === 0 || fields.some((f) => typeof f !== "string" || f.trim() === "")) {
265
- problems.push(`${name} must be a non-empty array of exact vendor field names (discover them via operations.fieldCatalog)`);
266
- return [];
281
+ }
282
+ function checkSumFields(name, fields, oracles, problems) {
283
+ if (fields === void 0) return [];
284
+ if (!Array.isArray(fields) || fields.length === 0 || fields.some((f) => typeof f !== "string" || f.trim() === "")) {
285
+ problems.push(`${name} must be a non-empty array of exact vendor field names (discover them via operations.fieldCatalog)`);
286
+ return [];
287
+ }
288
+ const out = [];
289
+ for (const f of fields) {
290
+ if (oracles.fieldSets === void 0 || !oracles.fieldSets.includes(f)) {
291
+ problems.push(`${name}: "${f}" is not a field of ${oracles.resourceKey} \u2014 a hallucinated field is a shape error, not a silent fold-miss (exact vendor field names via operations.fieldCatalog)`);
292
+ } else {
293
+ out.push(f);
267
294
  }
268
- const out = [];
269
- for (const f of fields) {
270
- if (fieldSets === void 0 || !fieldSets.includes(f)) {
271
- problems.push(`${name}: "${f}" is not a field of ${resourceKey} \u2014 a hallucinated field is a shape error, not a silent fold-miss (exact vendor field names via operations.fieldCatalog)`);
272
- } else {
273
- out.push(f);
274
- }
295
+ }
296
+ return out;
297
+ }
298
+ function parseGroupSpec(f, oracles, problems) {
299
+ const { resourceKey, fieldSets, dateSets } = oracles;
300
+ const at = f.indexOf("@");
301
+ if (at === -1) {
302
+ if (fieldSets === void 0 || !fieldSets.includes(f)) {
303
+ problems.push(`groupBy: "${f}" is not a field of ${resourceKey} \u2014 a hallucinated field is a shape error, not a silent fold-miss (exact vendor field names via operations.fieldCatalog)`);
304
+ return void 0;
275
305
  }
276
- return out;
277
- };
306
+ return { name: f, base: f };
307
+ }
308
+ const base = f.slice(0, at);
309
+ const suffix = f.slice(at + 1);
310
+ if (!AGGREGATE_DATE_BUCKETS.includes(suffix)) {
311
+ problems.push(`groupBy: "${f}": unknown bucket suffix "${suffix}" \u2014 the closed set is month | week | year (the suffix applies to date-typed fields only)`);
312
+ return void 0;
313
+ }
314
+ if (fieldSets === void 0 || !fieldSets.includes(base)) {
315
+ problems.push(`groupBy: "${f}": "${base}" is not a field of ${resourceKey} \u2014 a hallucinated field is a shape error, not a silent fold-miss (exact vendor field names via operations.fieldCatalog)`);
316
+ return void 0;
317
+ }
318
+ if (dateSets === void 0 || !dateSets.includes(base)) {
319
+ problems.push(`groupBy: "${f}": "${base}" is not a date field of ${resourceKey} (the @month/@week/@year bucket suffix applies to date-typed fields of the resource's vendored model \u2014 exact vendor field names via operations.fieldCatalog)`);
320
+ return void 0;
321
+ }
322
+ return { name: f, base, bucket: suffix };
323
+ }
324
+ function parseGroupBy(opts, oracles, problems) {
278
325
  const groupBy = [];
279
- if (opts.groupBy !== void 0) {
280
- if (!Array.isArray(opts.groupBy) || opts.groupBy.length === 0 || opts.groupBy.some((f) => typeof f !== "string" || f.trim() === "")) {
281
- problems.push("groupBy must be a non-empty array of exact vendor field names (a date-typed field may carry the closed bucket suffix @month | @week | @year \u2014 discover them via operations.fieldCatalog)");
282
- } else {
283
- for (const f of opts.groupBy) {
284
- const at = f.indexOf("@");
285
- if (at === -1) {
286
- if (fieldSets === void 0 || !fieldSets.includes(f)) {
287
- problems.push(`groupBy: "${f}" is not a field of ${resourceKey} \u2014 a hallucinated field is a shape error, not a silent fold-miss (exact vendor field names via operations.fieldCatalog)`);
288
- } else {
289
- groupBy.push({ name: f, base: f });
290
- }
291
- continue;
292
- }
293
- const base = f.slice(0, at);
294
- const suffix = f.slice(at + 1);
295
- if (!AGGREGATE_DATE_BUCKETS.includes(suffix)) {
296
- problems.push(`groupBy: "${f}": unknown bucket suffix "${suffix}" \u2014 the closed set is month | week | year (the suffix applies to date-typed fields only)`);
297
- continue;
298
- }
299
- if (fieldSets === void 0 || !fieldSets.includes(base)) {
300
- problems.push(`groupBy: "${f}": "${base}" is not a field of ${resourceKey} \u2014 a hallucinated field is a shape error, not a silent fold-miss (exact vendor field names via operations.fieldCatalog)`);
301
- continue;
302
- }
303
- if (dateSets === void 0 || !dateSets.includes(base)) {
304
- problems.push(`groupBy: "${f}": "${base}" is not a date field of ${resourceKey} (the @month/@week/@year bucket suffix applies to date-typed fields of the resource's vendored model \u2014 exact vendor field names via operations.fieldCatalog)`);
305
- continue;
306
- }
307
- groupBy.push({ name: f, base, bucket: suffix });
308
- }
309
- }
326
+ if (opts.groupBy === void 0) return groupBy;
327
+ if (!Array.isArray(opts.groupBy) || opts.groupBy.length === 0 || opts.groupBy.some((f) => typeof f !== "string" || f.trim() === "")) {
328
+ problems.push("groupBy must be a non-empty array of exact vendor field names (a date-typed field may carry the closed bucket suffix @month | @week | @year \u2014 discover them via operations.fieldCatalog)");
329
+ return groupBy;
330
+ }
331
+ for (const f of opts.groupBy) {
332
+ const spec = parseGroupSpec(f, oracles, problems);
333
+ if (spec !== void 0) groupBy.push(spec);
310
334
  }
311
- const sum = checkFields("sum", opts.sum);
335
+ return groupBy;
336
+ }
337
+ function parseTopAndLimit(opts, problems) {
312
338
  let top = AGGREGATE_DEFAULT_TOP;
313
339
  if (opts.top !== void 0) {
314
340
  if (!Number.isInteger(opts.top)) {
@@ -325,6 +351,21 @@ function validateAggregate(operation, filter, opts) {
325
351
  limit = Math.min(Math.max(opts.limit, 1), BOUNDED_QUERY_MAX_LIMIT);
326
352
  }
327
353
  }
354
+ return { top, limit };
355
+ }
356
+ function validateAggregate(operation, filter, opts) {
357
+ const problems = [];
358
+ const resourceKey = parseOperationKey(operation, problems);
359
+ const conditions = parseFilterConditions(filter, problems);
360
+ checkOptsBag(opts, problems);
361
+ const oracles = {
362
+ resourceKey,
363
+ fieldSets: resourceKey !== void 0 ? MODEL_FIELD_SETS[resourceKey] : void 0,
364
+ dateSets: resourceKey !== void 0 ? MODEL_DATE_FIELDS[resourceKey] : void 0
365
+ };
366
+ const groupBy = parseGroupBy(opts, oracles, problems);
367
+ const sum = checkSumFields("sum", opts.sum, oracles, problems);
368
+ const { top, limit } = parseTopAndLimit(opts, problems);
328
369
  if (problems.length > 0) {
329
370
  throw new AutotaskConfigError(
330
371
  `aggregate: ${problems.length} problem(s) found pre-request (zero wire was issued)
@@ -334,27 +375,9 @@ function validateAggregate(operation, filter, opts) {
334
375
  }
335
376
  return { resourceKey, conditions, groupBy, sum, top, limit };
336
377
  }
337
- async function aggregate(client, operation, filter, opts = {}) {
338
- const v = validateAggregate(operation, filter, opts);
339
- const holder = client[camelKey(v.resourceKey)];
340
- if (holder === void 0) {
341
- throw new AutotaskConfigError(`aggregate: the client exposes no accessor for resource "${v.resourceKey}"`, {
342
- operation: "operations.aggregate",
343
- suggestedAction: "Rebuild the client from the SDK build that ships this registry (the resource set is pinned by the plan)."
344
- });
345
- }
346
- const wireFilter = v.conditions.length === 0 ? void 0 : v.conditions;
347
- if (v.groupBy.length === 0 && v.sum.length === 0) {
348
- const raw = await holder.count({ filter: wireFilter });
349
- const sentinel = typeof raw === "object" && raw !== null && "count" in raw ? raw : { count: raw };
350
- const result2 = { mode: "count", count: sentinel.count, blind: sentinel.blindSurface !== void 0 };
351
- if (sentinel.blindSurface !== void 0) result2.blindSurface = sentinel.blindSurface;
352
- if (sentinel.emptyReason !== void 0) result2.emptyReason = sentinel.emptyReason;
353
- return result2;
354
- }
355
- const res = await holder.queryBounded({ filter: wireFilter, limit: v.limit });
378
+ function foldGroups(items, v) {
356
379
  const groups = /* @__PURE__ */ new Map();
357
- for (const item of res.items) {
380
+ for (const item of items) {
358
381
  const key = {};
359
382
  const values = [];
360
383
  for (const spec of v.groupBy) {
@@ -374,7 +397,28 @@ async function aggregate(client, operation, filter, opts = {}) {
374
397
  for (const f of v.sum) g.sums[f] = (g.sums[f] ?? 0) + num(item[f]);
375
398
  }
376
399
  }
377
- const sorted = [...groups.values()].sort((a, b) => b.count - a.count || (a.keyOrder < b.keyOrder ? -1 : a.keyOrder > b.keyOrder ? 1 : 0));
400
+ return [...groups.values()].sort((a, b) => b.count - a.count || (a.keyOrder < b.keyOrder ? -1 : a.keyOrder > b.keyOrder ? 1 : 0));
401
+ }
402
+ async function aggregate(client, operation, filter, opts = {}) {
403
+ const v = validateAggregate(operation, filter, opts);
404
+ const holder = client[camelKey(v.resourceKey)];
405
+ if (holder === void 0) {
406
+ throw new AutotaskConfigError(`aggregate: the client exposes no accessor for resource "${v.resourceKey}"`, {
407
+ operation: "operations.aggregate",
408
+ suggestedAction: "Rebuild the client from the SDK build that ships this registry (the resource set is pinned by the plan)."
409
+ });
410
+ }
411
+ const wireFilter = v.conditions.length === 0 ? void 0 : v.conditions;
412
+ if (v.groupBy.length === 0 && v.sum.length === 0) {
413
+ const raw = await holder.count({ filter: wireFilter });
414
+ const sentinel = typeof raw === "object" && raw !== null && "count" in raw ? raw : { count: raw };
415
+ const result2 = { mode: "count", count: sentinel.count, blind: sentinel.blindSurface !== void 0 };
416
+ if (sentinel.blindSurface !== void 0) result2.blindSurface = sentinel.blindSurface;
417
+ if (sentinel.emptyReason !== void 0) result2.emptyReason = sentinel.emptyReason;
418
+ return result2;
419
+ }
420
+ const res = await holder.queryBounded({ filter: wireFilter, limit: v.limit });
421
+ const sorted = foldGroups(res.items, v);
378
422
  const top = sorted.slice(0, v.top);
379
423
  const result = {
380
424
  mode: v.groupBy.length > 0 ? "group" : "sum",
@@ -646,8 +690,7 @@ function parseMatchId(record) {
646
690
  }
647
691
  return { id: null, unsafe: true };
648
692
  }
649
- async function recovery(client, input, opts = {}) {
650
- const operation = RECOVERY_CONFIRM;
693
+ function validateRecoveryInput(input, operation) {
651
694
  if (!isRecord(input)) {
652
695
  configError(
653
696
  `recovery: input must be an object { resource, marker, ... } (got ${typeof input})`,
@@ -679,6 +722,9 @@ async function recovery(client, input, opts = {}) {
679
722
  "Pass action: 'delete' for the destructive path (dry-run-first; the apply requires the confirm string)."
680
723
  );
681
724
  }
725
+ return { resource, marker, action };
726
+ }
727
+ function resolveRecoveryTarget(client, resource, operation) {
682
728
  const entry = RELATIONSHIP_MAP[resource];
683
729
  if (entry === void 0) {
684
730
  throw new AutotaskError(
@@ -699,6 +745,9 @@ async function recovery(client, input, opts = {}) {
699
745
  "Use a resource with a query surface, or address the record by id directly (get/fetchMany)."
700
746
  );
701
747
  }
748
+ return { entry, holder };
749
+ }
750
+ function resolveMarkerField(input, resource, operation) {
702
751
  const field = input.field !== void 0 ? String(input.field) : MARKER_FIELDS[resource];
703
752
  if (typeof field !== "string" || field.trim() === "") {
704
753
  configError(
@@ -707,55 +756,126 @@ async function recovery(client, input, opts = {}) {
707
756
  "Pass input.field \u2014 a name-like vendor field the marker was stamped into (see the resource model in autotask_describe / autotask_catalog)."
708
757
  );
709
758
  }
710
- let parentId;
711
- if (input.parentId !== void 0) {
712
- if (!Number.isInteger(input.parentId) || input.parentId < 0) {
713
- configError(
714
- `recovery: input.parentId must be a non-negative integer (got ${String(input.parentId)})`,
715
- operation,
716
- "Pass the parent record id (e.g. the ticket id for a ticket note)."
717
- );
718
- }
719
- parentId = input.parentId;
720
- if (entry.parent === null || entry.parent.field === null) {
721
- configError(
722
- `recovery: ${resource} is not a parent-scoped child resource \u2014 input.parentId does not apply`,
723
- operation,
724
- `Drop input.parentId (the search runs unscoped), or pass the child resource that IS scoped under ${resource}.`
725
- );
726
- }
759
+ return field;
760
+ }
761
+ function resolveParentId(input, entry, resource, operation) {
762
+ if (input.parentId === void 0) return void 0;
763
+ if (!Number.isInteger(input.parentId) || input.parentId < 0) {
764
+ configError(
765
+ `recovery: input.parentId must be a non-negative integer (got ${String(input.parentId)})`,
766
+ operation,
767
+ "Pass the parent record id (e.g. the ticket id for a ticket note)."
768
+ );
769
+ }
770
+ if (entry.parent === null || entry.parent.field === null) {
771
+ configError(
772
+ `recovery: ${resource} is not a parent-scoped child resource \u2014 input.parentId does not apply`,
773
+ operation,
774
+ `Drop input.parentId (the search runs unscoped), or pass the child resource that IS scoped under ${resource}.`
775
+ );
727
776
  }
777
+ return input.parentId;
778
+ }
779
+ function resolveExtraFilter(input, operation) {
728
780
  const extra = [];
729
- if (input.filter !== void 0) {
730
- if (!Array.isArray(input.filter)) {
781
+ if (input.filter === void 0) return extra;
782
+ if (!Array.isArray(input.filter)) {
783
+ configError(
784
+ `recovery: input.filter must be an array of leaf filter conditions (got ${typeof input.filter})`,
785
+ operation,
786
+ "Pass e.g. [{ field: 'status', op: 'eq', value: 'open' }] \u2014 leaf conditions only."
787
+ );
788
+ }
789
+ for (const c of input.filter) {
790
+ if (!isRecord(c) || typeof c.op !== "string" || c.op === "and" || c.op === "or") {
731
791
  configError(
732
- `recovery: input.filter must be an array of leaf filter conditions (got ${typeof input.filter})`,
792
+ "recovery: input.filter accepts only plain leaf conditions ({ field, op, value }) \u2014 no grouping",
733
793
  operation,
734
- "Pass e.g. [{ field: 'status', op: 'eq', value: 'open' }] \u2014 leaf conditions only."
794
+ "Pass flat leaf conditions; grouping operators are not supported here."
735
795
  );
736
796
  }
737
- for (const c of input.filter) {
738
- if (!isRecord(c) || typeof c.op !== "string" || c.op === "and" || c.op === "or") {
739
- configError(
740
- "recovery: input.filter accepts only plain leaf conditions ({ field, op, value }) \u2014 no grouping",
741
- operation,
742
- "Pass flat leaf conditions; grouping operators are not supported here."
743
- );
744
- }
745
- extra.push(c);
746
- }
797
+ extra.push(c);
747
798
  }
748
- let limit = RECOVERY_DEFAULT_LIMIT;
749
- if (input.limit !== void 0) {
750
- if (!Number.isInteger(input.limit) || input.limit < 1 || input.limit > RECOVERY_MAX_LIMIT) {
751
- configError(
752
- `recovery: input.limit must be an integer 1..${RECOVERY_MAX_LIMIT} (got ${String(input.limit)})`,
799
+ return extra;
800
+ }
801
+ function resolveLimit(input, operation) {
802
+ if (input.limit === void 0) return RECOVERY_DEFAULT_LIMIT;
803
+ if (!Number.isInteger(input.limit) || input.limit < 1 || input.limit > RECOVERY_MAX_LIMIT) {
804
+ configError(
805
+ `recovery: input.limit must be an integer 1..${RECOVERY_MAX_LIMIT} (got ${String(input.limit)})`,
806
+ operation,
807
+ `Raise the marker precision instead of the cap, or pass limit up to ${RECOVERY_MAX_LIMIT}.`
808
+ );
809
+ }
810
+ return input.limit;
811
+ }
812
+ async function applyRecoveryDeletes(holder, matches, parentId, resource, operation) {
813
+ const deleteMethod = holder.delete;
814
+ if (deleteMethod === void 0) {
815
+ throw new DeleteBoundaryError(
816
+ `recovery: ${resource} has no delete method on the client \u2014 the boundary is marked available in the plan but the typed delete surface is missing`,
817
+ {
753
818
  operation,
754
- `Raise the marker precision instead of the cap, or pass limit up to ${RECOVERY_MAX_LIMIT}.`
755
- );
819
+ suggestedAction: "Report the registry drift (plan says deletable, client exposes no delete) \u2014 or clean up via the Autotask UI.",
820
+ details: { resource }
821
+ }
822
+ );
823
+ }
824
+ const deleted = [];
825
+ for (const match of matches) {
826
+ const id = match.id;
827
+ let verify = "error";
828
+ let code;
829
+ try {
830
+ if (parentId !== void 0) {
831
+ await deleteMethod.call(holder, parentId, id, { dryRun: false });
832
+ } else {
833
+ await deleteMethod.call(holder, id, { dryRun: false });
834
+ }
835
+ try {
836
+ if (parentId !== void 0) {
837
+ await holder.get.call(holder, parentId, id);
838
+ verify = "still-present";
839
+ } else {
840
+ await holder.get.call(holder, id);
841
+ verify = "still-present";
842
+ }
843
+ } catch (err) {
844
+ if (err instanceof NotFoundError) verify = "not-found";
845
+ else {
846
+ verify = "error";
847
+ code = isAutotaskError(err) ? err.code : "SERVER_ERROR";
848
+ }
849
+ }
850
+ } catch (err) {
851
+ code = isAutotaskError(err) ? err.code : "SERVER_ERROR";
756
852
  }
757
- limit = input.limit;
853
+ deleted.push({ id, verified: verify === "not-found", code });
758
854
  }
855
+ return deleted;
856
+ }
857
+ function collectMatches(items, field) {
858
+ const matches = [];
859
+ const unsafeIds = [];
860
+ items.forEach((record, idx) => {
861
+ const parsed = parseMatchId(record);
862
+ if (parsed.unsafe) unsafeIds.push(idx);
863
+ const value = record[field];
864
+ matches.push({
865
+ id: parsed.id,
866
+ value: typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? value : null
867
+ });
868
+ });
869
+ return { matches, unsafeIds };
870
+ }
871
+ async function recovery(client, input, opts = {}) {
872
+ const operation = RECOVERY_CONFIRM;
873
+ const { resource, marker, action } = validateRecoveryInput(input, operation);
874
+ const { entry, holder } = resolveRecoveryTarget(client, resource, operation);
875
+ const field = resolveMarkerField(input, resource, operation);
876
+ const parentId = resolveParentId(input, entry, resource, operation);
877
+ const extra = resolveExtraFilter(input, operation);
878
+ const limit = resolveLimit(input, operation);
759
879
  const boundaryNote = DELETE_BOUNDARY_NOTES[resource];
760
880
  const boundary = {
761
881
  available: boundaryNote === void 0,
@@ -805,17 +925,7 @@ async function recovery(client, input, opts = {}) {
805
925
  }
806
926
  );
807
927
  }
808
- const matches = [];
809
- const unsafeIds = [];
810
- query.items.forEach((record, idx) => {
811
- const parsed = parseMatchId(record);
812
- if (parsed.unsafe) unsafeIds.push(idx);
813
- const value = record[field];
814
- matches.push({
815
- id: parsed.id,
816
- value: typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? value : null
817
- });
818
- });
928
+ const { matches, unsafeIds } = collectMatches(query.items, field);
819
929
  const unsafeId = unsafeIds.length > 0;
820
930
  const base = {
821
931
  resource,
@@ -844,54 +954,11 @@ async function recovery(client, input, opts = {}) {
844
954
  }
845
955
  );
846
956
  }
847
- const deleteMethod = holder.delete;
848
- if (deleteMethod === void 0) {
849
- throw new DeleteBoundaryError(
850
- `recovery: ${resource} has no delete method on the client \u2014 the boundary is marked available in the plan but the typed delete surface is missing`,
851
- {
852
- operation,
853
- suggestedAction: "Report the registry drift (plan says deletable, client exposes no delete) \u2014 or clean up via the Autotask UI.",
854
- details: { resource }
855
- }
856
- );
857
- }
858
- const deleted = [];
859
- for (const match of matches) {
860
- const id = match.id;
861
- let verify = "error";
862
- let code;
863
- try {
864
- if (parentId !== void 0) {
865
- await deleteMethod.call(holder, parentId, id, { dryRun: false });
866
- } else {
867
- await deleteMethod.call(holder, id, { dryRun: false });
868
- }
869
- try {
870
- if (parentId !== void 0) {
871
- await holder.get.call(holder, parentId, id);
872
- verify = "still-present";
873
- } else {
874
- await holder.get.call(holder, id);
875
- verify = "still-present";
876
- }
877
- } catch (err) {
878
- if (err instanceof NotFoundError) verify = "not-found";
879
- else {
880
- verify = "error";
881
- code = isAutotaskError(err) ? err.code : "SERVER_ERROR";
882
- }
883
- }
884
- } catch (err) {
885
- code = isAutotaskError(err) ? err.code : "SERVER_ERROR";
886
- }
887
- deleted.push({ id, verified: verify === "not-found", code });
888
- }
957
+ const deleted = await applyRecoveryDeletes(holder, matches, parentId, resource, operation);
889
958
  return { ...base, deleted, dryRun: false };
890
959
  }
891
960
 
892
- // src/operations/invoke.ts
893
- var RECORDS = CAPABILITIES;
894
- var byId = new Map(RECORDS.map((r) => [r.id, r]));
961
+ // src/operations/invoke/modes.ts
895
962
  function needsConfirmation(record) {
896
963
  return record.effect === "destructive" || (record.flags ?? []).includes("requiresApproval");
897
964
  }
@@ -924,6 +991,8 @@ function crossModeRefusal(operation, effect, mode) {
924
991
  const target = actual === "read" ? `Route it through the read mode (${MODE_SDK[actual]} / ${MODE_TOOL[actual]}).` : actual === "write" ? `Route it through the write mode (${MODE_SDK[actual]} / ${MODE_TOOL[actual]} \u2014 dry-run first).` : `Deletes have their own tool (${MODE_TOOL[actual]}) and require confirm: "${operation}".`;
925
992
  return `invoke: "${operation}" is ${subject} (effect=${effect}) \u2014 refused on the ${mode} path (${MODE_TOOL[mode]} routes effect=${MODE_EFFECT[mode]} operations only). ${target}`;
926
993
  }
994
+
995
+ // src/operations/invoke/validate.ts
927
996
  function isSafeNonNegativeInteger(value) {
928
997
  return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
929
998
  }
@@ -957,8 +1026,11 @@ function validateDispatchArgs(record, argsIn) {
957
1026
  const schema = record.inputSchema ?? {};
958
1027
  const names = Object.keys(schema);
959
1028
  if (args.length > names.length) {
1029
+ const bagName = names.find((n) => schema[n].properties);
1030
+ const surplus = args[names.length];
1031
+ const bagHint = bagName && surplus !== null && typeof surplus === "object" && !Array.isArray(surplus) ? ` \u2014 options go inside the single ${bagName} object` : "";
960
1032
  throw new AutotaskConfigError(
961
- `invoke: "${operation}" declares ${names.length} positional arg(s) (${declaredArgList(record)}) but ${args.length} were passed \u2014 extra args are refused, not dropped (v0.2.0 silently dropped them)`,
1033
+ `invoke: "${operation}" declares ${names.length} positional arg(s) (${declaredArgList(record)}) but ${args.length} were passed \u2014 extra args are refused${bagHint}`,
962
1034
  {
963
1035
  operation,
964
1036
  suggestedAction: `Pass exactly the declared args in order (${declaredArgList(record)}); dryRun/confirm/lastModifiedDate travel through the invoke options, never the args.`
@@ -985,6 +1057,8 @@ function validateDispatchArgs(record, argsIn) {
985
1057
  }
986
1058
  }
987
1059
  }
1060
+
1061
+ // src/operations/invoke/inputSets.ts
988
1062
  var PARAMS_BAG_KEYS = {
989
1063
  query: ["filter", "limit", "includeFields", "via", "latest", "noteChars", "fieldsPreset", "fieldCheck"],
990
1064
  // No registry row DECLARES `queryAll` today (a plan decision: the raw walk is
@@ -1050,116 +1124,6 @@ function validateParamsBag(record, args) {
1050
1124
  }
1051
1125
  );
1052
1126
  }
1053
- async function liveFieldNames(holder) {
1054
- const reader = holder.getFieldInfo;
1055
- if (typeof reader !== "function") return null;
1056
- try {
1057
- const rows = await reader.call(holder);
1058
- if (!Array.isArray(rows)) return null;
1059
- const names = [];
1060
- for (const row of rows) {
1061
- if (typeof row.name === "string") names.push(row.name);
1062
- }
1063
- return names.length > 0 ? names : null;
1064
- } catch (err) {
1065
- if (!isAutotaskError(err) || err.code === "ABORTED") throw err;
1066
- return null;
1067
- }
1068
- }
1069
- var UNKNOWN_NAME_LIST_CAP = 20;
1070
- var UNKNOWN_NAME_DISPLAY_CHARS = 80;
1071
- function formatUnknownNameList(names) {
1072
- const listed = names.slice(0, UNKNOWN_NAME_LIST_CAP).map((n) => `"${n.length > UNKNOWN_NAME_DISPLAY_CHARS ? `${n.slice(0, UNKNOWN_NAME_DISPLAY_CHARS)}\u2026` : n}"`).join(", ");
1073
- const more = names.length > UNKNOWN_NAME_LIST_CAP ? ` (and ${names.length - UNKNOWN_NAME_LIST_CAP} more)` : "";
1074
- return `${listed}${more}`;
1075
- }
1076
- function collectFilterFieldNames(conditions) {
1077
- const out = [];
1078
- const walk = (conds) => {
1079
- for (const cond of conds) {
1080
- if (!isRecord(cond)) continue;
1081
- if (cond.op === "and" || cond.op === "or") {
1082
- if (Array.isArray(cond.items)) walk(cond.items);
1083
- continue;
1084
- }
1085
- if (cond.udf === true) continue;
1086
- if (typeof cond.field === "string" && cond.field.length > 0) out.push(cond.field);
1087
- }
1088
- };
1089
- walk(conditions);
1090
- return out;
1091
- }
1092
- async function validateQueryFieldNames(record, holder, args) {
1093
- const bag = args[0];
1094
- if (!isRecord(bag)) return;
1095
- const fieldCheck = bag["fieldCheck"];
1096
- if (fieldCheck !== void 0 && typeof fieldCheck !== "boolean") {
1097
- throw new AutotaskConfigError(
1098
- `invoke: "${record.id}" params bag fieldCheck must be a boolean (got ${typeof fieldCheck}) \u2014 the request was not issued`,
1099
- { operation: record.id, suggestedAction: "Pass fieldCheck: true (default) or fieldCheck: false (disable the field-name checks \u2014 includeFields AND filter condition names)." }
1100
- );
1101
- }
1102
- const includeFields = bag["includeFields"];
1103
- let includeNames;
1104
- if (includeFields !== void 0) {
1105
- let shapeOk = Array.isArray(includeFields);
1106
- const names = [];
1107
- if (shapeOk) {
1108
- for (const entry of includeFields) {
1109
- if (typeof entry !== "string" || entry.length === 0) {
1110
- shapeOk = false;
1111
- break;
1112
- }
1113
- names.push(entry);
1114
- }
1115
- }
1116
- if (!shapeOk) {
1117
- throw new AutotaskConfigError(
1118
- `invoke: "${record.id}" params bag includeFields must be an array of non-empty field-name strings \u2014 the request was not issued`,
1119
- {
1120
- operation: record.id,
1121
- suggestedAction: 'Pass includeFields as an array of exact vendor field names, e.g. ["id", "title"]; the live set is in operations.fieldCatalog.'
1122
- }
1123
- );
1124
- }
1125
- includeNames = names;
1126
- }
1127
- let filterNames;
1128
- if (bag["filter"] !== void 0) {
1129
- const conditions = normalizedConditions(bag["filter"]);
1130
- filterNames = collectFilterFieldNames(conditions ?? []);
1131
- }
1132
- if (fieldCheck === false) return;
1133
- const hasInclude = includeNames !== void 0 && includeNames.length > 0;
1134
- const hasFilter = filterNames !== void 0 && filterNames.length > 0;
1135
- if (!hasInclude && !hasFilter) return;
1136
- const live = await liveFieldNames(holder);
1137
- let known;
1138
- let liveOnlyExempt = [];
1139
- if (live !== null) {
1140
- known = live;
1141
- } else {
1142
- const vendored = MODEL_FIELD_SETS[record.resource];
1143
- liveOnlyExempt = Array.isArray(record.metadata?.liveOnlyFields) ? record.metadata.liveOnlyFields : [];
1144
- known = vendored === void 0 ? void 0 : [...vendored, ...liveOnlyExempt];
1145
- }
1146
- if (known === void 0) return;
1147
- const authority = live !== null ? "the tenant's live entity field metadata" : liveOnlyExempt.length > 0 ? "the vendored spec (src/field-sets.ts) + the plan-pinned live-only fields (exempt from the vendored refusal)" : "the vendored spec (src/field-sets.ts)";
1148
- const unknownInclude = hasInclude ? includeNames.filter((name) => !known.includes(name)) : [];
1149
- const unknownFilter = hasFilter ? [...new Set(filterNames.filter((name) => !known.includes(name)))] : [];
1150
- if (unknownInclude.length === 0 && unknownFilter.length === 0) return;
1151
- const parts = [];
1152
- if (unknownInclude.length > 0) parts.push(`includeFields has unknown field name(s) ${formatUnknownNameList(unknownInclude)}`);
1153
- if (unknownFilter.length > 0) parts.push(`filter condition(s) carry unknown field name(s) ${formatUnknownNameList(unknownFilter)}`);
1154
- const wireClause = record.operation === "count" ? "the count request was not issued" : "the query POST was not issued";
1155
- throw new AutotaskConfigError(
1156
- `invoke: "${record.id}" ${parts.join("; ")} against ${authority} \u2014 ${wireClause}`,
1157
- {
1158
- operation: record.id,
1159
- suggestedAction: `Use exact vendor field names (operations.fieldCatalog lists the live set for the entity); pass fieldCheck: false to disable this name check. The authority here was ${authority}.`
1160
- }
1161
- );
1162
- }
1163
1127
  var INVOKE_OPTION_KEYS = ["dryRun", "confirm", "lastModifiedDate", "maxRetries", "mode"];
1164
1128
  function validateInvokeOptions(opts, operation) {
1165
1129
  if (opts === void 0 || opts === null) return;
@@ -1187,8 +1151,25 @@ function unknownKeysRefusal(raw, validKeys, subject, operation) {
1187
1151
  }
1188
1152
  );
1189
1153
  }
1190
- async function invokeMode(client, operation, argsIn = [], opts = {}) {
1191
- const args = argsIn ?? [];
1154
+
1155
+ // src/operations/invoke/registry.ts
1156
+ var RECORDS = CAPABILITIES;
1157
+ var byId = new Map(RECORDS.map((r) => [r.id, r]));
1158
+
1159
+ // src/operations/invoke/dispatch.ts
1160
+ var INVOKE_MAX_INLINE_BYTES = 1024 * 1024;
1161
+ function bufferResult(buf) {
1162
+ if (buf.byteLength <= INVOKE_MAX_INLINE_BYTES) {
1163
+ return { encoding: "base64", byteLength: buf.byteLength, truncated: false, data: buf.toString("base64") };
1164
+ }
1165
+ return {
1166
+ encoding: "base64",
1167
+ byteLength: buf.byteLength,
1168
+ truncated: true,
1169
+ note: `Body exceeds the ${INVOKE_MAX_INLINE_BYTES}-byte inline limit and was not returned; call the resource's download method directly.`
1170
+ };
1171
+ }
1172
+ function resolveRecord(operation) {
1192
1173
  const record = byId.get(operation);
1193
1174
  if (record === void 0) {
1194
1175
  throw new AutotaskConfigError(
@@ -1196,6 +1177,9 @@ async function invokeMode(client, operation, argsIn = [], opts = {}) {
1196
1177
  { operation, suggestedAction: "List the registry (the ./capabilities subpath) for the exact operation key." }
1197
1178
  );
1198
1179
  }
1180
+ return record;
1181
+ }
1182
+ function governPreRequest(record, operation, opts) {
1199
1183
  validateInvokeOptions(opts, operation);
1200
1184
  const mode = opts.mode;
1201
1185
  if (mode !== void 0 && MODE_BY_EFFECT[record.effect] !== mode) {
@@ -1207,8 +1191,7 @@ async function invokeMode(client, operation, argsIn = [], opts = {}) {
1207
1191
  { operation, suggestedAction: "Pass dryRun: true (simulate) or dryRun: false (apply) as a real boolean." }
1208
1192
  );
1209
1193
  }
1210
- const isWrite = record.effect !== "read";
1211
- if (!isWrite && opts.dryRun === true) {
1194
+ if (record.effect === "read" && opts.dryRun === true) {
1212
1195
  throw new AutotaskConfigError(
1213
1196
  `invoke: "${operation}" is a read \u2014 dryRun is not applicable (reads issue their request as-is)`,
1214
1197
  { operation }
@@ -1221,53 +1204,57 @@ async function invokeMode(client, operation, argsIn = [], opts = {}) {
1221
1204
  "Pass confirm equal to the operation key to acknowledge the deliberate act."
1222
1205
  );
1223
1206
  }
1224
- validateDispatchArgs(record, args);
1225
- validateParamsBag(record, args);
1226
- const boundedQuery = record.kind === "primitive" && record.operation === "query";
1227
- const countedQuery = record.kind === "primitive" && record.operation === "count";
1228
- let holder = client[camelKey(record.resource)];
1207
+ }
1208
+ function operationsHolder(client) {
1209
+ return {
1210
+ resolveAny: (identifier, opts) => resolveAny(client, identifier, opts),
1211
+ searchAcrossResources: (query, opts) => searchAcrossResources(client, query, opts),
1212
+ fetchMany: (items, opts) => fetchMany(client, items, opts),
1213
+ // Issue #62: the generic aggregate + the cheap count — the
1214
+ // groupBy/sum-absent mode is ONE vendor /query/count (the S12-C blind
1215
+ // sentinel rides through); the group/sum modes walk the named
1216
+ // <resource>.query op via the house bounded walk and fold client-side
1217
+ // (truncation honesty: hasMore/totalScanned — no silent partial sums).
1218
+ aggregate: (operation, filter, opts) => aggregate(client, operation, filter, opts),
1219
+ // Issue #59 PR-B: the bulk fetch by ids — query-first chunked `id in`
1220
+ // reads + the per-id `get` fallback (blind-surface-safe; per-item
1221
+ // typed outcomes reuse the fetchMany shapes).
1222
+ fetchByIds: (items, opts) => fetchByIds(client, items, opts),
1223
+ // Issue #65: opt-in resource name expansion — per-id resources.get
1224
+ // reads ONLY (never an id-in query — the resources table can be blind
1225
+ // to count/query while per-id reads work); blind-marked rows for ids
1226
+ // the API user cannot resolve; the name cache (default-on).
1227
+ expandResourceNames: (ids, opts) => expandResourceNames(client, ids, opts),
1228
+ relationshipMap: (resourceKey) => relationshipMap(client, resourceKey),
1229
+ // Issue #11: the served blind-surface inventory (zero wire) + the
1230
+ // bounded caller-opted reachability sweep (read-only, confirmProbe-
1231
+ // required — the op's own pre-request gates keep it deliberate).
1232
+ blindSurfaces: (opts) => blindSurfaces(client, opts),
1233
+ probeReachability: (opts) => probeReachability(client, opts),
1234
+ // Issue #12: the served zone-fact create-guidance catalog (zero wire).
1235
+ createGuidance: (opts) => createGuidance(client, opts),
1236
+ // Issue #202: ExecuteCommand deep links — pure, zero wire.
1237
+ uiLink: (input) => uiLink(client, input),
1238
+ // Issue #31: the opt-in live field-metadata channel — N bounded
1239
+ // entityInformation GETs (the op's own cap refuses over-cap fan-outs
1240
+ // pre-request; per-resource failures are recorded on the row and
1241
+ // never abort the call).
1242
+ fieldCatalog: (opts) => fieldCatalog(client, opts),
1243
+ // Issue #9: the one-call recovery/cleanup op — effect write in the
1244
+ // registry (the governor's dry-run-first applies to it); the op's own
1245
+ // confirm gate makes the destructive action deliberate (the governor
1246
+ // forwards the caller's confirm through the trailing bag below).
1247
+ recovery: (input, opts) => recovery(client, input, opts)
1248
+ };
1249
+ }
1250
+ function resolveHolder(client, record, operation) {
1251
+ const clientBag = client;
1252
+ let holder = clientBag[camelKey(record.resource)];
1229
1253
  if (typeof holder === "function") {
1230
- holder = client[`${camelKey(record.resource)}Resource`] ?? holder;
1254
+ holder = clientBag[`${camelKey(record.resource)}Resource`] ?? holder;
1231
1255
  }
1232
1256
  if (holder === void 0 && record.resource === "operations") {
1233
- holder = {
1234
- resolveAny: (identifier, opts2) => resolveAny(client, identifier, opts2),
1235
- searchAcrossResources: (query, opts2) => searchAcrossResources(client, query, opts2),
1236
- fetchMany: (items, opts2) => fetchMany(client, items, opts2),
1237
- // Issue #62: the generic aggregate + the cheap count — the
1238
- // groupBy/sum-absent mode is ONE vendor /query/count (the S12-C blind
1239
- // sentinel rides through); the group/sum modes walk the named
1240
- // <resource>.query op via the house bounded walk and fold client-side
1241
- // (truncation honesty: hasMore/totalScanned — no silent partial sums).
1242
- aggregate: (operation2, filter, opts2) => aggregate(client, operation2, filter, opts2),
1243
- // Issue #59 PR-B: the bulk fetch by ids — query-first chunked `id in`
1244
- // reads + the per-id `get` fallback (blind-surface-safe; per-item
1245
- // typed outcomes reuse the fetchMany shapes).
1246
- fetchByIds: (items, opts2) => fetchByIds(client, items, opts2),
1247
- // Issue #65: opt-in resource name expansion — per-id resources.get
1248
- // reads ONLY (never an id-in query — the resources table can be blind
1249
- // to count/query while per-id reads work); blind-marked rows for ids
1250
- // the API user cannot resolve; the name cache (default-on).
1251
- expandResourceNames: (ids, opts2) => expandResourceNames(client, ids, opts2),
1252
- relationshipMap: (resourceKey) => relationshipMap(client, resourceKey),
1253
- // Issue #11: the served blind-surface inventory (zero wire) + the
1254
- // bounded caller-opted reachability sweep (read-only, confirmProbe-
1255
- // required — the op's own pre-request gates keep it deliberate).
1256
- blindSurfaces: (opts2) => blindSurfaces(client, opts2),
1257
- probeReachability: (opts2) => probeReachability(client, opts2),
1258
- // Issue #12: the served zone-fact create-guidance catalog (zero wire).
1259
- createGuidance: (opts2) => createGuidance(client, opts2),
1260
- // Issue #31: the opt-in live field-metadata channel — N bounded
1261
- // entityInformation GETs (the op's own cap refuses over-cap fan-outs
1262
- // pre-request; per-resource failures are recorded on the row and
1263
- // never abort the call).
1264
- fieldCatalog: (opts2) => fieldCatalog(client, opts2),
1265
- // Issue #9: the one-call recovery/cleanup op — effect write in the
1266
- // registry (the governor's dry-run-first applies to it); the op's own
1267
- // confirm gate makes the destructive action deliberate (the governor
1268
- // forwards the caller's confirm through the trailing bag below).
1269
- recovery: (input, opts2) => recovery(client, input, opts2)
1270
- };
1257
+ holder = operationsHolder(client);
1271
1258
  }
1272
1259
  if (holder === void 0) {
1273
1260
  throw new AutotaskConfigError(
@@ -1275,15 +1262,13 @@ async function invokeMode(client, operation, argsIn = [], opts = {}) {
1275
1262
  { operation }
1276
1263
  );
1277
1264
  }
1278
- if (boundedQuery || countedQuery) await validateQueryFieldNames(record, holder, args);
1279
- const methodName = boundedQuery ? "queryBounded" : record.operation;
1280
- const method = holder[methodName];
1281
- if (typeof method !== "function") {
1282
- throw new AutotaskConfigError(
1283
- `invoke: "${record.resource}.${methodName}" is not a callable method on the client`,
1284
- { operation }
1285
- );
1286
- }
1265
+ return holder;
1266
+ }
1267
+ var BAG_RIDING_ARGS = {
1268
+ createIfNotExists: ["dedupFields"],
1269
+ moveConfigurationItem: ["destinationCompanyId"]
1270
+ };
1271
+ function buildMutationOpts(opts, isWrite) {
1287
1272
  const dryRun = isWrite ? opts.dryRun !== false : false;
1288
1273
  const mutationOpts = { dryRun };
1289
1274
  if (isWrite && opts.lastModifiedDate !== void 0) {
@@ -1295,10 +1280,9 @@ async function invokeMode(client, operation, argsIn = [], opts = {}) {
1295
1280
  if (isWrite && opts.confirm !== void 0) {
1296
1281
  mutationOpts.confirm = opts.confirm;
1297
1282
  }
1298
- const BAG_RIDING_ARGS = {
1299
- createIfNotExists: ["dedupFields"],
1300
- moveConfigurationItem: ["destinationCompanyId"]
1301
- };
1283
+ return mutationOpts;
1284
+ }
1285
+ function buildCallArgs(record, args, mutationOpts, isWrite) {
1302
1286
  const declaredNames = Object.keys(record.inputSchema ?? {});
1303
1287
  const bagRiding = new Set(BAG_RIDING_ARGS[record.operation] ?? []);
1304
1288
  let positional = args;
@@ -1315,21 +1299,34 @@ async function invokeMode(client, operation, argsIn = [], opts = {}) {
1315
1299
  while (callArgs.length < declaredPositional.length) callArgs.push(void 0);
1316
1300
  callArgs.push(mutationOpts);
1317
1301
  }
1318
- const result = await method.apply(holder, callArgs);
1319
- return Buffer.isBuffer(result) ? bufferResult(result) : result;
1302
+ return callArgs;
1320
1303
  }
1321
- var INVOKE_MAX_INLINE_BYTES = 1024 * 1024;
1322
- function bufferResult(buf) {
1323
- if (buf.byteLength <= INVOKE_MAX_INLINE_BYTES) {
1324
- return { encoding: "base64", byteLength: buf.byteLength, truncated: false, data: buf.toString("base64") };
1304
+ async function invokeMode(client, operation, argsIn = [], opts = {}) {
1305
+ const args = argsIn ?? [];
1306
+ const record = resolveRecord(operation);
1307
+ governPreRequest(record, operation, opts);
1308
+ validateDispatchArgs(record, args);
1309
+ validateParamsBag(record, args);
1310
+ const boundedQuery = record.kind === "primitive" && record.operation === "query";
1311
+ const countedQuery = record.kind === "primitive" && record.operation === "count";
1312
+ const holder = resolveHolder(client, record, operation);
1313
+ if (boundedQuery || countedQuery) await validateQueryFieldNames(record, holder, args);
1314
+ const methodName = boundedQuery ? "queryBounded" : record.operation;
1315
+ const method = holder[methodName];
1316
+ if (typeof method !== "function") {
1317
+ throw new AutotaskConfigError(
1318
+ `invoke: "${record.resource}.${methodName}" is not a callable method on the client`,
1319
+ { operation }
1320
+ );
1325
1321
  }
1326
- return {
1327
- encoding: "base64",
1328
- byteLength: buf.byteLength,
1329
- truncated: true,
1330
- note: `Body exceeds the ${INVOKE_MAX_INLINE_BYTES}-byte inline limit and was not returned; call the resource's download method directly.`
1331
- };
1322
+ const isWrite = record.effect !== "read";
1323
+ const mutationOpts = buildMutationOpts(opts, isWrite);
1324
+ const callArgs = buildCallArgs(record, args, mutationOpts, isWrite);
1325
+ const result = await method.apply(holder, callArgs);
1326
+ return Buffer.isBuffer(result) ? bufferResult(result) : result;
1332
1327
  }
1328
+
1329
+ // src/operations/invoke/wrappers.ts
1333
1330
  async function invoke(client, operation, args = [], opts = {}) {
1334
1331
  return invokeMode(client, operation, args, { ...opts, mode: void 0 });
1335
1332
  }
@@ -1362,9 +1359,11 @@ export {
1362
1359
  RECOVERY_CONFIRM,
1363
1360
  RECOVERY_DEFAULT_LIMIT,
1364
1361
  RECOVERY_MAX_LIMIT,
1362
+ UI_LINK_ENTITIES,
1365
1363
  WORKFLOW_RESOURCES,
1366
1364
  aggregate,
1367
1365
  blindSurfaces,
1366
+ buildUiLink,
1368
1367
  createGuidance,
1369
1368
  dateBucketLabel,
1370
1369
  expandResourceNames,
@@ -1381,6 +1380,7 @@ export {
1381
1380
  relationshipMap,
1382
1381
  resolveAny,
1383
1382
  searchAcrossResources,
1383
+ uiLink,
1384
1384
  unknownKeysRefusal,
1385
1385
  validateDispatchArgs,
1386
1386
  validateFetchManyItems,