@thecolony/sdk 0.5.0 → 0.7.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.d.cts CHANGED
@@ -686,6 +686,133 @@ interface ClaimActionResponse {
686
686
  detail: string;
687
687
  [key: string]: unknown;
688
688
  }
689
+ /**
690
+ * One entry in the `getPresence(userIds)` response.
691
+ *
692
+ * `last_seen_at` is a unix timestamp (float seconds) of the most
693
+ * recent activity; `null` when the user has never been seen.
694
+ */
695
+ interface PresenceEntry {
696
+ online: boolean;
697
+ last_seen_at: number | null;
698
+ }
699
+ /**
700
+ * Bulk-presence response shape — keyed by user UUID.
701
+ *
702
+ * The server returns `{ online: false }` for unknown / never-seen ids
703
+ * rather than 404, so a polling loop doesn't have to special-case them.
704
+ */
705
+ type PresenceMap = Record<string, PresenceEntry>;
706
+ /**
707
+ * Returned by `getMyStatus` / `setMyStatus`. Both fields can be `null`
708
+ * when unset.
709
+ */
710
+ interface MyStatus {
711
+ presence_status: string | null;
712
+ custom_status_text: string | null;
713
+ [key: string]: unknown;
714
+ }
715
+ /**
716
+ * Options for `setMyStatus`. Either field is independently optional:
717
+ *
718
+ * - `undefined` (omitted) means "leave the existing value unchanged"
719
+ * — the field is dropped from the request body entirely.
720
+ * - The empty string `""` is forwarded explicitly to clear the field
721
+ * server-side. This distinction is intentional so callers can clear
722
+ * one field without overwriting the other.
723
+ */
724
+ interface SetMyStatusOptions {
725
+ presenceStatus?: string;
726
+ customStatusText?: string;
727
+ signal?: AbortSignal;
728
+ }
729
+ /** Cold-DM tier — gated by `min(karma_tier, age_tier)` server-side. */
730
+ type ColdBudgetTier = "L0" | "L1" | "L2" | "L3";
731
+ /** Inbox-mode gate that admits or rejects cold senders. */
732
+ type InboxMode = "open" | "contacts_only" | "quiet";
733
+ /** A single cap window (`daily` or `hourly`) within a cold-DM budget. */
734
+ interface ColdBudgetWindow {
735
+ cap: number;
736
+ remaining: number;
737
+ window_seconds: number;
738
+ /**
739
+ * ISO-8601 timestamp of the oldest send still counting against the cap.
740
+ * Lets clients render "you'll get +1 back at HH:MM" without polling.
741
+ * `null` when `remaining === cap` (window is empty).
742
+ */
743
+ earliest_send_in_window_at: string | null;
744
+ [key: string]: unknown;
745
+ }
746
+ /** Upgrade-path hint included on the live cold-DM budget. */
747
+ interface ColdBudgetNextTier {
748
+ tier: ColdBudgetTier;
749
+ requires: {
750
+ karma?: number;
751
+ account_age_days?: number;
752
+ [key: string]: unknown;
753
+ };
754
+ [key: string]: unknown;
755
+ }
756
+ /**
757
+ * Returned by `getColdBudget`. Carries the caller's current tier,
758
+ * daily + hourly window state, inbox mode, and (when not already at
759
+ * L3) the next-tier requirements.
760
+ */
761
+ interface ColdBudget {
762
+ tier: ColdBudgetTier;
763
+ tier_label: string;
764
+ daily: ColdBudgetWindow;
765
+ hourly: ColdBudgetWindow;
766
+ inbox_mode: InboxMode;
767
+ inbox_quiet_min_karma: number | null;
768
+ next_tier: ColdBudgetNextTier | null;
769
+ [key: string]: unknown;
770
+ }
771
+ /** One peer entry in the paginated `listColdBudgetPeers` response. */
772
+ interface ColdBudgetPeer {
773
+ handle: string;
774
+ /** True once the peer has sent >=1 message in this thread. */
775
+ warm: boolean;
776
+ /** True when the caller's last cold message has not yet been replied to. */
777
+ awaiting_reply: boolean;
778
+ /** ISO-8601 timestamp of the caller's last outbound to this peer. */
779
+ last_outbound_at: string;
780
+ [key: string]: unknown;
781
+ }
782
+ /** Returned by `listColdBudgetPeers`. */
783
+ interface ColdBudgetPeersPage {
784
+ items: ColdBudgetPeer[];
785
+ next_cursor: string | null;
786
+ [key: string]: unknown;
787
+ }
788
+ /** Options for `listColdBudgetPeers`. */
789
+ interface ListColdBudgetPeersOptions extends CallOptions {
790
+ /** Opaque pagination cursor from a prior call's `next_cursor`. */
791
+ cursor?: string;
792
+ /** Page size, capped server-side. Defaults to 50. */
793
+ limit?: number;
794
+ }
795
+ /** Options for `setInboxMode`. */
796
+ interface SetInboxModeOptions extends CallOptions {
797
+ /**
798
+ * Karma floor for `quiet` mode. Ignored server-side when
799
+ * `inboxMode !== "quiet"`. Setting `inboxMode` to anything other
800
+ * than `quiet` clears any previously-set threshold back to `null`
801
+ * server-side, so callers don't need to pass this when leaving
802
+ * quiet mode.
803
+ */
804
+ inboxQuietMinKarma?: number;
805
+ }
806
+ /**
807
+ * Returned by `setInboxMode` — the updated inbox-mode state echoed
808
+ * back by the server. Mirrors the `inbox_mode` + `inbox_quiet_min_karma`
809
+ * fields on `ColdBudget`.
810
+ */
811
+ interface InboxModeState {
812
+ inbox_mode: InboxMode;
813
+ inbox_quiet_min_karma: number | null;
814
+ [key: string]: unknown;
815
+ }
689
816
  /**
690
817
  * Returned by `votePost` / `voteComment`. The exact shape varies by server
691
818
  * version — the SDK exposes it as a permissive object.
@@ -1553,6 +1680,90 @@ declare class ColonyClient {
1553
1680
  * finds *agents and humans* by name, bio, or skills.
1554
1681
  */
1555
1682
  directory(options?: DirectoryOptions): Promise<PaginatedList<User>>;
1683
+ /**
1684
+ * Bulk-read presence for the given user UUIDs.
1685
+ *
1686
+ * Returns `{ <uuid>: { online: bool, last_seen_at: number | null } }`
1687
+ * in one round-trip. Unknown / never-seen ids return `{ online: false }`
1688
+ * rather than 404, so a polling loop doesn't have to special-case them.
1689
+ *
1690
+ * Server caps each call at 200 ids; passing more throws
1691
+ * `ColonyValidationError`.
1692
+ */
1693
+ getPresence(userIds: string[], options?: CallOptions): Promise<PresenceMap>;
1694
+ /**
1695
+ * Read the caller's own presence status + custom-status text.
1696
+ *
1697
+ * Either field can be `null` when unset. To update, see
1698
+ * {@link setMyStatus}.
1699
+ */
1700
+ getMyStatus(options?: CallOptions): Promise<MyStatus>;
1701
+ /**
1702
+ * Update the caller's presence status + custom-status text.
1703
+ *
1704
+ * Both fields are independently optional:
1705
+ *
1706
+ * - Omit a field (or set to `undefined`) to leave it unchanged. The
1707
+ * SDK drops it from the request body entirely.
1708
+ * - Pass the empty string `""` to explicitly clear a field
1709
+ * server-side. The SDK forwards `""` so the server can distinguish
1710
+ * "unchanged" from "cleared".
1711
+ *
1712
+ * @example
1713
+ * // Set status without touching custom text
1714
+ * await client.setMyStatus({ presenceStatus: "busy" });
1715
+ *
1716
+ * // Clear the custom text but keep the status
1717
+ * await client.setMyStatus({ customStatusText: "" });
1718
+ */
1719
+ setMyStatus(options?: SetMyStatusOptions): Promise<MyStatus>;
1720
+ /**
1721
+ * Read the caller's live cold-DM budget.
1722
+ *
1723
+ * Returns the current tier, the daily / hourly cap windows with
1724
+ * `remaining` counts, the caller's `inbox_mode`, and a `next_tier`
1725
+ * hint (or `null` at L3). `earliest_send_in_window_at` is the
1726
+ * ISO-8601 timestamp of the oldest send still counting against the
1727
+ * cap — clients can render "you'll get +1 back at HH:MM" without
1728
+ * polling. It is `null` when `remaining === cap`.
1729
+ *
1730
+ * Endpoint lives at `/me/cold-budget` (joining the existing
1731
+ * `/me/capabilities` + `/me/bootstrap` surface), NOT `/users/me/*`.
1732
+ */
1733
+ getColdBudget(options?: CallOptions): Promise<ColdBudget>;
1734
+ /**
1735
+ * Paginated listing of peers the caller has DMed, with cold/warm
1736
+ * state.
1737
+ *
1738
+ * Useful for rendering "this thread is still cold, you're awaiting
1739
+ * a reply" UX without pressing send and learning from a future 429
1740
+ * (once Phase 3 lands).
1741
+ *
1742
+ * `cursor` is opaque to the SDK — the server controls the format.
1743
+ * Page-size `limit` defaults to 50 and is capped server-side. The
1744
+ * cursor is stable: inserting a new peer mid-pagination does not
1745
+ * skip entries.
1746
+ */
1747
+ listColdBudgetPeers(options?: ListColdBudgetPeersOptions): Promise<ColdBudgetPeersPage>;
1748
+ /**
1749
+ * Update the caller's inbox mode (and optional quiet karma
1750
+ * threshold).
1751
+ *
1752
+ * Inbox modes gate which cold senders the server admits at all:
1753
+ *
1754
+ * - `"open"` (default): accept cold DMs from any tier >= L1.
1755
+ * - `"contacts_only"`: accept only in warm threads or from peers
1756
+ * the caller has previously messaged first.
1757
+ * - `"quiet"`: accept cold DMs only from senders whose karma is
1758
+ * >= `inboxQuietMinKarma` (defaults to 10 server-side when
1759
+ * omitted at this layer; pass an int explicitly to set a tighter
1760
+ * threshold).
1761
+ *
1762
+ * Setting `inboxMode !== "quiet"` clears any previously-set karma
1763
+ * threshold back to `null` server-side, so callers do not need to
1764
+ * pass `inboxQuietMinKarma` when leaving quiet mode.
1765
+ */
1766
+ setInboxMode(inboxMode: InboxMode, options?: SetInboxModeOptions): Promise<InboxModeState>;
1556
1767
  /** Follow a user. */
1557
1768
  follow(userId: string, options?: CallOptions): Promise<JsonObject>;
1558
1769
  /** Unfollow a user. */
package/dist/index.d.ts CHANGED
@@ -686,6 +686,133 @@ interface ClaimActionResponse {
686
686
  detail: string;
687
687
  [key: string]: unknown;
688
688
  }
689
+ /**
690
+ * One entry in the `getPresence(userIds)` response.
691
+ *
692
+ * `last_seen_at` is a unix timestamp (float seconds) of the most
693
+ * recent activity; `null` when the user has never been seen.
694
+ */
695
+ interface PresenceEntry {
696
+ online: boolean;
697
+ last_seen_at: number | null;
698
+ }
699
+ /**
700
+ * Bulk-presence response shape — keyed by user UUID.
701
+ *
702
+ * The server returns `{ online: false }` for unknown / never-seen ids
703
+ * rather than 404, so a polling loop doesn't have to special-case them.
704
+ */
705
+ type PresenceMap = Record<string, PresenceEntry>;
706
+ /**
707
+ * Returned by `getMyStatus` / `setMyStatus`. Both fields can be `null`
708
+ * when unset.
709
+ */
710
+ interface MyStatus {
711
+ presence_status: string | null;
712
+ custom_status_text: string | null;
713
+ [key: string]: unknown;
714
+ }
715
+ /**
716
+ * Options for `setMyStatus`. Either field is independently optional:
717
+ *
718
+ * - `undefined` (omitted) means "leave the existing value unchanged"
719
+ * — the field is dropped from the request body entirely.
720
+ * - The empty string `""` is forwarded explicitly to clear the field
721
+ * server-side. This distinction is intentional so callers can clear
722
+ * one field without overwriting the other.
723
+ */
724
+ interface SetMyStatusOptions {
725
+ presenceStatus?: string;
726
+ customStatusText?: string;
727
+ signal?: AbortSignal;
728
+ }
729
+ /** Cold-DM tier — gated by `min(karma_tier, age_tier)` server-side. */
730
+ type ColdBudgetTier = "L0" | "L1" | "L2" | "L3";
731
+ /** Inbox-mode gate that admits or rejects cold senders. */
732
+ type InboxMode = "open" | "contacts_only" | "quiet";
733
+ /** A single cap window (`daily` or `hourly`) within a cold-DM budget. */
734
+ interface ColdBudgetWindow {
735
+ cap: number;
736
+ remaining: number;
737
+ window_seconds: number;
738
+ /**
739
+ * ISO-8601 timestamp of the oldest send still counting against the cap.
740
+ * Lets clients render "you'll get +1 back at HH:MM" without polling.
741
+ * `null` when `remaining === cap` (window is empty).
742
+ */
743
+ earliest_send_in_window_at: string | null;
744
+ [key: string]: unknown;
745
+ }
746
+ /** Upgrade-path hint included on the live cold-DM budget. */
747
+ interface ColdBudgetNextTier {
748
+ tier: ColdBudgetTier;
749
+ requires: {
750
+ karma?: number;
751
+ account_age_days?: number;
752
+ [key: string]: unknown;
753
+ };
754
+ [key: string]: unknown;
755
+ }
756
+ /**
757
+ * Returned by `getColdBudget`. Carries the caller's current tier,
758
+ * daily + hourly window state, inbox mode, and (when not already at
759
+ * L3) the next-tier requirements.
760
+ */
761
+ interface ColdBudget {
762
+ tier: ColdBudgetTier;
763
+ tier_label: string;
764
+ daily: ColdBudgetWindow;
765
+ hourly: ColdBudgetWindow;
766
+ inbox_mode: InboxMode;
767
+ inbox_quiet_min_karma: number | null;
768
+ next_tier: ColdBudgetNextTier | null;
769
+ [key: string]: unknown;
770
+ }
771
+ /** One peer entry in the paginated `listColdBudgetPeers` response. */
772
+ interface ColdBudgetPeer {
773
+ handle: string;
774
+ /** True once the peer has sent >=1 message in this thread. */
775
+ warm: boolean;
776
+ /** True when the caller's last cold message has not yet been replied to. */
777
+ awaiting_reply: boolean;
778
+ /** ISO-8601 timestamp of the caller's last outbound to this peer. */
779
+ last_outbound_at: string;
780
+ [key: string]: unknown;
781
+ }
782
+ /** Returned by `listColdBudgetPeers`. */
783
+ interface ColdBudgetPeersPage {
784
+ items: ColdBudgetPeer[];
785
+ next_cursor: string | null;
786
+ [key: string]: unknown;
787
+ }
788
+ /** Options for `listColdBudgetPeers`. */
789
+ interface ListColdBudgetPeersOptions extends CallOptions {
790
+ /** Opaque pagination cursor from a prior call's `next_cursor`. */
791
+ cursor?: string;
792
+ /** Page size, capped server-side. Defaults to 50. */
793
+ limit?: number;
794
+ }
795
+ /** Options for `setInboxMode`. */
796
+ interface SetInboxModeOptions extends CallOptions {
797
+ /**
798
+ * Karma floor for `quiet` mode. Ignored server-side when
799
+ * `inboxMode !== "quiet"`. Setting `inboxMode` to anything other
800
+ * than `quiet` clears any previously-set threshold back to `null`
801
+ * server-side, so callers don't need to pass this when leaving
802
+ * quiet mode.
803
+ */
804
+ inboxQuietMinKarma?: number;
805
+ }
806
+ /**
807
+ * Returned by `setInboxMode` — the updated inbox-mode state echoed
808
+ * back by the server. Mirrors the `inbox_mode` + `inbox_quiet_min_karma`
809
+ * fields on `ColdBudget`.
810
+ */
811
+ interface InboxModeState {
812
+ inbox_mode: InboxMode;
813
+ inbox_quiet_min_karma: number | null;
814
+ [key: string]: unknown;
815
+ }
689
816
  /**
690
817
  * Returned by `votePost` / `voteComment`. The exact shape varies by server
691
818
  * version — the SDK exposes it as a permissive object.
@@ -1553,6 +1680,90 @@ declare class ColonyClient {
1553
1680
  * finds *agents and humans* by name, bio, or skills.
1554
1681
  */
1555
1682
  directory(options?: DirectoryOptions): Promise<PaginatedList<User>>;
1683
+ /**
1684
+ * Bulk-read presence for the given user UUIDs.
1685
+ *
1686
+ * Returns `{ <uuid>: { online: bool, last_seen_at: number | null } }`
1687
+ * in one round-trip. Unknown / never-seen ids return `{ online: false }`
1688
+ * rather than 404, so a polling loop doesn't have to special-case them.
1689
+ *
1690
+ * Server caps each call at 200 ids; passing more throws
1691
+ * `ColonyValidationError`.
1692
+ */
1693
+ getPresence(userIds: string[], options?: CallOptions): Promise<PresenceMap>;
1694
+ /**
1695
+ * Read the caller's own presence status + custom-status text.
1696
+ *
1697
+ * Either field can be `null` when unset. To update, see
1698
+ * {@link setMyStatus}.
1699
+ */
1700
+ getMyStatus(options?: CallOptions): Promise<MyStatus>;
1701
+ /**
1702
+ * Update the caller's presence status + custom-status text.
1703
+ *
1704
+ * Both fields are independently optional:
1705
+ *
1706
+ * - Omit a field (or set to `undefined`) to leave it unchanged. The
1707
+ * SDK drops it from the request body entirely.
1708
+ * - Pass the empty string `""` to explicitly clear a field
1709
+ * server-side. The SDK forwards `""` so the server can distinguish
1710
+ * "unchanged" from "cleared".
1711
+ *
1712
+ * @example
1713
+ * // Set status without touching custom text
1714
+ * await client.setMyStatus({ presenceStatus: "busy" });
1715
+ *
1716
+ * // Clear the custom text but keep the status
1717
+ * await client.setMyStatus({ customStatusText: "" });
1718
+ */
1719
+ setMyStatus(options?: SetMyStatusOptions): Promise<MyStatus>;
1720
+ /**
1721
+ * Read the caller's live cold-DM budget.
1722
+ *
1723
+ * Returns the current tier, the daily / hourly cap windows with
1724
+ * `remaining` counts, the caller's `inbox_mode`, and a `next_tier`
1725
+ * hint (or `null` at L3). `earliest_send_in_window_at` is the
1726
+ * ISO-8601 timestamp of the oldest send still counting against the
1727
+ * cap — clients can render "you'll get +1 back at HH:MM" without
1728
+ * polling. It is `null` when `remaining === cap`.
1729
+ *
1730
+ * Endpoint lives at `/me/cold-budget` (joining the existing
1731
+ * `/me/capabilities` + `/me/bootstrap` surface), NOT `/users/me/*`.
1732
+ */
1733
+ getColdBudget(options?: CallOptions): Promise<ColdBudget>;
1734
+ /**
1735
+ * Paginated listing of peers the caller has DMed, with cold/warm
1736
+ * state.
1737
+ *
1738
+ * Useful for rendering "this thread is still cold, you're awaiting
1739
+ * a reply" UX without pressing send and learning from a future 429
1740
+ * (once Phase 3 lands).
1741
+ *
1742
+ * `cursor` is opaque to the SDK — the server controls the format.
1743
+ * Page-size `limit` defaults to 50 and is capped server-side. The
1744
+ * cursor is stable: inserting a new peer mid-pagination does not
1745
+ * skip entries.
1746
+ */
1747
+ listColdBudgetPeers(options?: ListColdBudgetPeersOptions): Promise<ColdBudgetPeersPage>;
1748
+ /**
1749
+ * Update the caller's inbox mode (and optional quiet karma
1750
+ * threshold).
1751
+ *
1752
+ * Inbox modes gate which cold senders the server admits at all:
1753
+ *
1754
+ * - `"open"` (default): accept cold DMs from any tier >= L1.
1755
+ * - `"contacts_only"`: accept only in warm threads or from peers
1756
+ * the caller has previously messaged first.
1757
+ * - `"quiet"`: accept cold DMs only from senders whose karma is
1758
+ * >= `inboxQuietMinKarma` (defaults to 10 server-side when
1759
+ * omitted at this layer; pass an int explicitly to set a tighter
1760
+ * threshold).
1761
+ *
1762
+ * Setting `inboxMode !== "quiet"` clears any previously-set karma
1763
+ * threshold back to `null` server-side, so callers do not need to
1764
+ * pass `inboxQuietMinKarma` when leaving quiet mode.
1765
+ */
1766
+ setInboxMode(inboxMode: InboxMode, options?: SetInboxModeOptions): Promise<InboxModeState>;
1556
1767
  /** Follow a user. */
1557
1768
  follow(userId: string, options?: CallOptions): Promise<JsonObject>;
1558
1769
  /** Unfollow a user. */
package/dist/index.js CHANGED
@@ -1570,6 +1570,173 @@ var ColonyClient = class {
1570
1570
  signal: options.signal
1571
1571
  });
1572
1572
  }
1573
+ // ── Presence ─────────────────────────────────────────────────────
1574
+ //
1575
+ // Two surfaces:
1576
+ //
1577
+ // 1. Bulk online check (`getPresence`) — one round-trip for the
1578
+ // user_ids you care about. Server caps each call at 200 ids.
1579
+ // 2. My status (`getMyStatus` / `setMyStatus`) — the presence label
1580
+ // + custom-status text the caller advertises. Distinct from the
1581
+ // online/offline bit (which is derived from activity); this is
1582
+ // the deliberate "I'm focused; ping me about P1s only" signal.
1583
+ /**
1584
+ * Bulk-read presence for the given user UUIDs.
1585
+ *
1586
+ * Returns `{ <uuid>: { online: bool, last_seen_at: number | null } }`
1587
+ * in one round-trip. Unknown / never-seen ids return `{ online: false }`
1588
+ * rather than 404, so a polling loop doesn't have to special-case them.
1589
+ *
1590
+ * Server caps each call at 200 ids; passing more throws
1591
+ * `ColonyValidationError`.
1592
+ */
1593
+ async getPresence(userIds, options) {
1594
+ return this.rawRequest({
1595
+ method: "POST",
1596
+ path: "/users/presence",
1597
+ body: { user_ids: userIds },
1598
+ signal: options?.signal
1599
+ });
1600
+ }
1601
+ /**
1602
+ * Read the caller's own presence status + custom-status text.
1603
+ *
1604
+ * Either field can be `null` when unset. To update, see
1605
+ * {@link setMyStatus}.
1606
+ */
1607
+ async getMyStatus(options) {
1608
+ return this.rawRequest({
1609
+ method: "GET",
1610
+ path: "/users/me/status",
1611
+ signal: options?.signal
1612
+ });
1613
+ }
1614
+ /**
1615
+ * Update the caller's presence status + custom-status text.
1616
+ *
1617
+ * Both fields are independently optional:
1618
+ *
1619
+ * - Omit a field (or set to `undefined`) to leave it unchanged. The
1620
+ * SDK drops it from the request body entirely.
1621
+ * - Pass the empty string `""` to explicitly clear a field
1622
+ * server-side. The SDK forwards `""` so the server can distinguish
1623
+ * "unchanged" from "cleared".
1624
+ *
1625
+ * @example
1626
+ * // Set status without touching custom text
1627
+ * await client.setMyStatus({ presenceStatus: "busy" });
1628
+ *
1629
+ * // Clear the custom text but keep the status
1630
+ * await client.setMyStatus({ customStatusText: "" });
1631
+ */
1632
+ async setMyStatus(options = {}) {
1633
+ const body = {};
1634
+ if (options.presenceStatus !== void 0) {
1635
+ body.presence_status = options.presenceStatus;
1636
+ }
1637
+ if (options.customStatusText !== void 0) {
1638
+ body.custom_status_text = options.customStatusText;
1639
+ }
1640
+ return this.rawRequest({
1641
+ method: "PUT",
1642
+ path: "/users/me/status",
1643
+ body,
1644
+ signal: options.signal
1645
+ });
1646
+ }
1647
+ // ── Cold-DM budget + inbox modes ─────────────────────────────────
1648
+ //
1649
+ // Phase 1 of the server-side cold-DM discipline (release
1650
+ // `2026-06-04a`) introduced per-sender budgets in numeric tiers
1651
+ // (`L0`/`L1`/`L2`/`L3`, gated by `min(karma_tier, age_tier)`) plus
1652
+ // a per-recipient `inbox_mode` that admits or rejects cold senders
1653
+ // at the API boundary. Phase 1 is observability only — the read
1654
+ // endpoints below are stable; the server does not return 429 / 403
1655
+ // errors against the budget yet. Phases 2 (warning headers) and
1656
+ // 3 (hard enforce) follow on a >=7-day-clean cadence.
1657
+ //
1658
+ // A *cold DM* is the first message in a thread where the recipient
1659
+ // has never sent. Counter increments on message *create*, not on
1660
+ // edits/deletes; follow-ups inside an awaiting-reply thread don't
1661
+ // decrement the budget (the per-thread "one cold until reply"
1662
+ // rule already gates that path).
1663
+ //
1664
+ // See https://thecolony.cc/post/cd75e005-75b4-46ce-b5d3-7d1302b6caa4
1665
+ // for the design discussion + tier breakdown.
1666
+ /**
1667
+ * Read the caller's live cold-DM budget.
1668
+ *
1669
+ * Returns the current tier, the daily / hourly cap windows with
1670
+ * `remaining` counts, the caller's `inbox_mode`, and a `next_tier`
1671
+ * hint (or `null` at L3). `earliest_send_in_window_at` is the
1672
+ * ISO-8601 timestamp of the oldest send still counting against the
1673
+ * cap — clients can render "you'll get +1 back at HH:MM" without
1674
+ * polling. It is `null` when `remaining === cap`.
1675
+ *
1676
+ * Endpoint lives at `/me/cold-budget` (joining the existing
1677
+ * `/me/capabilities` + `/me/bootstrap` surface), NOT `/users/me/*`.
1678
+ */
1679
+ async getColdBudget(options) {
1680
+ return this.rawRequest({
1681
+ method: "GET",
1682
+ path: "/me/cold-budget",
1683
+ signal: options?.signal
1684
+ });
1685
+ }
1686
+ /**
1687
+ * Paginated listing of peers the caller has DMed, with cold/warm
1688
+ * state.
1689
+ *
1690
+ * Useful for rendering "this thread is still cold, you're awaiting
1691
+ * a reply" UX without pressing send and learning from a future 429
1692
+ * (once Phase 3 lands).
1693
+ *
1694
+ * `cursor` is opaque to the SDK — the server controls the format.
1695
+ * Page-size `limit` defaults to 50 and is capped server-side. The
1696
+ * cursor is stable: inserting a new peer mid-pagination does not
1697
+ * skip entries.
1698
+ */
1699
+ async listColdBudgetPeers(options = {}) {
1700
+ const params = new URLSearchParams({ limit: String(options.limit ?? 50) });
1701
+ if (options.cursor !== void 0) {
1702
+ params.set("cursor", options.cursor);
1703
+ }
1704
+ return this.rawRequest({
1705
+ method: "GET",
1706
+ path: `/me/cold-budget/peers?${params.toString()}`,
1707
+ signal: options.signal
1708
+ });
1709
+ }
1710
+ /**
1711
+ * Update the caller's inbox mode (and optional quiet karma
1712
+ * threshold).
1713
+ *
1714
+ * Inbox modes gate which cold senders the server admits at all:
1715
+ *
1716
+ * - `"open"` (default): accept cold DMs from any tier >= L1.
1717
+ * - `"contacts_only"`: accept only in warm threads or from peers
1718
+ * the caller has previously messaged first.
1719
+ * - `"quiet"`: accept cold DMs only from senders whose karma is
1720
+ * >= `inboxQuietMinKarma` (defaults to 10 server-side when
1721
+ * omitted at this layer; pass an int explicitly to set a tighter
1722
+ * threshold).
1723
+ *
1724
+ * Setting `inboxMode !== "quiet"` clears any previously-set karma
1725
+ * threshold back to `null` server-side, so callers do not need to
1726
+ * pass `inboxQuietMinKarma` when leaving quiet mode.
1727
+ */
1728
+ async setInboxMode(inboxMode, options = {}) {
1729
+ const body = { inbox_mode: inboxMode };
1730
+ if (options.inboxQuietMinKarma !== void 0) {
1731
+ body.inbox_quiet_min_karma = options.inboxQuietMinKarma;
1732
+ }
1733
+ return this.rawRequest({
1734
+ method: "PATCH",
1735
+ path: "/me/inbox",
1736
+ body,
1737
+ signal: options.signal
1738
+ });
1739
+ }
1573
1740
  // ── Following ────────────────────────────────────────────────────
1574
1741
  /** Follow a user. */
1575
1742
  async follow(userId, options) {