From 0f475e337f9527c93d95a8c7864540b81a46f48d Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 17 Feb 2026 07:11:32 +0000 Subject: [PATCH 1/2] feat: add @hypr/helpchat foundation package Co-Authored-By: yujonglee --- packages/helpchat/package.json | 15 ++++ packages/helpchat/src/client.ts | 130 ++++++++++++++++++++++++++++++++ packages/helpchat/src/events.ts | 69 +++++++++++++++++ packages/helpchat/src/index.ts | 22 ++++++ packages/helpchat/src/types.ts | 56 ++++++++++++++ packages/helpchat/tsconfig.json | 13 ++++ pnpm-lock.yaml | 6 ++ 7 files changed, 311 insertions(+) create mode 100644 packages/helpchat/package.json create mode 100644 packages/helpchat/src/client.ts create mode 100644 packages/helpchat/src/events.ts create mode 100644 packages/helpchat/src/index.ts create mode 100644 packages/helpchat/src/types.ts create mode 100644 packages/helpchat/tsconfig.json diff --git a/packages/helpchat/package.json b/packages/helpchat/package.json new file mode 100644 index 0000000000..a9fcd42734 --- /dev/null +++ b/packages/helpchat/package.json @@ -0,0 +1,15 @@ +{ + "name": "@hypr/helpchat", + "version": "0.0.0", + "private": true, + "type": "module", + "exports": { + ".": "./src/index.ts" + }, + "scripts": { + "typecheck": "tsc --noEmit" + }, + "devDependencies": { + "typescript": "~5.8.3" + } +} diff --git a/packages/helpchat/src/client.ts b/packages/helpchat/src/client.ts new file mode 100644 index 0000000000..59374ceccb --- /dev/null +++ b/packages/helpchat/src/client.ts @@ -0,0 +1,130 @@ +import type { + Contact, + Conversation, + CreateContactRequest, + CreateConversationRequest, + FetchFn, + HelpChatConfig, + Message, + SendMessageRequest, +} from "./types"; + +function resolveFetch(config: HelpChatConfig): FetchFn { + return config.fetchFn ?? globalThis.fetch; +} + +function authHeaders(config: HelpChatConfig): Record { + const headers: Record = { + "Content-Type": "application/json", + }; + if (config.accessToken) { + headers["Authorization"] = `Bearer ${config.accessToken}`; + } + return headers; +} + +// --------------------------------------------------------------------------- +// Contacts +// --------------------------------------------------------------------------- + +export async function createContact( + config: HelpChatConfig, + req: CreateContactRequest, +): Promise { + const fetchFn = resolveFetch(config); + const res = await fetchFn(`${config.baseUrl}/support/chatwoot/contact`, { + method: "POST", + headers: authHeaders(config), + body: JSON.stringify(req), + }); + if (!res.ok) { + throw new Error(`createContact failed: ${res.status}`); + } + return res.json() as Promise; +} + +// --------------------------------------------------------------------------- +// Conversations +// --------------------------------------------------------------------------- + +export async function createConversation( + config: HelpChatConfig, + req: CreateConversationRequest, +): Promise<{ conversationId: number }> { + const fetchFn = resolveFetch(config); + const res = await fetchFn( + `${config.baseUrl}/support/chatwoot/conversations`, + { + method: "POST", + headers: authHeaders(config), + body: JSON.stringify(req), + }, + ); + if (!res.ok) { + throw new Error(`createConversation failed: ${res.status}`); + } + return res.json() as Promise<{ conversationId: number }>; +} + +export async function listConversations( + config: HelpChatConfig, + sourceId: string, +): Promise { + const fetchFn = resolveFetch(config); + const params = new URLSearchParams({ source_id: sourceId }); + const res = await fetchFn( + `${config.baseUrl}/support/chatwoot/conversations?${params}`, + { + method: "GET", + headers: authHeaders(config), + }, + ); + if (!res.ok) { + throw new Error(`listConversations failed: ${res.status}`); + } + return res.json() as Promise; +} + +// --------------------------------------------------------------------------- +// Messages +// --------------------------------------------------------------------------- + +export async function sendMessage( + config: HelpChatConfig, + conversationId: number, + req: SendMessageRequest, +): Promise { + const fetchFn = resolveFetch(config); + const res = await fetchFn( + `${config.baseUrl}/support/chatwoot/conversations/${conversationId}/messages`, + { + method: "POST", + headers: authHeaders(config), + body: JSON.stringify(req), + }, + ); + if (!res.ok) { + throw new Error(`sendMessage failed: ${res.status}`); + } + return res.json() as Promise; +} + +export async function getMessages( + config: HelpChatConfig, + conversationId: number, + sourceId: string, +): Promise { + const fetchFn = resolveFetch(config); + const params = new URLSearchParams({ source_id: sourceId }); + const res = await fetchFn( + `${config.baseUrl}/support/chatwoot/conversations/${conversationId}/messages?${params}`, + { + method: "GET", + headers: authHeaders(config), + }, + ); + if (!res.ok) { + throw new Error(`getMessages failed: ${res.status}`); + } + return res.json() as Promise; +} diff --git a/packages/helpchat/src/events.ts b/packages/helpchat/src/events.ts new file mode 100644 index 0000000000..42a43c1c95 --- /dev/null +++ b/packages/helpchat/src/events.ts @@ -0,0 +1,69 @@ +import type { AgentMessage, FetchFn, HelpChatConfig } from "./types"; + +export interface EventStreamOptions { + config: HelpChatConfig; + conversationId: number; + pubsubToken: string; + onMessage: (message: AgentMessage) => void; + signal?: AbortSignal; +} + +/** + * Connect to the SSE event stream for real-time human agent messages. + * + * The backend proxies Chatwoot's ActionCable WebSocket into a plain SSE + * stream at `/support/chatwoot/conversations/{id}/events`. + * + * Resolves when the stream ends; rejects on connection error. + */ +export async function connectEventStream( + options: EventStreamOptions, +): Promise { + const { config, conversationId, pubsubToken, onMessage, signal } = options; + + const fetchFn: FetchFn = config.fetchFn ?? globalThis.fetch; + + const params = new URLSearchParams({ pubsub_token: pubsubToken }); + const url = + `${config.baseUrl}/support/chatwoot/conversations/${conversationId}/events?${params}`; + + const headers: Record = { Accept: "text/event-stream" }; + if (config.accessToken) { + headers["Authorization"] = `Bearer ${config.accessToken}`; + } + + const res = await fetchFn(url, { method: "GET", headers, signal }); + + if (!res.ok || !res.body) { + throw new Error(`event stream failed: ${res.status}`); + } + + const reader = res.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ""; + + for (;;) { + const { done, value } = await reader.read(); + if (done) break; + + buffer += decoder.decode(value, { stream: true }); + const parts = buffer.split("\n\n"); + buffer = parts.pop() ?? ""; + + for (const part of parts) { + const dataLine = part + .split("\n") + .find((line) => line.startsWith("data: ")); + if (!dataLine) continue; + + try { + const payload = JSON.parse(dataLine.slice(6)) as AgentMessage; + if (payload.content) { + onMessage(payload); + } + } catch { + // skip malformed SSE frames + } + } + } +} diff --git a/packages/helpchat/src/index.ts b/packages/helpchat/src/index.ts new file mode 100644 index 0000000000..28416520b8 --- /dev/null +++ b/packages/helpchat/src/index.ts @@ -0,0 +1,22 @@ +export type { + AgentMessage, + Contact, + Conversation, + CreateContactRequest, + CreateConversationRequest, + FetchFn, + HelpChatConfig, + Message, + SendMessageRequest, +} from "./types"; + +export { + createContact, + createConversation, + getMessages, + listConversations, + sendMessage, +} from "./client"; + +export { connectEventStream } from "./events"; +export type { EventStreamOptions } from "./events"; diff --git a/packages/helpchat/src/types.ts b/packages/helpchat/src/types.ts new file mode 100644 index 0000000000..39add8665a --- /dev/null +++ b/packages/helpchat/src/types.ts @@ -0,0 +1,56 @@ +/** Custom fetch function signature for platform-agnostic HTTP. */ +export type FetchFn = typeof globalThis.fetch; + +/** Configuration for connecting to the helpchat backend. */ +export interface HelpChatConfig { + /** Base URL of the support API (e.g. "https://api.example.com"). */ + baseUrl: string; + /** Optional auth token included as Bearer header. */ + accessToken?: string | null; + /** Optional custom fetch implementation (e.g. Tauri HTTP plugin). */ + fetchFn?: FetchFn; +} + +// --------------------------------------------------------------------------- +// Request / Response shapes (mirror crates/api-support/src/routes/chatwoot/) +// --------------------------------------------------------------------------- + +export interface CreateContactRequest { + identifier: string; + name?: string; + email?: string; + customAttributes?: Record; +} + +export interface Contact { + sourceId: string; + pubsubToken: string; +} + +export interface CreateConversationRequest { + sourceId: string; + customAttributes?: Record; +} + +export interface Conversation { + id: number; + inboxId: string | null; +} + +export interface SendMessageRequest { + content: string; + messageType?: "incoming" | "outgoing"; + sourceId?: string; +} + +export interface Message { + id: string; + content: string | null; + messageType: string | null; + createdAt: string | null; +} + +export interface AgentMessage { + content: string; + senderName: string; +} diff --git a/packages/helpchat/tsconfig.json b/packages/helpchat/tsconfig.json new file mode 100644 index 0000000000..ba70bdb506 --- /dev/null +++ b/packages/helpchat/tsconfig.json @@ -0,0 +1,13 @@ +{ + "include": ["src"], + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2023", "DOM"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "isolatedModules": true, + "noEmit": true, + "strict": true + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b4df77a6e4..550753e1e7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1250,6 +1250,12 @@ importers: specifier: ^5.9.3 version: 5.9.3 + packages/helpchat: + devDependencies: + typescript: + specifier: ~5.8.3 + version: 5.8.3 + packages/store: dependencies: tinybase: From 8640b136b3fccfafcf9dcb612b96b7ec9203947b Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 17 Feb 2026 07:19:24 +0000 Subject: [PATCH 2/2] fix: dprint formatting in events.ts Co-Authored-By: yujonglee --- packages/helpchat/src/events.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/helpchat/src/events.ts b/packages/helpchat/src/events.ts index 42a43c1c95..6feedb8a2b 100644 --- a/packages/helpchat/src/events.ts +++ b/packages/helpchat/src/events.ts @@ -24,8 +24,7 @@ export async function connectEventStream( const fetchFn: FetchFn = config.fetchFn ?? globalThis.fetch; const params = new URLSearchParams({ pubsub_token: pubsubToken }); - const url = - `${config.baseUrl}/support/chatwoot/conversations/${conversationId}/events?${params}`; + const url = `${config.baseUrl}/support/chatwoot/conversations/${conversationId}/events?${params}`; const headers: Record = { Accept: "text/event-stream" }; if (config.accessToken) {