@withpica/mcp-sdk 3.23.0 → 3.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.
package/dist/index.js CHANGED
@@ -54,6 +54,14 @@ function getTimeoutForPath(path) {
54
54
  ? LONG_TIMEOUT_MS
55
55
  : DEFAULT_TIMEOUT_MS;
56
56
  }
57
+ /** The `offset` query parameter of a request path (0 when absent). */
58
+ function offsetOf(path) {
59
+ const q = path.indexOf("?");
60
+ if (q === -1)
61
+ return 0;
62
+ const n = Number(new URLSearchParams(path.slice(q + 1)).get("offset"));
63
+ return Number.isFinite(n) && n > 0 ? n : 0;
64
+ }
57
65
  function sleep(ms) {
58
66
  return new Promise((resolve) => setTimeout(resolve, ms));
59
67
  }
@@ -245,13 +253,20 @@ class BaseResource {
245
253
  * for routes that stream a generated file (e.g. `text/csv`) rather than
246
254
  * `{success, data}`. `request()` unconditionally calls `response.json()`,
247
255
  * which throws a SyntaxError parsing CSV text; every such route needs this
248
- * method instead (see `ImportResource.getTemplate()` for the prior
249
- * precedent this generalises).
256
+ * method instead (`ImportResource.getTemplate()`, `ExportResource.catalogCsv()`).
250
257
  *
251
258
  * `fetchWithRetry` already throws `ApiError` for non-2xx responses (reading
252
259
  * the body as text), so reaching here means `response.ok`. Still guard
253
260
  * against a 2xx JSON error envelope (`{success:false}`) via content-type,
254
261
  * since a route can commit to status 200 before a later validation step.
262
+ *
263
+ * An HTML page is refused. Neither route that uses this
264
+ * (`/admin/import/template`, `/admin/works/export` CSV formats) answers
265
+ * `text/html`, so one means something in front of PICA answered (on a
266
+ * protected Vercel preview, a missing bypass header ends on Vercel's
267
+ * login page with status 200). Returning that page as the file read as a
268
+ * success (2026-10-06, pica_import_template). Locked by
269
+ * `mcp-server/src/__tests__/sdk-request-text-and-bare-fetch.test.ts`.
255
270
  */
256
271
  async requestText(method, path) {
257
272
  const url = `${this.baseUrl}${path}`;
@@ -275,6 +290,9 @@ class BaseResource {
275
290
  }
276
291
  return typeof data === "string" ? data : JSON.stringify(data);
277
292
  }
293
+ if (contentType.includes("text/html")) {
294
+ throw new ApiError(`Expected a file from ${path} but got an HTML page; the request did not reach PICA`, response.status, false);
295
+ }
278
296
  return response.text();
279
297
  }
280
298
  /**
@@ -302,8 +320,18 @@ class BaseResource {
302
320
  // `total`/`hasMore` (e.g. GET /admin/recordings/search, ADR unified
303
321
  // catalog search). Check nested first, then flat, then fall back to
304
322
  // `count` / items.length so untouched callers keep their prior behaviour.
305
- const total = json.pagination?.total ?? json.total ?? json.count ?? items.length;
306
- const hasMore = json.pagination?.hasMore ?? json.hasMore ?? total > items.length;
323
+ // An explicit `pagination.total: null` means the server does not know
324
+ // the total — keep it null rather than falling through to the page
325
+ // length, which would read as "that's all of them".
326
+ const total = json.pagination && json.pagination.total === null
327
+ ? null
328
+ : (json.pagination?.total ?? json.total ?? json.count ?? items.length);
329
+ // With no server-side hasMore, the answer is offset + this page < total.
330
+ // `total > items.length` alone ignored the offset, so every LAST page of
331
+ // a multi-page result said "more" (row-cap T3).
332
+ const hasMore = json.pagination?.hasMore ??
333
+ json.hasMore ??
334
+ (total !== null && offsetOf(path) + items.length < total);
307
335
  // Carry the server's zero-result guidance through — silently dropping it
308
336
  // here left agents staring at a bare empty list (ops_issue 689a8e9a).
309
337
  const recovery_hint = typeof json.recovery_hint === "string" ? json.recovery_hint : undefined;
@@ -396,6 +424,15 @@ class WorksResource extends BaseResource {
396
424
  const qs = qp.toString();
397
425
  return this.requestPaginated("GET", `/admin/works${qs ? `?${qs}` : ""}`);
398
426
  }
427
+ /**
428
+ * The agreements linked to ONE work (GET /admin/works/:id/agreements).
429
+ * Inspect used `agreements.list()`, which returned the org's first page of
430
+ * agreements under a work's heading (row-cap T3).
431
+ */
432
+ async getAgreements(workId) {
433
+ const res = await this.request("GET", `/admin/works/${workId}/agreements`);
434
+ return Array.isArray(res) ? res : [];
435
+ }
399
436
  async get(id) {
400
437
  return this.request("GET", `/admin/works/${id}`);
401
438
  }
@@ -507,6 +544,22 @@ class WorksResource extends BaseResource {
507
544
  async addWriters(workId, names) {
508
545
  return this.request("POST", `/admin/works/${workId}/writers`, { names });
509
546
  }
547
+ /** A song's directory topics: approved, proposed, moods awaiting review, settings. */
548
+ async getThemes(workId) {
549
+ return this.request("GET", `/admin/works/${encodeURIComponent(workId)}/themes`);
550
+ }
551
+ /** Set the song's approved topics directly, and/or its switch and lyric visibility. */
552
+ async setThemes(workId, body) {
553
+ return this.request("PUT", `/admin/works/${encodeURIComponent(workId)}/themes`, body);
554
+ }
555
+ /** Propose topics (and recording moods) for the owner to review. Nothing goes public. */
556
+ async proposeThemes(workId, body) {
557
+ return this.request("POST", `/admin/works/${encodeURIComponent(workId)}/themes/proposals`, body);
558
+ }
559
+ /** The owner's review of proposed topics and moods. */
560
+ async reviewThemes(workId, body) {
561
+ return this.request("PATCH", `/admin/works/${encodeURIComponent(workId)}/themes/proposals`, body);
562
+ }
510
563
  }
511
564
  class PeopleResource extends BaseResource {
512
565
  async list(params) {
@@ -618,6 +671,23 @@ class PeopleResource extends BaseResource {
618
671
  return this.request("GET", `/admin/people/triangulate?${qp.toString()}`);
619
672
  }
620
673
  }
674
+ function licenseEnquiryListPath(params) {
675
+ const queryParams = new URLSearchParams();
676
+ if (params?.work_id)
677
+ queryParams.set("work_id", params.work_id);
678
+ if (params?.status)
679
+ queryParams.set("status", params.status);
680
+ if (params?.project_type)
681
+ queryParams.set("project_type", params.project_type);
682
+ if (params?.territory)
683
+ queryParams.set("territory", params.territory);
684
+ if (params?.limit !== undefined)
685
+ queryParams.set("limit", String(params.limit));
686
+ if (params?.offset !== undefined)
687
+ queryParams.set("offset", String(params.offset));
688
+ const query = queryParams.toString();
689
+ return `/admin/license-enquiries${query ? `?${query}` : ""}`;
690
+ }
621
691
  class LicensingResource extends BaseResource {
622
692
  /**
623
693
  * Search for sync-licensable tracks by audio characteristics
@@ -663,33 +733,26 @@ class LicensingResource extends BaseResource {
663
733
  return this.request("POST", "/admin/license-enquiries", enquiry);
664
734
  }
665
735
  /**
666
- * List license enquiries with optional filters
736
+ * List license enquiries with optional filters (one page, the bare array;
737
+ * see listEnquiriesPage for the real total).
667
738
  */
668
739
  async listEnquiries(params) {
669
- const queryParams = new URLSearchParams();
670
- if (params?.work_id)
671
- queryParams.set("work_id", params.work_id);
672
- if (params?.status)
673
- queryParams.set("status", params.status);
674
- if (params?.project_type)
675
- queryParams.set("project_type", params.project_type);
676
- if (params?.territory)
677
- queryParams.set("territory", params.territory);
678
- if (params?.limit !== undefined)
679
- queryParams.set("limit", String(params.limit));
680
- if (params?.offset !== undefined)
681
- queryParams.set("offset", String(params.offset));
682
- const query = queryParams.toString();
683
- const path = `/admin/license-enquiries${query ? `?${query}` : ""}`;
684
- // The admin route returns successResponse({ data: [...], count }); after
685
- // request() unwraps the outer { success, data } envelope the payload is the
686
- // { data, count } object, not the array. Tolerate both shapes (mirrors
687
- // listDiscoveriesRecent + the other list resources) so the caller never
688
- // calls `.map` on a non-array — the "t.map is not a function" crash that
689
- // made pica_list_license_enquiries fail on every call shape.
690
- const res = (await this.request("GET", path));
740
+ // Older servers nested the list as `{ data: { data: [...], count } }`, so
741
+ // after request() unwraps the outer envelope the payload was an object,
742
+ // not the array. Tolerate both shapes so the caller never calls `.map` on
743
+ // a non-array — the "t.map is not a function" crash that made
744
+ // pica_list_license_enquiries fail on every call shape.
745
+ const res = (await this.request("GET", licenseEnquiryListPath(params)));
691
746
  return Array.isArray(res) ? res : (res?.data ?? []);
692
747
  }
748
+ /**
749
+ * One page of license enquiries WITH the real total and has_more
750
+ * (row-cap T3): `listEnquiries()` returns the bare array, so callers
751
+ * reported the page length as the total.
752
+ */
753
+ async listEnquiriesPage(params) {
754
+ return this.requestPaginated("GET", licenseEnquiryListPath(params));
755
+ }
693
756
  /**
694
757
  * Get a specific license enquiry by ID
695
758
  */
@@ -712,34 +775,40 @@ class LicensingResource extends BaseResource {
712
775
  return this.request("PATCH", `/admin/license-enquiries/${id}`, { counterOffer, notes });
713
776
  }
714
777
  }
715
- /**
716
- * ADR-289 Wave C1 — booking enquiries (internal-team session/production
717
- * bookings, distinct from `LicensingResource`'s sync-license enquiries).
718
- * Wraps `/admin/bookings` + `/admin/bookings/[id]`.
719
- */
778
+ function bookingListPath(params) {
779
+ const qp = new URLSearchParams();
780
+ if (params?.status)
781
+ qp.set("status", params.status);
782
+ if (params?.priority)
783
+ qp.set("priority", params.priority);
784
+ if (params?.query)
785
+ qp.set("query", params.query);
786
+ if (params?.enquiry_type)
787
+ qp.set("enquiry_type", params.enquiry_type);
788
+ if (params?.follow_up_required !== undefined)
789
+ qp.set("follow_up_required", String(params.follow_up_required));
790
+ if (params?.limit !== undefined)
791
+ qp.set("limit", String(params.limit));
792
+ if (params?.offset !== undefined)
793
+ qp.set("offset", String(params.offset));
794
+ const qs = qp.toString();
795
+ return `/admin/bookings${qs ? `?${qs}` : ""}`;
796
+ }
720
797
  class BookingsResource extends BaseResource {
721
798
  /**
722
- * List/search booking enquiries. `GET /admin/bookings` returns the raw
723
- * array (not `{data, count}`) — `request()` unwraps to that array.
799
+ * One page of booking enquiries (the bare array; see listPage for the real
800
+ * total). `request()` unwraps the route's `{ data, count }` envelope.
724
801
  */
725
802
  async list(params) {
726
- const qp = new URLSearchParams();
727
- if (params?.status)
728
- qp.set("status", params.status);
729
- if (params?.priority)
730
- qp.set("priority", params.priority);
731
- if (params?.query)
732
- qp.set("query", params.query);
733
- if (params?.enquiry_type)
734
- qp.set("enquiry_type", params.enquiry_type);
735
- if (params?.follow_up_required !== undefined)
736
- qp.set("follow_up_required", String(params.follow_up_required));
737
- if (params?.limit !== undefined)
738
- qp.set("limit", String(params.limit));
739
- if (params?.offset !== undefined)
740
- qp.set("offset", String(params.offset));
741
- const qs = qp.toString();
742
- return this.request("GET", `/admin/bookings${qs ? `?${qs}` : ""}`);
803
+ return this.request("GET", bookingListPath(params));
804
+ }
805
+ /**
806
+ * One page of booking enquiries WITH the real total and has_more
807
+ * (row-cap T3): `list()` returns the bare array, so callers reported the
808
+ * page length as the total.
809
+ */
810
+ async listPage(params) {
811
+ return this.requestPaginated("GET", bookingListPath(params));
743
812
  }
744
813
  async get(id) {
745
814
  return this.request("GET", `/admin/bookings/${encodeURIComponent(id)}`);
@@ -1074,39 +1143,51 @@ class MultimediaResource extends BaseResource {
1074
1143
  });
1075
1144
  }
1076
1145
  }
1146
+ function agreementListPath(params) {
1147
+ const queryParams = new URLSearchParams();
1148
+ if (params?.query)
1149
+ queryParams.set("query", params.query);
1150
+ if (params?.agreement_type)
1151
+ queryParams.set("agreement_type", params.agreement_type);
1152
+ if (params?.status)
1153
+ queryParams.set("status", params.status);
1154
+ if (params?.party_name)
1155
+ queryParams.set("party_name", params.party_name);
1156
+ if (params?.includeWorkCounts)
1157
+ queryParams.set("includeWorkCounts", "true");
1158
+ if (params?.awaiting_signature)
1159
+ queryParams.set("awaiting_signature", "true");
1160
+ if (params?.start_date_from)
1161
+ queryParams.set("start_date_from", params.start_date_from);
1162
+ if (params?.start_date_to)
1163
+ queryParams.set("start_date_to", params.start_date_to);
1164
+ if (params?.end_date_from)
1165
+ queryParams.set("end_date_from", params.end_date_from);
1166
+ if (params?.end_date_to)
1167
+ queryParams.set("end_date_to", params.end_date_to);
1168
+ if (params?.expiring_within_days !== undefined)
1169
+ queryParams.set("expiring_within_days", String(params.expiring_within_days));
1170
+ if (params?.has_end_date !== undefined)
1171
+ queryParams.set("has_end_date", String(params.has_end_date));
1172
+ if (params?.limit !== undefined)
1173
+ queryParams.set("limit", String(params.limit));
1174
+ if (params?.offset !== undefined)
1175
+ queryParams.set("offset", String(params.offset));
1176
+ const query = queryParams.toString();
1177
+ return `/admin/agreements${query ? `?${query}` : ""}`;
1178
+ }
1077
1179
  class AgreementsResource extends BaseResource {
1180
+ /** One page of agreements (the bare array; see listPage for the real total). */
1078
1181
  async list(params) {
1079
- const queryParams = new URLSearchParams();
1080
- if (params?.query)
1081
- queryParams.set("query", params.query);
1082
- if (params?.agreement_type)
1083
- queryParams.set("agreement_type", params.agreement_type);
1084
- if (params?.status)
1085
- queryParams.set("status", params.status);
1086
- if (params?.party_name)
1087
- queryParams.set("party_name", params.party_name);
1088
- if (params?.includeWorkCounts)
1089
- queryParams.set("includeWorkCounts", "true");
1090
- if (params?.awaiting_signature)
1091
- queryParams.set("awaiting_signature", "true");
1092
- if (params?.start_date_from)
1093
- queryParams.set("start_date_from", params.start_date_from);
1094
- if (params?.start_date_to)
1095
- queryParams.set("start_date_to", params.start_date_to);
1096
- if (params?.end_date_from)
1097
- queryParams.set("end_date_from", params.end_date_from);
1098
- if (params?.end_date_to)
1099
- queryParams.set("end_date_to", params.end_date_to);
1100
- if (params?.expiring_within_days !== undefined)
1101
- queryParams.set("expiring_within_days", String(params.expiring_within_days));
1102
- if (params?.has_end_date !== undefined)
1103
- queryParams.set("has_end_date", String(params.has_end_date));
1104
- if (params?.limit !== undefined)
1105
- queryParams.set("limit", String(params.limit));
1106
- if (params?.offset !== undefined)
1107
- queryParams.set("offset", String(params.offset));
1108
- const query = queryParams.toString();
1109
- return this.request("GET", `/admin/agreements${query ? `?${query}` : ""}`);
1182
+ return this.request("GET", agreementListPath(params));
1183
+ }
1184
+ /**
1185
+ * One page of agreements WITH the real total and has_more (row-cap T3):
1186
+ * `list()` returns the bare array, so callers reported the page length
1187
+ * as the total.
1188
+ */
1189
+ async listPage(params) {
1190
+ return this.requestPaginated("GET", agreementListPath(params));
1110
1191
  }
1111
1192
  async get(id) {
1112
1193
  return this.request("GET", `/admin/agreements/${id}`);
@@ -1179,10 +1260,28 @@ class AgreementsResource extends BaseResource {
1179
1260
  return this.request("POST", `/admin/agreements/${id}/send-for-signature`, { expiresInDays: params?.expires_in_days });
1180
1261
  }
1181
1262
  }
1182
- // ADR-222 — sync placements as a first-class domain. REST routes implemented
1183
- // by W4 at /api/admin/sync-placements (paired PR on the same parent feature
1184
- // branch). All methods scope to the caller's organisation server-side via
1185
- // requireOrganisationContext — no orgId param needed on the SDK surface.
1263
+ /** Sync fit: GET /admin/sync-fit, the same read as /inspect/sync. */
1264
+ class SyncFitResource extends BaseResource {
1265
+ async query(params) {
1266
+ const q = new URLSearchParams();
1267
+ if (params?.work_id)
1268
+ q.set("work_id", params.work_id);
1269
+ if (params?.screen_project_id)
1270
+ q.set("screen_project_id", params.screen_project_id);
1271
+ if (params?.song_facts)
1272
+ q.set("song_facts", "true");
1273
+ if (params?.limit)
1274
+ q.set("limit", String(params.limit));
1275
+ if (params?.offset)
1276
+ q.set("offset", String(params.offset));
1277
+ const qs = q.toString();
1278
+ const res = await this.request("GET", `/admin/sync-fit${qs ? `?${qs}` : ""}`);
1279
+ if (res && typeof res === "object" && "data" in res && res.data) {
1280
+ return res.data;
1281
+ }
1282
+ return res;
1283
+ }
1284
+ }
1186
1285
  class SyncPlacementsResource extends BaseResource {
1187
1286
  async list(params) {
1188
1287
  const queryParams = new URLSearchParams();
@@ -1725,6 +1824,16 @@ class EnrichmentResource extends BaseResource {
1725
1824
  async bulkRejectProposals(input) {
1726
1825
  return this.request("POST", "/admin/enrichment-proposals/bulk-reject", input);
1727
1826
  }
1827
+ /**
1828
+ * What bulkRejectProposals would reject, without rejecting: the route
1829
+ * resolves the same pending set the real call would (row-cap T3).
1830
+ */
1831
+ async previewBulkRejectProposals(input) {
1832
+ return this.request("POST", "/admin/enrichment-proposals/bulk-reject", {
1833
+ ...input,
1834
+ dry_run: true,
1835
+ });
1836
+ }
1728
1837
  /**
1729
1838
  * Apply many pending proposals in one call — the accept side of the
1730
1839
  * /inspect/found queue.
@@ -2548,7 +2657,7 @@ class ExportResource extends BaseResource {
2548
2657
  // request() unconditionally calls response.json(), which throws parsing
2549
2658
  // a CSV body — every pro/distributor/nro call 500'd with INTERNAL_ERROR
2550
2659
  // (pica_export_catalog_csv: 5/5 prod errors, ops audit 2026-07-04).
2551
- // requestText() mirrors the ImportResource.getTemplate() precedent for
2660
+ // requestText() serves ImportResource.getTemplate() too, for
2552
2661
  // the same response shape.
2553
2662
  if (params.format === "sync-pdf") {
2554
2663
  return this.request("GET", path);
@@ -2847,14 +2956,10 @@ class ImportResource extends BaseResource {
2847
2956
  });
2848
2957
  }
2849
2958
  async getTemplate(domain) {
2850
- // Template route returns CSV as text/csv — use raw fetch
2851
- const url = `${this.baseUrl}/admin/import/template?domain=${domain}`;
2852
- const response = await fetch(url, {
2853
- headers: { Authorization: `Bearer ${this.apiKey}` },
2854
- });
2855
- if (!response.ok)
2856
- throw new Error(`Template fetch failed: ${response.status}`);
2857
- return response.text();
2959
+ // The route answers text/csv, so requestText(), not request(). It was a
2960
+ // bare fetch() with no preview-protection bypass header, so on a Vercel
2961
+ // preview it never reached the route (2026-10-06).
2962
+ return this.requestText("GET", `/admin/import/template?domain=${encodeURIComponent(domain)}`);
2858
2963
  }
2859
2964
  /**
2860
2965
  * Emails that reached the organisation's forwarding address, newest first,
@@ -2937,6 +3042,40 @@ class CollaboratorsResource extends BaseResource {
2937
3042
  const qs = qp.toString();
2938
3043
  return this.request("GET", `/admin/collaborators/invites${qs ? `?${qs}` : ""}`);
2939
3044
  }
3045
+ /**
3046
+ * One page of the invites this org sent WITH the real total and has_more
3047
+ * (row-cap T3). `listInvites()` is typed as an array but the route nests
3048
+ * its rows as `data: { data, count }`, so it never carried a usable total.
3049
+ */
3050
+ async listInvitesPage(params) {
3051
+ const qp = new URLSearchParams();
3052
+ if (params?.status)
3053
+ qp.set("status", params.status);
3054
+ if (params?.workId)
3055
+ qp.set("workId", params.workId);
3056
+ if (params?.limit !== undefined)
3057
+ qp.set("limit", String(params.limit));
3058
+ if (params?.offset)
3059
+ qp.set("offset", String(params.offset));
3060
+ const qs = qp.toString();
3061
+ return this.requestNestedPage(`/admin/collaborators/invites${qs ? `?${qs}` : ""}`, params?.offset ?? 0);
3062
+ }
3063
+ /**
3064
+ * Reads a collaborator list route, whose rows sit at `data.data` (kept
3065
+ * for published SDKs) with the total in the top-level `pagination`.
3066
+ */
3067
+ async requestNestedPage(path, offset) {
3068
+ const json = await this.requestWithEnvelope("GET", path);
3069
+ const inner = json.data;
3070
+ const rows = Array.isArray(inner) ? inner : (inner?.data ?? []);
3071
+ const pagination = json.pagination;
3072
+ const total = pagination?.total ?? rows.length;
3073
+ return {
3074
+ data: rows,
3075
+ total,
3076
+ hasMore: pagination?.hasMore ?? offset + rows.length < total,
3077
+ };
3078
+ }
2940
3079
  async resendInvite(inviteId) {
2941
3080
  return this.request("POST", `/admin/collaborators/invites/${inviteId}/resend`);
2942
3081
  }
@@ -2955,6 +3094,21 @@ class CollaboratorsResource extends BaseResource {
2955
3094
  const res = await this.request("GET", `/admin/collaborators/incoming${qs ? `?${qs}` : ""}`);
2956
3095
  return res.data;
2957
3096
  }
3097
+ /**
3098
+ * One page of the invites addressed to the caller WITH the real total and
3099
+ * has_more (row-cap T3: `listIncoming()` returns only the rows).
3100
+ */
3101
+ async listIncomingPage(params) {
3102
+ const qp = new URLSearchParams();
3103
+ if (params?.status)
3104
+ qp.set("status", params.status);
3105
+ if (params?.limit !== undefined)
3106
+ qp.set("limit", String(params.limit));
3107
+ if (params?.offset)
3108
+ qp.set("offset", String(params.offset));
3109
+ const qs = qp.toString();
3110
+ return this.requestNestedPage(`/admin/collaborators/incoming${qs ? `?${qs}` : ""}`, params?.offset ?? 0);
3111
+ }
2958
3112
  /**
2959
3113
  * ADR-157 warm path — accept an invite addressed to the authenticated
2960
3114
  * user, and flip its status to confirmed. The credit write is
@@ -3186,6 +3340,10 @@ class DirectoryResource extends BaseResource {
3186
3340
  async updateSettings(updates) {
3187
3341
  return this.request("PATCH", "/admin/directory/settings", updates);
3188
3342
  }
3343
+ /** Listed songs with lyrics and topics on, but no approved topic yet. */
3344
+ async untaggedThemes() {
3345
+ return this.request("GET", "/admin/directory/themes/untagged");
3346
+ }
3189
3347
  }
3190
3348
  class ProjectsResource extends BaseResource {
3191
3349
  async list(params) {
@@ -3432,6 +3590,21 @@ class RecordingSplitsResource extends BaseResource {
3432
3590
  }
3433
3591
  }
3434
3592
  class PublishersResource extends BaseResource {
3593
+ /**
3594
+ * One page of the publisher registry with an honest has_more. The total
3595
+ * is null: the route does not count the platform-wide registry.
3596
+ */
3597
+ async listPage(params) {
3598
+ const queryParams = new URLSearchParams();
3599
+ if (params?.query)
3600
+ queryParams.set("query", params.query);
3601
+ if (params?.limit)
3602
+ queryParams.set("limit", String(params.limit));
3603
+ if (params?.offset)
3604
+ queryParams.set("offset", String(params.offset));
3605
+ const qs = queryParams.toString();
3606
+ return this.requestPaginated("GET", `/admin/publishers${qs ? `?${qs}` : ""}`);
3607
+ }
3435
3608
  async list(params) {
3436
3609
  const queryParams = new URLSearchParams();
3437
3610
  if (params?.query)
@@ -3446,6 +3619,25 @@ class PublishersResource extends BaseResource {
3446
3619
  }
3447
3620
  }
3448
3621
  class LabelsResource extends BaseResource {
3622
+ /**
3623
+ * One page of label/distributor organisations with an honest has_more.
3624
+ * The total is null: the route does not count platform-wide rows.
3625
+ */
3626
+ async listPage(params) {
3627
+ const queryParams = new URLSearchParams();
3628
+ if (params?.query)
3629
+ queryParams.set("query", params.query);
3630
+ if (params?.limit)
3631
+ queryParams.set("limit", String(params.limit));
3632
+ if (params?.offset)
3633
+ queryParams.set("offset", String(params.offset));
3634
+ if (params?.org_type)
3635
+ queryParams.set("org_type", params.org_type);
3636
+ if (params?.ids?.length)
3637
+ queryParams.set("ids", params.ids.join(","));
3638
+ const qs = queryParams.toString();
3639
+ return this.requestPaginated("GET", `/admin/labels${qs ? `?${qs}` : ""}`);
3640
+ }
3449
3641
  async list(params) {
3450
3642
  const queryParams = new URLSearchParams();
3451
3643
  if (params?.query)
@@ -3474,14 +3666,20 @@ class LabelsResource extends BaseResource {
3474
3666
  return (res ?? null);
3475
3667
  }
3476
3668
  }
3477
- /**
3478
- * ADR-268 WS1 — party org claim flow. `invite()` sends an artist-anchored
3479
- * claim invite for an unclaimed label/distributor/publisher org;
3480
- * `list()` reads the claims the calling org has sent.
3481
- *
3482
- * Typed against the UNWRAPPED reality (ADR-265 lesson): BaseResource.request
3483
- * auto-unwraps the `{success,data}` envelope, so methods see `data` itself.
3484
- */
3669
+ function partyClaimListPath(params) {
3670
+ const queryParams = new URLSearchParams();
3671
+ if (params?.target_organisation_id) {
3672
+ queryParams.set("target_organisation_id", params.target_organisation_id);
3673
+ }
3674
+ if (params?.status)
3675
+ queryParams.set("status", params.status);
3676
+ if (params?.limit)
3677
+ queryParams.set("limit", String(params.limit));
3678
+ if (params?.offset)
3679
+ queryParams.set("offset", String(params.offset));
3680
+ const qs = queryParams.toString();
3681
+ return `/admin/party-claims${qs ? `?${qs}` : ""}`;
3682
+ }
3485
3683
  class PartyClaimsResource extends BaseResource {
3486
3684
  async invite(params) {
3487
3685
  const res = await this.request("POST", "/admin/party-claims", params);
@@ -3493,20 +3691,19 @@ class PartyClaimsResource extends BaseResource {
3493
3691
  return (res ?? null);
3494
3692
  }
3495
3693
  async list(params) {
3496
- const queryParams = new URLSearchParams();
3497
- if (params?.target_organisation_id) {
3498
- queryParams.set("target_organisation_id", params.target_organisation_id);
3499
- }
3500
- if (params?.status)
3501
- queryParams.set("status", params.status);
3502
- if (params?.limit)
3503
- queryParams.set("limit", String(params.limit));
3504
- const qs = queryParams.toString();
3505
- const res = await this.request("GET", `/admin/party-claims${qs ? `?${qs}` : ""}`);
3694
+ const res = await this.request("GET", partyClaimListPath(params));
3506
3695
  if (Array.isArray(res))
3507
3696
  return res;
3508
3697
  return (res?.data ?? []);
3509
3698
  }
3699
+ /**
3700
+ * One page of sent claim invites WITH the real total and has_more
3701
+ * (row-cap T3): `list()` returns the bare array, so callers reported the
3702
+ * page length as the total.
3703
+ */
3704
+ async listPage(params) {
3705
+ return this.requestPaginated("GET", partyClaimListPath(params));
3706
+ }
3510
3707
  }
3511
3708
  /**
3512
3709
  * Claim your profile: invite people to claim their profile (a preview
@@ -3538,25 +3735,38 @@ class ProfileClaimsResource extends BaseResource {
3538
3735
  };
3539
3736
  }
3540
3737
  }
3738
+ function releaseListPath(params) {
3739
+ const parts = [];
3740
+ if (params?.limit != null)
3741
+ parts.push(`limit=${params.limit}`);
3742
+ if (params?.offset != null)
3743
+ parts.push(`offset=${params.offset}`);
3744
+ if (params?.query)
3745
+ parts.push(`query=${encodeURIComponent(params.query)}`);
3746
+ if (params?.upc)
3747
+ parts.push(`upc=${encodeURIComponent(params.upc)}`);
3748
+ if (params?.spotify_album_id)
3749
+ parts.push(`spotify_album_id=${encodeURIComponent(params.spotify_album_id)}`);
3750
+ if (params?.release_date_from)
3751
+ parts.push(`release_date_from=${encodeURIComponent(params.release_date_from)}`);
3752
+ if (params?.release_date_to)
3753
+ parts.push(`release_date_to=${encodeURIComponent(params.release_date_to)}`);
3754
+ const qs = parts.length > 0 ? `?${parts.join("&")}` : "";
3755
+ return `/admin/releases${qs}`;
3756
+ }
3541
3757
  class ReleasesResource extends BaseResource {
3758
+ /** One page of releases (the bare array; see listPage for the real total). */
3542
3759
  async list(params) {
3543
- const parts = [];
3544
- if (params?.limit != null)
3545
- parts.push(`limit=${params.limit}`);
3546
- if (params?.offset != null)
3547
- parts.push(`offset=${params.offset}`);
3548
- if (params?.query)
3549
- parts.push(`query=${encodeURIComponent(params.query)}`);
3550
- if (params?.upc)
3551
- parts.push(`upc=${encodeURIComponent(params.upc)}`);
3552
- if (params?.spotify_album_id)
3553
- parts.push(`spotify_album_id=${encodeURIComponent(params.spotify_album_id)}`);
3554
- if (params?.release_date_from)
3555
- parts.push(`release_date_from=${encodeURIComponent(params.release_date_from)}`);
3556
- if (params?.release_date_to)
3557
- parts.push(`release_date_to=${encodeURIComponent(params.release_date_to)}`);
3558
- const qs = parts.length > 0 ? `?${parts.join("&")}` : "";
3559
- return this.request("GET", `/admin/releases${qs}`);
3760
+ return this.request("GET", releaseListPath(params));
3761
+ }
3762
+ /**
3763
+ * One page of releases WITH the real total and has_more (row-cap T3):
3764
+ * `list()` returns the bare array, so callers guessed has_more from "the
3765
+ * page came back full". `total` is null when the server cannot count (a
3766
+ * title search past its match cap); has_more is the server's own answer.
3767
+ */
3768
+ async listPage(params) {
3769
+ return this.requestPaginated("GET", releaseListPath(params));
3560
3770
  }
3561
3771
  async get(id) {
3562
3772
  return this.request("GET", `/admin/releases/${id}`);
@@ -3675,15 +3885,27 @@ class ReleasesResource extends BaseResource {
3675
3885
  return this.request("POST", "/admin/releases/create-with-tracks", data);
3676
3886
  }
3677
3887
  }
3888
+ function sessionListPath(params) {
3889
+ const query = new URLSearchParams();
3890
+ if (params?.limit)
3891
+ query.set("limit", String(params.limit));
3892
+ if (params?.offset)
3893
+ query.set("offset", String(params.offset));
3894
+ const qs = query.toString();
3895
+ return `/admin/sessions${qs ? `?${qs}` : ""}`;
3896
+ }
3678
3897
  class SessionsResource extends BaseResource {
3898
+ /** One page of upcoming sessions (see listPage for the real total). */
3679
3899
  async list(params) {
3680
- const query = new URLSearchParams();
3681
- if (params?.limit)
3682
- query.set("limit", String(params.limit));
3683
- if (params?.offset)
3684
- query.set("offset", String(params.offset));
3685
- const qs = query.toString();
3686
- return this.request("GET", `/admin/sessions${qs ? `?${qs}` : ""}`);
3900
+ return this.request("GET", sessionListPath(params));
3901
+ }
3902
+ /**
3903
+ * One page of the next 7 days' sessions WITH the real total and has_more
3904
+ * (row-cap T3): `list()` returns the bare array, so callers could only
3905
+ * guess has_more from "the page came back full".
3906
+ */
3907
+ async listPage(params) {
3908
+ return this.requestPaginated("GET", sessionListPath(params));
3687
3909
  }
3688
3910
  async get(id) {
3689
3911
  return this.request("GET", `/admin/sessions/${id}`);
@@ -4038,6 +4260,11 @@ class CustodyResource extends BaseResource {
4038
4260
  async respond(body) {
4039
4261
  return this.request("POST", "/admin/custody/respond", body);
4040
4262
  }
4263
+ /**
4264
+ * Custody claims, one page per direction. Since row-cap T3 the result also
4265
+ * carries `totals: { incoming, outgoing }` (null for a direction not asked
4266
+ * for), `has_more: { incoming, outgoing }`, `limit` and `offset`.
4267
+ */
4041
4268
  async list(params) {
4042
4269
  const qp = new URLSearchParams();
4043
4270
  if (params?.direction)
@@ -4046,6 +4273,8 @@ class CustodyResource extends BaseResource {
4046
4273
  qp.set("status", params.status);
4047
4274
  if (params?.limit !== undefined)
4048
4275
  qp.set("limit", String(params.limit));
4276
+ if (params?.offset)
4277
+ qp.set("offset", String(params.offset));
4049
4278
  const qs = qp.toString();
4050
4279
  return this.request("GET", `/admin/custody/list${qs ? `?${qs}` : ""}`);
4051
4280
  }
@@ -4264,6 +4493,7 @@ export class PicaClient {
4264
4493
  agreements;
4265
4494
  // ADR-222 — sync placements (W3, paired with W4 REST routes).
4266
4495
  syncPlacements;
4496
+ syncFit;
4267
4497
  workspace;
4268
4498
  memory;
4269
4499
  audit;
@@ -4346,24 +4576,15 @@ export class PicaClient {
4346
4576
  // Registration Integrity Floor (spec 2026-07-09, Task 8).
4347
4577
  integrity;
4348
4578
  async catalogStats(params) {
4349
- const baseUrl = this.works["baseUrl"];
4350
- const apiKey = this.works["apiKey"];
4351
4579
  const qp = new URLSearchParams();
4352
4580
  if (params?.entity)
4353
4581
  qp.set("entity", params.entity);
4354
4582
  if (params?.group_by)
4355
4583
  qp.set("group_by", params.group_by);
4356
4584
  const suffix = qp.toString() ? `?${qp.toString()}` : "";
4357
- const res = await fetch(`${baseUrl}/admin/catalog/stats${suffix}`, {
4358
- headers: {
4359
- Authorization: `Bearer ${apiKey}`,
4360
- "Content-Type": "application/json",
4361
- },
4362
- });
4363
- if (!res.ok)
4364
- throw new Error(`Catalog stats failed: ${res.status}`);
4365
- const json = (await res.json());
4366
- return (json.data || json);
4585
+ // Through the shared request path (bypass header, timeout, retry, typed
4586
+ // errors) rather than a bare fetch() that skipped all four (2026-10-06).
4587
+ return this.works["request"]("GET", `/admin/catalog/stats${suffix}`);
4367
4588
  }
4368
4589
  constructor(config) {
4369
4590
  if (!config.apiKey) {
@@ -4385,6 +4606,7 @@ export class PicaClient {
4385
4606
  this.multimedia = new MultimediaResource(baseUrl, config.apiKey, debug);
4386
4607
  this.agreements = new AgreementsResource(baseUrl, config.apiKey, debug);
4387
4608
  this.syncPlacements = new SyncPlacementsResource(baseUrl, config.apiKey, debug);
4609
+ this.syncFit = new SyncFitResource(baseUrl, config.apiKey, debug);
4388
4610
  this.workspace = new WorkspaceResource(baseUrl, config.apiKey, debug);
4389
4611
  this.memory = new MemoryResource(baseUrl, config.apiKey, debug);
4390
4612
  this.audit = new AuditResource(baseUrl, config.apiKey, debug);