@withpica/mcp-sdk 3.1.1 → 3.3.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
@@ -203,6 +203,43 @@ class BaseResource {
203
203
  }
204
204
  return (data.data || data);
205
205
  }
206
+ /**
207
+ * Make a request whose SUCCESS body is raw text, not a JSON envelope —
208
+ * for routes that stream a generated file (e.g. `text/csv`) rather than
209
+ * `{success, data}`. `request()` unconditionally calls `response.json()`,
210
+ * which throws a SyntaxError parsing CSV text; every such route needs this
211
+ * method instead (see `ImportResource.getTemplate()` for the prior
212
+ * precedent this generalises).
213
+ *
214
+ * `fetchWithRetry` already throws `ApiError` for non-2xx responses (reading
215
+ * the body as text), so reaching here means `response.ok`. Still guard
216
+ * against a 2xx JSON error envelope (`{success:false}`) via content-type,
217
+ * since a route can commit to status 200 before a later validation step.
218
+ */
219
+ async requestText(method, path) {
220
+ const url = `${this.baseUrl}${path}`;
221
+ const timeoutMs = getTimeoutForPath(path);
222
+ if (this.debug) {
223
+ console.error(`[PICA SDK] ${method} ${url} (timeout: ${timeoutMs}ms)`);
224
+ }
225
+ const response = await this.fetchWithRetry(url, {
226
+ method,
227
+ headers: {
228
+ Authorization: `Bearer ${this.apiKey}`,
229
+ ...this.getBypassHeaders(),
230
+ },
231
+ }, timeoutMs);
232
+ const contentType = response.headers.get("content-type") || "";
233
+ if (contentType.includes("application/json")) {
234
+ const data = await response.json();
235
+ if (data && typeof data === "object" && data.success === false) {
236
+ const msg = typeof data.error === "string" ? data.error : "unspecified error";
237
+ throw new ApiError(`API returned success:false: ${msg}`, response.status, false);
238
+ }
239
+ return typeof data === "string" ? data : JSON.stringify(data);
240
+ }
241
+ return response.text();
242
+ }
206
243
  /**
207
244
  * Make a request and return paginated result with metadata
208
245
  */
@@ -393,6 +430,45 @@ class PeopleResource extends BaseResource {
393
430
  async bulkDelete(ids) {
394
431
  return this.request("POST", "/admin/people/bulk-delete", { ids });
395
432
  }
433
+ /**
434
+ * List a person's artist projects — per-stage-name Spotify/YouTube
435
+ * identity + enrichment status (ADR-289 Wave B2 item 4, ports the
436
+ * bespoke assistant's `get_person_artist_projects`).
437
+ */
438
+ async listArtistProjects(personId) {
439
+ return this.request("GET", `/admin/people/${personId}/artist-projects`);
440
+ }
441
+ /**
442
+ * Trigger enrichment for every stage name on a person — matches each
443
+ * against Spotify/YouTube artist identity, auto-applying confident
444
+ * matches and flagging ambiguous ones for disambiguation (ADR-289
445
+ * Wave B2 item 4, ports `enrich_artist_projects`).
446
+ */
447
+ async enrichArtistProjects(personId) {
448
+ return this.request("POST", `/admin/people/${personId}/enrich-artist-projects`);
449
+ }
450
+ /**
451
+ * Mark one of a person's artist projects as primary (unsets any other
452
+ * primary project for the same person server-side) (ADR-289 Wave B2
453
+ * item 4, ports `set_primary_artist_project`).
454
+ */
455
+ async setPrimaryArtistProject(artistProjectId) {
456
+ return this.request("PATCH", `/admin/artist-projects/${artistProjectId}/set-primary`);
457
+ }
458
+ /**
459
+ * Check whether an external artist name already matches an existing
460
+ * person — exact MusicBrainz-id match, then ISNI-via-MusicBrainz, then
461
+ * a fuzzy name fallback. Read-only (ADR-289 Wave B2 item 5, ports the
462
+ * bespoke assistant's `triangulate_artist` — verdict: NOT subsumed by
463
+ * `pica_resolve_person`, which requires an existing person_id and
464
+ * writes identifiers rather than searching for a match by bare name).
465
+ */
466
+ async triangulate(params) {
467
+ const qp = new URLSearchParams({ artist_name: params.artist_name });
468
+ if (params.musicbrainz_id)
469
+ qp.set("musicbrainz_id", params.musicbrainz_id);
470
+ return this.request("GET", `/admin/people/triangulate?${qp.toString()}`);
471
+ }
396
472
  }
397
473
  class LicensingResource extends BaseResource {
398
474
  /**
@@ -478,6 +554,58 @@ class LicensingResource extends BaseResource {
478
554
  async updateEnquiryStatus(id, status, notes) {
479
555
  return this.request("PATCH", `/admin/license-enquiries/${id}`, { status, your_notes: notes });
480
556
  }
557
+ /**
558
+ * Set a counter offer on a license enquiry (ADR-289 Wave B1 item 6 —
559
+ * counter-offer fields on `pica_update_license_enquiry_status`, ports
560
+ * the bespoke assistant's `set_license_counter_offer`). Setting a counter
561
+ * offer also moves the enquiry to status "quoted" server-side.
562
+ */
563
+ async setLicenseEnquiryCounterOffer(id, counterOffer, notes) {
564
+ return this.request("PATCH", `/admin/license-enquiries/${id}`, { counterOffer, notes });
565
+ }
566
+ }
567
+ /**
568
+ * ADR-289 Wave C1 — booking enquiries (internal-team session/production
569
+ * bookings, distinct from `LicensingResource`'s sync-license enquiries).
570
+ * Wraps `/admin/bookings` + `/admin/bookings/[id]`.
571
+ */
572
+ class BookingsResource extends BaseResource {
573
+ /**
574
+ * List/search booking enquiries. `GET /admin/bookings` returns the raw
575
+ * array (not `{data, count}`) — `request()` unwraps to that array.
576
+ */
577
+ async list(params) {
578
+ const qp = new URLSearchParams();
579
+ if (params?.status)
580
+ qp.set("status", params.status);
581
+ if (params?.priority)
582
+ qp.set("priority", params.priority);
583
+ if (params?.query)
584
+ qp.set("query", params.query);
585
+ if (params?.enquiry_type)
586
+ qp.set("enquiry_type", params.enquiry_type);
587
+ if (params?.follow_up_required !== undefined)
588
+ qp.set("follow_up_required", String(params.follow_up_required));
589
+ if (params?.limit !== undefined)
590
+ qp.set("limit", String(params.limit));
591
+ if (params?.offset !== undefined)
592
+ qp.set("offset", String(params.offset));
593
+ const qs = qp.toString();
594
+ return this.request("GET", `/admin/bookings${qs ? `?${qs}` : ""}`);
595
+ }
596
+ async get(id) {
597
+ return this.request("GET", `/admin/bookings/${encodeURIComponent(id)}`);
598
+ }
599
+ /**
600
+ * `PATCH /admin/bookings/[id]` only processes ONE update mode per call
601
+ * (status, OR responseContent+respondedBy, OR followUp — the route
602
+ * returns on the first matching branch). Callers needing multiple
603
+ * independent updates in one tool invocation must call this once per
604
+ * mode (mirrors the bespoke assistant's three separate handlers).
605
+ */
606
+ async update(id, data) {
607
+ return this.request("PATCH", `/admin/bookings/${encodeURIComponent(id)}`, data);
608
+ }
481
609
  }
482
610
  class CreditsResource extends BaseResource {
483
611
  async listForWork(workId) {
@@ -636,6 +764,8 @@ class AgreementsResource extends BaseResource {
636
764
  queryParams.set("party_name", params.party_name);
637
765
  if (params?.includeWorkCounts)
638
766
  queryParams.set("includeWorkCounts", "true");
767
+ if (params?.awaiting_signature)
768
+ queryParams.set("awaiting_signature", "true");
639
769
  if (params?.limit !== undefined)
640
770
  queryParams.set("limit", String(params.limit));
641
771
  if (params?.offset !== undefined)
@@ -649,6 +779,26 @@ class AgreementsResource extends BaseResource {
649
779
  async create(data) {
650
780
  return this.request("POST", "/admin/agreements", data);
651
781
  }
782
+ /**
783
+ * ADR-289 — the entry point of the e-signature flow. Renders the named
784
+ * template against `variables` server-side and creates the resulting
785
+ * agreement in `draft` status with `required_signers` set — the ONLY
786
+ * route that can populate `required_signers` on create (plain `create()`
787
+ * above has no such field). Follow with `sendForSignature()` to email the
788
+ * signing links.
789
+ */
790
+ async createFromTemplate(params) {
791
+ return this.request("POST", "/admin/agreements/create-from-template", {
792
+ templateId: params.templateId,
793
+ title: params.title,
794
+ variables: params.variables,
795
+ requiredSigners: params.requiredSigners,
796
+ agreementType: params.agreementType,
797
+ description: params.description,
798
+ expiresInDays: params.expiresInDays,
799
+ otherPartyName: params.otherPartyName,
800
+ });
801
+ }
652
802
  async update(id, updates) {
653
803
  return this.request("PATCH", `/admin/agreements/${id}`, updates);
654
804
  }
@@ -661,6 +811,23 @@ class AgreementsResource extends BaseResource {
661
811
  async linkWork(id, data) {
662
812
  return this.request("POST", `/admin/agreements/${id}/works`, data);
663
813
  }
814
+ /**
815
+ * Unlink (detach) a work from an agreement (ADR-289 Wave B1 item 5 —
816
+ * `detach` mode on `pica_agreements_link_work`, ports the bespoke
817
+ * assistant's `unlink_agreement_from_work`).
818
+ */
819
+ async unlinkWork(id, workId) {
820
+ await this.request("DELETE", `/admin/agreements/${id}/works?work_id=${encodeURIComponent(workId)}`);
821
+ }
822
+ /**
823
+ * Per-signer signature status: name, signed/pending, signed_at
824
+ * (ADR-289 Wave B1 item 3 — backs the `signatures` section on
825
+ * `pica_agreements_inspect`, ports the bespoke assistant's
826
+ * `get_agreement_signature_status`).
827
+ */
828
+ async getSignatureStatusWithSigners(id) {
829
+ return this.request("GET", `/admin/agreements/${id}/signatures`);
830
+ }
664
831
  async sourceAgreementSplits(workId, agreementId, extractedSplits, confidence, confirm) {
665
832
  return this.request("POST", `/admin/works/${workId}/agreement-source-splits`, {
666
833
  agreement_id: agreementId,
@@ -669,6 +836,14 @@ class AgreementsResource extends BaseResource {
669
836
  confirm: confirm ?? false,
670
837
  });
671
838
  }
839
+ /**
840
+ * ADR-289 — mint a per-signer `/sign/<token>` link and email every required
841
+ * signer of a draft agreement. Every signer lands in exactly one of
842
+ * `sent`/`skipped` (with a reason) — never silently half-done.
843
+ */
844
+ async sendForSignature(id, params) {
845
+ return this.request("POST", `/admin/agreements/${id}/send-for-signature`, { expiresInDays: params?.expires_in_days });
846
+ }
672
847
  }
673
848
  // ADR-222 — sync placements as a first-class domain. REST routes implemented
674
849
  // by W4 at /api/admin/sync-placements (paired PR on the same parent feature
@@ -785,6 +960,14 @@ class MemoryResource extends BaseResource {
785
960
  async delete(id) {
786
961
  await this.request("DELETE", `/admin/memory/${id}`);
787
962
  }
963
+ // ADR-289 Wave C2 — pica_memory_update. Backing route is
964
+ // PATCH /admin/memory/[id] (lib/services/assistant-memory
965
+ // updateMemory); only content is exposed here (mirrors the retired
966
+ // bespoke assistant's handleUpdateMemory, which hard-coded
967
+ // source:"corrected" server-side).
968
+ async update(id, params) {
969
+ return this.request("PATCH", `/admin/memory/${id}`, params);
970
+ }
788
971
  }
789
972
  class NotesResource extends BaseResource {
790
973
  async list(params) {
@@ -1862,7 +2045,20 @@ class ExportResource extends BaseResource {
1862
2045
  qs.set("workId", params.work_id);
1863
2046
  if (params.work_ids && params.work_ids.length > 0)
1864
2047
  qs.set("workIds", params.work_ids.join(","));
1865
- return this.request("GET", `/admin/works/export?${qs.toString()}`);
2048
+ const path = `/admin/works/export?${qs.toString()}`;
2049
+ // sync-pdf is the only format returning a JSON envelope
2050
+ // ({success, data, companyDetails}) — pro/distributor/nro stream raw
2051
+ // `text/csv` on success (route: app/api/admin/works/export/route.ts).
2052
+ // request() unconditionally calls response.json(), which throws parsing
2053
+ // a CSV body — every pro/distributor/nro call 500'd with INTERNAL_ERROR
2054
+ // (pica_export_catalog_csv: 5/5 prod errors, ops audit 2026-07-04).
2055
+ // requestText() mirrors the ImportResource.getTemplate() precedent for
2056
+ // the same response shape.
2057
+ if (params.format === "sync-pdf") {
2058
+ return this.request("GET", path);
2059
+ }
2060
+ const csv = await this.requestText("GET", path);
2061
+ return { format: params.format, csv };
1866
2062
  }
1867
2063
  async songRegistration(params) {
1868
2064
  // Returns a signed-URL JSON envelope:
@@ -1961,6 +2157,11 @@ class SendResource extends BaseResource {
1961
2157
  async resend(id) {
1962
2158
  return this.request("POST", `/admin/send/${id}/resend`);
1963
2159
  }
2160
+ // ADR-289 Wave C2 — pica_send_cancel. Backing route is
2161
+ // DELETE /admin/send/[id] (sendHubService.cancel).
2162
+ async cancel(id) {
2163
+ return this.request("DELETE", `/admin/send/${id}`);
2164
+ }
1964
2165
  }
1965
2166
  // --- Assets Resource (physical assets: equipment, instruments, studio gear) ---
1966
2167
  class AssetsResource extends BaseResource {
@@ -2413,6 +2614,83 @@ class ProjectsResource extends BaseResource {
2413
2614
  async attachWork(projectId, data) {
2414
2615
  return this.request("POST", `/admin/projects/${projectId}/works`, data);
2415
2616
  }
2617
+ /**
2618
+ * Detach (remove) a work from a project (ADR-289 Wave B2 item 2 —
2619
+ * `detach` mode on `pica_projects_attach_works`, ports the bespoke
2620
+ * assistant's `remove_work_from_project`). The work itself is not
2621
+ * deleted — only the `project_works` junction row.
2622
+ */
2623
+ async detachWork(projectId, workId) {
2624
+ await this.request("DELETE", `/admin/projects/${projectId}/works/${encodeURIComponent(workId)}`);
2625
+ }
2626
+ // -------------------------------------------------------------------
2627
+ // ADR-289 Wave C1 — participants, ports the bespoke assistant's
2628
+ // add_participants / update_participant / remove_participant /
2629
+ // list_project_participants / get_participant_details trio.
2630
+ //
2631
+ // Route asymmetry (both pre-existing, not introduced here):
2632
+ // - PATCH /participants/[participantId] keys off the participant's
2633
+ // OWN row id.
2634
+ // - DELETE /participants/[participantId] actually keys off person_id
2635
+ // (route comment: "participantId here is actually the person_id
2636
+ // for consistency"). removeParticipant() below reproduces that
2637
+ // literally; callers wanting participant-id ergonomics resolve via
2638
+ // listParticipants() first (see ProjectsTools.manageParticipants).
2639
+ // -------------------------------------------------------------------
2640
+ async listParticipants(projectId) {
2641
+ return this.request("GET", `/admin/projects/${projectId}/participants`);
2642
+ }
2643
+ async addParticipant(projectId, data) {
2644
+ return this.request("POST", `/admin/projects/${projectId}/participants`, data);
2645
+ }
2646
+ async addParticipantsBulk(projectId, participants) {
2647
+ return this.request("POST", `/admin/projects/${projectId}/participants`, {
2648
+ participants,
2649
+ });
2650
+ }
2651
+ /** `participantId` is the `project_participants` row id. */
2652
+ async updateParticipant(projectId, participantId, data) {
2653
+ return this.request("PATCH", `/admin/projects/${projectId}/participants/${encodeURIComponent(participantId)}`, data);
2654
+ }
2655
+ /** `personId` — the route's `[participantId]` segment is actually person_id here. */
2656
+ async removeParticipant(projectId, personId) {
2657
+ await this.request("DELETE", `/admin/projects/${projectId}/participants/${encodeURIComponent(personId)}`);
2658
+ }
2659
+ // -------------------------------------------------------------------
2660
+ // ADR-289 Wave C1 — multimedia, ports the bespoke assistant's
2661
+ // list_project_multimedia / add_multimedia_to_project /
2662
+ // update_project_multimedia / remove_multimedia_from_project.
2663
+ //
2664
+ // Route asymmetry (both pre-existing, not introduced here):
2665
+ // - PATCH /multimedia/[multimediaId] keys off the `project_multimedia`
2666
+ // join row's OWN id.
2667
+ // - DELETE /multimedia/[multimediaId] keys off `multimedia_id` (the
2668
+ // underlying multimedia_items id), not the join row id. Callers
2669
+ // wanting join-row-id ergonomics resolve via listMultimedia() first
2670
+ // (see ProjectsTools.manageMultimedia).
2671
+ // -------------------------------------------------------------------
2672
+ async listMultimedia(projectId, day) {
2673
+ const qs = day !== undefined ? `?day=${day}` : "";
2674
+ return this.request("GET", `/admin/projects/${projectId}/multimedia${qs}`);
2675
+ }
2676
+ async addMultimedia(projectId, data) {
2677
+ return this.request("POST", `/admin/projects/${projectId}/multimedia`, data);
2678
+ }
2679
+ async addMultimediaBulk(projectId, multimediaIds, options) {
2680
+ return this.request("POST", `/admin/projects/${projectId}/multimedia`, {
2681
+ multimedia_ids: multimediaIds,
2682
+ project_day: options?.project_day,
2683
+ allow_download: options?.allow_download,
2684
+ });
2685
+ }
2686
+ /** `multimediaLinkId` is the `project_multimedia` join row's own id. */
2687
+ async updateMultimediaLink(projectId, multimediaLinkId, data) {
2688
+ return this.request("PATCH", `/admin/projects/${projectId}/multimedia/${encodeURIComponent(multimediaLinkId)}`, data);
2689
+ }
2690
+ /** `multimediaId` — the route's `[multimediaId]` segment on DELETE is the underlying multimedia_items id, not the join row id. */
2691
+ async removeMultimediaLink(projectId, multimediaId) {
2692
+ await this.request("DELETE", `/admin/projects/${projectId}/multimedia/${encodeURIComponent(multimediaId)}`);
2693
+ }
2416
2694
  }
2417
2695
  class SplitSheetsResource extends BaseResource {
2418
2696
  async listForWork(workId) {
@@ -3121,6 +3399,25 @@ class TelegramResource extends BaseResource {
3121
3399
  async getPreferences() {
3122
3400
  return this.request("GET", "/admin/settings/telegram/notifications");
3123
3401
  }
3402
+ // ADR-289 Wave C2 — pica_telegram_config write half. The REST route
3403
+ // (POST /admin/settings/telegram/notifications) speaks camelCase
3404
+ // (eventType/isActive/configs) — translate from the snake_case tool args
3405
+ // here so the tool layer stays consistent with every other tool's
3406
+ // snake_case convention.
3407
+ async setPreferences(params) {
3408
+ const body = {};
3409
+ if (params.event_type !== undefined) {
3410
+ body.eventType = params.event_type;
3411
+ body.isActive = params.is_enabled;
3412
+ }
3413
+ if (params.bulk_config) {
3414
+ body.configs = params.bulk_config.map((c) => ({
3415
+ eventType: c.event_type,
3416
+ isActive: c.is_enabled,
3417
+ }));
3418
+ }
3419
+ return this.request("POST", "/admin/settings/telegram/notifications", body);
3420
+ }
3124
3421
  // ADR-159: MCP-native Telegram pairing. Wraps the pair endpoint which
3125
3422
  // returns a magic code (or short-circuits on already-connected / live
3126
3423
  // pending code). Rate-limited to 3 new codes per user per rolling hour.
@@ -3167,6 +3464,8 @@ export class PicaClient {
3167
3464
  people;
3168
3465
  recordings;
3169
3466
  licensing;
3467
+ // ADR-289 Wave C1 — internal-team booking enquiries.
3468
+ bookings;
3170
3469
  credits;
3171
3470
  creditsBalance;
3172
3471
  picaScore;
@@ -3274,6 +3573,7 @@ export class PicaClient {
3274
3573
  this.people = new PeopleResource(baseUrl, config.apiKey, debug);
3275
3574
  this.recordings = new RecordingsResource(baseUrl, config.apiKey, debug);
3276
3575
  this.licensing = new LicensingResource(baseUrl, config.apiKey, debug);
3576
+ this.bookings = new BookingsResource(baseUrl, config.apiKey, debug);
3277
3577
  this.credits = new CreditsResource(baseUrl, config.apiKey, debug);
3278
3578
  this.creditsBalance = new CreditsBalanceResource(baseUrl, config.apiKey, debug);
3279
3579
  this.picaScore = new PicaScoreResource(baseUrl, config.apiKey, debug);