@cosmoops/connect-sdk 0.28.0 → 0.29.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/CHANGELOG.md CHANGED
@@ -3,6 +3,50 @@
3
3
  `@cosmoops/connect-sdk` follows semver, with the 0.x convention that **minor
4
4
  versions may carry breaking changes** and patch versions never do.
5
5
 
6
+ ## 0.29.0
7
+
8
+ Voice was readable and writing was not. `calls.list` has always let a partner
9
+ read back what a customer said out loud; the WhatsApp threads a workspace's
10
+ agents hold had no partner surface at all, so a CRM could show every call on a
11
+ contact and nothing they had typed.
12
+
13
+ ### Added
14
+
15
+ - `connect.conversations.list({ workspaceId, contactAddress?, status? })` and
16
+ `connect.conversations.get(id, { workspaceId })`, plus the four types they
17
+ answer with: `ConnectConversation`, `ConnectConversationMessage`,
18
+ `ConversationsPage` and `ConversationDetail`.
19
+
20
+ **These are the *workspace's* threads, not your application's**, which is the
21
+ difference from `calls` and it decides how they are gated. A call arrived on
22
+ a channel you hold, so `calls.list` is scoped to your application by the key
23
+ alone. A conversation lives in the workspace's own subtree and belongs to it
24
+ the way its contact book does — so reading one needs the `conversations:read`
25
+ scope **and** a live subscription in that workspace, and, when the call comes
26
+ through the browser bridge, the signed-in member's own `agent:read`.
27
+
28
+ Three things are worth reading before you render one:
29
+
30
+ `contactAddress` is the filter to reach for beside your own customer record.
31
+ You hold a phone number, and you cannot derive a conversation id from one —
32
+ the id is built from a hashed key, deliberately, so an id you could guess is
33
+ an id you could probe. Send the number however your records spell it.
34
+
35
+ `canReplyFreely` is computed against Connect's clock, and re-deriving it from
36
+ `lastInboundAt` against yours is the mistake to avoid: WhatsApp allows a
37
+ free-form reply only inside 24 hours of the customer's last message, and a
38
+ skew between your server and Connect's shows up as a thread your UI calls
39
+ answerable when it is not.
40
+
41
+ Messages that sent nothing are included and carry `failure`. Render them — a
42
+ transcript that silently drops them reads as a customer being ignored, which
43
+ is the opposite of what happened. `refused` is not a failure at all: it is
44
+ the agent deciding not to answer, which is what it is for.
45
+
46
+ Read-only, and deliberately. Sending on a thread would mean saying whether it
47
+ is a free-form reply or an approved template, and what that means for the
48
+ agent still answering it.
49
+
6
50
  ## 0.28.0
7
51
 
8
52
  Connect's webhook bus carried four events, all of them about billing and
@@ -11,6 +11,7 @@ const scheduling_js_1 = require("./resources/scheduling.js");
11
11
  const contacts_js_1 = require("./resources/contacts.js");
12
12
  const channels_js_1 = require("./resources/channels.js");
13
13
  const calls_js_1 = require("./resources/calls.js");
14
+ const conversations_js_1 = require("./resources/conversations.js");
14
15
  const kv_js_1 = require("./resources/kv.js");
15
16
  const billing_js_1 = require("./resources/billing.js");
16
17
  const drive_js_1 = require("./resources/drive.js");
@@ -92,6 +93,8 @@ class ConnectClient {
92
93
  channels;
93
94
  /** The calls that arrived on those channels, with their transcripts. */
94
95
  calls;
96
+ /** The message threads a subscribing workspace's agents are holding. */
97
+ conversations;
95
98
  /** RAG chat and structured extraction. */
96
99
  ai;
97
100
  enrichment;
@@ -145,6 +148,7 @@ class ConnectClient {
145
148
  this.contacts = new contacts_js_1.ContactsResource(transport);
146
149
  this.channels = new channels_js_1.ChannelsResource(transport);
147
150
  this.calls = new calls_js_1.CallsResource(transport);
151
+ this.conversations = new conversations_js_1.ConversationsResource(transport);
148
152
  this.ai = new ai_js_1.AiResource(transport);
149
153
  this.enrichment = new enrichment_js_1.EnrichmentResource(transport);
150
154
  this.data = new data_js_1.DataResource(transport);
@@ -161,4 +165,4 @@ class ConnectClient {
161
165
  }
162
166
  exports.ConnectClient = ConnectClient;
163
167
  /** Kept in step with `package.json` by `sdk.test.ts`. */
164
- exports.SDK_VERSION = "0.28.0";
168
+ exports.SDK_VERSION = "0.29.0";
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ConversationsResource = void 0;
4
+ const http_js_1 = require("../http.js");
5
+ const acting_as_js_1 = require("./acting-as.js");
6
+ /**
7
+ * What a workspace's agents are saying in writing.
8
+ *
9
+ * **The sibling of `calls`, with one difference that decides how it is
10
+ * gated: these are the *workspace's* threads, not your application's.** A call
11
+ * arrived on a channel you hold, so `calls.list` is scoped to your
12
+ * application. A conversation lives in the workspace's own subtree and belongs
13
+ * to it the way its contact book does - so reading one needs a live
14
+ * subscription in that workspace, and, when the call comes from a browser
15
+ * through the bridge, the signed-in member's own `agent:read`.
16
+ *
17
+ * Read-only, and deliberately. Sending on a thread is not a missing verb:
18
+ * inside WhatsApp's 24-hour window a reply is the agent's to make, outside it
19
+ * only an approved template may be sent, and posting into a thread would mean
20
+ * saying which of those it was and what it means for the agent still
21
+ * answering.
22
+ */
23
+ class ConversationsResource {
24
+ transport;
25
+ constructor(transport) {
26
+ this.transport = transport;
27
+ }
28
+ /**
29
+ * The workspace's threads, newest activity first.
30
+ *
31
+ * **Page until `hasMore` is `false`; do not stop on an empty `items`.** An
32
+ * inbox grows without bound, so there is no total count to page against.
33
+ */
34
+ list(input) {
35
+ return (0, http_js_1.request)(this.transport, {
36
+ method: "GET",
37
+ path: "/conversations",
38
+ query: {
39
+ workspaceId: input.workspaceId,
40
+ contactAddress: input.contactAddress,
41
+ status: input.status,
42
+ cursor: input.cursor,
43
+ limit: input.limit === undefined ? undefined : String(input.limit),
44
+ },
45
+ headers: (0, acting_as_js_1.actingAs)(input.asUser),
46
+ });
47
+ }
48
+ /**
49
+ * One thread and its recent messages, oldest first.
50
+ *
51
+ * **Capped, not paged** - you get the same window the agent itself sees when
52
+ * it composes a reply, so you are never shown less context than it used.
53
+ *
54
+ * Turns that produced no message are included and carry `failure`. Render
55
+ * them: a transcript that silently drops them reads as a customer being
56
+ * ignored, which is the opposite of what happened.
57
+ *
58
+ * Throws `ConnectError` with `code: "not_found"` for a thread this workspace
59
+ * does not hold.
60
+ */
61
+ get(conversationId, options) {
62
+ return (0, http_js_1.request)(this.transport, {
63
+ method: "GET",
64
+ path: `/conversations/${encodeURIComponent(conversationId)}`,
65
+ query: { workspaceId: options.workspaceId },
66
+ headers: (0, acting_as_js_1.actingAs)(options.asUser),
67
+ });
68
+ }
69
+ }
70
+ exports.ConversationsResource = ConversationsResource;
@@ -8,6 +8,7 @@ import { SchedulingResource } from "./resources/scheduling.js";
8
8
  import { ContactsResource } from "./resources/contacts.js";
9
9
  import { ChannelsResource } from "./resources/channels.js";
10
10
  import { CallsResource } from "./resources/calls.js";
11
+ import { ConversationsResource } from "./resources/conversations.js";
11
12
  import { KvResource } from "./resources/kv.js";
12
13
  import { BillingResource } from "./resources/billing.js";
13
14
  import { DriveResource } from "./resources/drive.js";
@@ -89,6 +90,8 @@ export class ConnectClient {
89
90
  channels;
90
91
  /** The calls that arrived on those channels, with their transcripts. */
91
92
  calls;
93
+ /** The message threads a subscribing workspace's agents are holding. */
94
+ conversations;
92
95
  /** RAG chat and structured extraction. */
93
96
  ai;
94
97
  enrichment;
@@ -142,6 +145,7 @@ export class ConnectClient {
142
145
  this.contacts = new ContactsResource(transport);
143
146
  this.channels = new ChannelsResource(transport);
144
147
  this.calls = new CallsResource(transport);
148
+ this.conversations = new ConversationsResource(transport);
145
149
  this.ai = new AiResource(transport);
146
150
  this.enrichment = new EnrichmentResource(transport);
147
151
  this.data = new DataResource(transport);
@@ -157,4 +161,4 @@ export class ConnectClient {
157
161
  }
158
162
  }
159
163
  /** Kept in step with `package.json` by `sdk.test.ts`. */
160
- export const SDK_VERSION = "0.28.0";
164
+ export const SDK_VERSION = "0.29.0";
@@ -0,0 +1,66 @@
1
+ import { request } from "../http.js";
2
+ import { actingAs } from "./acting-as.js";
3
+ /**
4
+ * What a workspace's agents are saying in writing.
5
+ *
6
+ * **The sibling of `calls`, with one difference that decides how it is
7
+ * gated: these are the *workspace's* threads, not your application's.** A call
8
+ * arrived on a channel you hold, so `calls.list` is scoped to your
9
+ * application. A conversation lives in the workspace's own subtree and belongs
10
+ * to it the way its contact book does - so reading one needs a live
11
+ * subscription in that workspace, and, when the call comes from a browser
12
+ * through the bridge, the signed-in member's own `agent:read`.
13
+ *
14
+ * Read-only, and deliberately. Sending on a thread is not a missing verb:
15
+ * inside WhatsApp's 24-hour window a reply is the agent's to make, outside it
16
+ * only an approved template may be sent, and posting into a thread would mean
17
+ * saying which of those it was and what it means for the agent still
18
+ * answering.
19
+ */
20
+ export class ConversationsResource {
21
+ transport;
22
+ constructor(transport) {
23
+ this.transport = transport;
24
+ }
25
+ /**
26
+ * The workspace's threads, newest activity first.
27
+ *
28
+ * **Page until `hasMore` is `false`; do not stop on an empty `items`.** An
29
+ * inbox grows without bound, so there is no total count to page against.
30
+ */
31
+ list(input) {
32
+ return request(this.transport, {
33
+ method: "GET",
34
+ path: "/conversations",
35
+ query: {
36
+ workspaceId: input.workspaceId,
37
+ contactAddress: input.contactAddress,
38
+ status: input.status,
39
+ cursor: input.cursor,
40
+ limit: input.limit === undefined ? undefined : String(input.limit),
41
+ },
42
+ headers: actingAs(input.asUser),
43
+ });
44
+ }
45
+ /**
46
+ * One thread and its recent messages, oldest first.
47
+ *
48
+ * **Capped, not paged** - you get the same window the agent itself sees when
49
+ * it composes a reply, so you are never shown less context than it used.
50
+ *
51
+ * Turns that produced no message are included and carry `failure`. Render
52
+ * them: a transcript that silently drops them reads as a customer being
53
+ * ignored, which is the opposite of what happened.
54
+ *
55
+ * Throws `ConnectError` with `code: "not_found"` for a thread this workspace
56
+ * does not hold.
57
+ */
58
+ get(conversationId, options) {
59
+ return request(this.transport, {
60
+ method: "GET",
61
+ path: `/conversations/${encodeURIComponent(conversationId)}`,
62
+ query: { workspaceId: options.workspaceId },
63
+ headers: actingAs(options.asUser),
64
+ });
65
+ }
66
+ }
@@ -9,6 +9,7 @@ import { SchedulingResource } from "./resources/scheduling.js";
9
9
  import { ContactsResource } from "./resources/contacts.js";
10
10
  import { ChannelsResource } from "./resources/channels.js";
11
11
  import { CallsResource } from "./resources/calls.js";
12
+ import { ConversationsResource } from "./resources/conversations.js";
12
13
  import { KvResource } from "./resources/kv.js";
13
14
  import { BillingResource } from "./resources/billing.js";
14
15
  import { DriveResource } from "./resources/drive.js";
@@ -88,6 +89,8 @@ export declare class ConnectClient {
88
89
  readonly channels: ChannelsResource;
89
90
  /** The calls that arrived on those channels, with their transcripts. */
90
91
  readonly calls: CallsResource;
92
+ /** The message threads a subscribing workspace's agents are holding. */
93
+ readonly conversations: ConversationsResource;
91
94
  /** RAG chat and structured extraction. */
92
95
  readonly ai: AiResource;
93
96
  readonly enrichment: EnrichmentResource;
@@ -123,4 +126,4 @@ export declare class ConnectClient {
123
126
  constructor(options: ConnectClientOptions);
124
127
  }
125
128
  /** Kept in step with `package.json` by `sdk.test.ts`. */
126
- export declare const SDK_VERSION = "0.28.0";
129
+ export declare const SDK_VERSION = "0.29.0";
@@ -0,0 +1,68 @@
1
+ import { type TransportOptions } from "../http.js";
2
+ import type { ConversationDetail, ConversationsPage } from "../../types.js";
3
+ export interface ListConversationsInput {
4
+ workspaceId: string;
5
+ /**
6
+ * Narrow to one party's threads.
7
+ *
8
+ * Any reasonable spelling - `+919000000001`, `919000000001`,
9
+ * `+91 90000 00001` all resolve to the same threads, because the number is
10
+ * normalized server-side before the match. This is the filter to reach for
11
+ * beside your own customer record: you hold a phone number, and you cannot
12
+ * derive a conversation id from one because the id is built from a *hashed*
13
+ * key.
14
+ */
15
+ contactAddress?: string;
16
+ status?: "open" | "snoozed" | "closed";
17
+ /** From a previous page's `nextCursor`. */
18
+ cursor?: string;
19
+ /** 1-50, default 20. */
20
+ limit?: number;
21
+ /** See `ActingAsOption` on the scheduling resource - same contract. */
22
+ asUser?: string;
23
+ }
24
+ /**
25
+ * What a workspace's agents are saying in writing.
26
+ *
27
+ * **The sibling of `calls`, with one difference that decides how it is
28
+ * gated: these are the *workspace's* threads, not your application's.** A call
29
+ * arrived on a channel you hold, so `calls.list` is scoped to your
30
+ * application. A conversation lives in the workspace's own subtree and belongs
31
+ * to it the way its contact book does - so reading one needs a live
32
+ * subscription in that workspace, and, when the call comes from a browser
33
+ * through the bridge, the signed-in member's own `agent:read`.
34
+ *
35
+ * Read-only, and deliberately. Sending on a thread is not a missing verb:
36
+ * inside WhatsApp's 24-hour window a reply is the agent's to make, outside it
37
+ * only an approved template may be sent, and posting into a thread would mean
38
+ * saying which of those it was and what it means for the agent still
39
+ * answering.
40
+ */
41
+ export declare class ConversationsResource {
42
+ private readonly transport;
43
+ constructor(transport: TransportOptions);
44
+ /**
45
+ * The workspace's threads, newest activity first.
46
+ *
47
+ * **Page until `hasMore` is `false`; do not stop on an empty `items`.** An
48
+ * inbox grows without bound, so there is no total count to page against.
49
+ */
50
+ list(input: ListConversationsInput): Promise<ConversationsPage>;
51
+ /**
52
+ * One thread and its recent messages, oldest first.
53
+ *
54
+ * **Capped, not paged** - you get the same window the agent itself sees when
55
+ * it composes a reply, so you are never shown less context than it used.
56
+ *
57
+ * Turns that produced no message are included and carry `failure`. Render
58
+ * them: a transcript that silently drops them reads as a customer being
59
+ * ignored, which is the opposite of what happened.
60
+ *
61
+ * Throws `ConnectError` with `code: "not_found"` for a thread this workspace
62
+ * does not hold.
63
+ */
64
+ get(conversationId: string, options: {
65
+ workspaceId: string;
66
+ asUser?: string;
67
+ }): Promise<ConversationDetail>;
68
+ }
@@ -1822,3 +1822,85 @@ export interface CallsPage {
1822
1822
  nextCursor: string | null;
1823
1823
  hasMore: boolean;
1824
1824
  }
1825
+ /**
1826
+ * One message thread a workspace's agents are holding.
1827
+ *
1828
+ * Hand-written rather than imported from Connect, like every other type in
1829
+ * this file - `conformance.test.ts` type-checks it against the server DTO, so
1830
+ * drift fails the build here rather than in your integration.
1831
+ *
1832
+ * Dates are ISO-8601 strings. JSON has no date type, and a UTC ISO string
1833
+ * sorts lexicographically in chronological order, so these compare and
1834
+ * range-filter without being parsed first.
1835
+ */
1836
+ export interface ConnectConversation {
1837
+ id: string;
1838
+ /** The channel the thread arrived on. */
1839
+ channelId: string;
1840
+ /** Today always `whatsapp` - a voice channel produces calls, not threads. */
1841
+ kind: "whatsapp";
1842
+ /** The other party, normalized to E.164. */
1843
+ contactAddress: string;
1844
+ /**
1845
+ * The Connect contact this thread is about, once one has been matched.
1846
+ *
1847
+ * The join to your own records - store this on yours rather than matching
1848
+ * phone numbers. See `ContactsResource`.
1849
+ */
1850
+ contactId: string | null;
1851
+ /** As the provider reported it, when it did. */
1852
+ contactName: string | null;
1853
+ status: "open" | "snoozed" | "closed";
1854
+ /** `null` means nothing is answering: no routing, or a human took over. */
1855
+ agentId: string | null;
1856
+ lastInboundAt: string;
1857
+ lastOutboundAt: string | null;
1858
+ lastMessagePreview: string;
1859
+ /**
1860
+ * Whether a free-form reply is still allowed, computed against **Connect's**
1861
+ * clock.
1862
+ *
1863
+ * WhatsApp allows one only inside 24 hours of the customer's last message;
1864
+ * outside it, only an approved template may be sent. Do not re-derive this
1865
+ * from `lastInboundAt` against your own clock - a skew shows up as a thread
1866
+ * your UI calls answerable when it is not.
1867
+ */
1868
+ canReplyFreely: boolean;
1869
+ createdAt: string;
1870
+ updatedAt: string;
1871
+ }
1872
+ /**
1873
+ * Why a turn produced no message.
1874
+ *
1875
+ * `refused` is not a failure at all - it is the agent deciding not to answer,
1876
+ * which is what it is for.
1877
+ */
1878
+ export type ConnectMessageFailure = "window_closed" | "no_agent" | "model_error" | "send_failed" | "refused";
1879
+ /** One message on a thread. A turn that sent nothing still appears, carrying `failure`. */
1880
+ export interface ConnectConversationMessage {
1881
+ id: string;
1882
+ direction: "inbound" | "outbound";
1883
+ /**
1884
+ * Who produced it.
1885
+ *
1886
+ * `agent` and `human` are both outbound and deliberately distinguished - a
1887
+ * thread that cannot show which replies were automatic is one nobody trusts
1888
+ * enough to leave running. `system` is neither party: a turn Connect itself
1889
+ * recorded, which is what a `failure` row is.
1890
+ */
1891
+ author: "contact" | "agent" | "human" | "system";
1892
+ text: string;
1893
+ failure: ConnectMessageFailure | null;
1894
+ createdAt: string;
1895
+ }
1896
+ /** A thread and its recent messages, oldest first. Capped rather than paged. */
1897
+ export interface ConversationDetail {
1898
+ conversation: ConnectConversation;
1899
+ messages: ConnectConversationMessage[];
1900
+ }
1901
+ /** One page of threads, newest activity first. Page until `hasMore` is `false`. */
1902
+ export interface ConversationsPage {
1903
+ items: ConnectConversation[];
1904
+ nextCursor: string | null;
1905
+ hasMore: boolean;
1906
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmoops/connect-sdk",
3
- "version": "0.28.0",
3
+ "version": "0.29.0",
4
4
  "description": "Official server-side SDK for the CosmoOps Connect platform API - Login with Connect, entitlements, billing and metering.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "commonjs",