Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

@syncagent/js

Package Overview
Dependencies
Maintainers
1
Versions
27
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@syncagent/js - npm Package Compare versions

Comparing version
0.3.4
to
0.4.0
+250
-1
dist/index.d.mts

@@ -12,2 +12,89 @@ interface ToolParameter {

}
interface CustomerChatResult {
conversationId: string;
response: string;
escalated: boolean;
autoReply: boolean;
flowActive: boolean;
resolved: boolean;
welcomeMessage?: string;
sources?: any[];
flowSession?: {
flowId: string;
currentNodeId: string;
};
}
/** Guest identity data collected from the identification form */
interface GuestIdentity {
name: string;
email: string;
phone: string | null;
/** The generated guest_XXXX identifier */
guestId: string;
}
/** Configuration for the guest identification form */
interface GuestFormConfig {
/** Custom title text (default: "Welcome") */
title?: string;
/** Custom subtitle text (default: "Please introduce yourself to get started") */
subtitle?: string;
/** Custom submit button text (default: "Start Chat") */
submitButtonText?: string;
/** Placeholder for name field */
namePlaceholder?: string;
/** Placeholder for email field */
emailPlaceholder?: string;
/** Placeholder for phone field */
phonePlaceholder?: string;
/** Custom CSS class applied to the form container */
className?: string;
/** Called after successful form submission with the guest identity */
onSubmit?: (identity: GuestIdentity) => void;
}
/** Validation result for a single field */
interface FieldValidationResult {
valid: boolean;
error?: string;
}
interface CustomerChatOptions {
/** Existing conversation ID to continue */
conversationId?: string;
/** Additional metadata to store on the conversation */
metadata?: Record<string, any>;
/** Called when the conversation is escalated to a human agent */
onEscalated?: () => void;
/** Called when the conversation is resolved */
onResolved?: (conversationId: string) => void;
/** Called when a guest completes identification (JS-only usage) */
onGuestIdentified?: (identity: GuestIdentity) => void;
}
/**
* Shared config for `SyncAgentClient.createDual()`.
* Omits `customerMode` since both modes are created automatically.
*/
interface DualClientConfig {
apiKey: string;
connectionString: string;
/** Customer identifier — scopes customer agent conversations to this user. Required. */
externalUserId: string;
baseUrl?: string;
filter?: Record<string, any>;
operations?: ("read" | "create" | "update" | "delete")[];
tools?: Record<string, ToolDefinition>;
toolsOnly?: boolean;
autoDetectPage?: boolean;
systemInstruction?: string;
confirmWrites?: boolean;
language?: string;
maxResults?: number;
sensitiveFields?: string[];
onBeforeToolCall?: (toolName: string, args: Record<string, any>) => boolean | Promise<boolean>;
onAfterToolCall?: (toolName: string, args: Record<string, any>, result: any) => void | Promise<void>;
}
interface DualClient {
/** Database agent — for internal/admin use. Calls `chat()` for direct DB access. */
db: SyncAgentClient;
/** Customer agent — for end-user support. Calls `customerChat()` for the support pipeline. */
support: SyncAgentClient;
}
interface SyncAgentConfig {

@@ -17,2 +104,6 @@ apiKey: string;

connectionString?: string;
/** Enable customer agent mode. Routes messages through the customer support pipeline instead of direct DB agent. */
customerMode?: boolean;
/** Customer identifier for multi-tenant scoping. Required when customerMode is true. */
externalUserId?: string;
/** Custom tools the AI agent can call — executed client-side in your app */

@@ -134,2 +225,7 @@ tools?: Record<string, ToolDefinition>;

onAfterToolCall?: (toolName: string, args: Record<string, any>, result: any) => void | Promise<void>;
/**
* Configuration for the guest identification form.
* Only used when customerMode is true and no externalUserId is provided.
*/
guestForm?: GuestFormConfig;
}

@@ -183,2 +279,41 @@ interface Message {

}
/** Return type for the React useDualChat() hook and framework equivalents */
interface DualChatReturn {
db: {
messages: Message[];
isLoading: boolean;
error: Error | null;
status: {
step: string;
label: string;
} | null;
lastData: ToolData | null;
sendMessage: (content: string) => Promise<void>;
stop: () => void;
reset: () => void;
};
support: {
messages: Message[];
conversationId: string | null;
isLoading: boolean;
isEscalated: boolean;
isResolved: boolean;
error: Error | null;
welcomeMessage: string | null;
sendMessage: (content: string, metadata?: Record<string, any>) => Promise<void>;
rateConversation: (rating: number) => Promise<void>;
reset: () => void;
};
}
/** Options for useDualChat() hook/composable */
interface DualChatOptions {
context?: Record<string, any>;
onData?: (data: ToolData) => void;
onEscalated?: () => void;
onResolved?: (conversationId: string) => void;
}
/** Config type alias for unified client (SyncAgentConfig with externalUserId required) */
type UnifiedConfig = SyncAgentConfig & {
externalUserId: string;
};

@@ -201,2 +336,6 @@ declare class SyncAgentClient {

private onAfterToolCall;
private customerMode;
private externalUserId;
private guestStorage;
private guestFormConfig;
constructor(config: SyncAgentConfig);

@@ -213,2 +352,50 @@ private validateConnectionString;

getSchema(): Promise<CollectionSchema[]>;
/**
* Send a message in customer agent mode.
* Routes through the customer support pipeline (persona → flow → KB → escalation → AI).
*
* When no externalUserId is configured (guest mode):
* - Checks GuestStorageManager for a previously stored identity
* - If found, uses the stored guestId as externalUserId
* - If not found, throws GuestIdentificationRequiredError (caught by framework layers to show form)
*/
customerChat(message: string, options?: CustomerChatOptions): Promise<CustomerChatResult>;
/**
* Submit a satisfaction rating (1-5) for a resolved conversation.
*/
rateConversation(conversationId: string, rating: number): Promise<void>;
/**
* Set guest identity after form completion.
* Persists the identity to storage and sets the externalUserId for subsequent API calls.
* Called by framework layers (React hook, etc.) after the guest identification form is submitted.
*/
setGuestIdentity(identity: GuestIdentity): void;
/**
* Get the current guest identity from storage.
* Returns null if no guest identity has been stored.
*/
getGuestIdentity(): GuestIdentity | null;
/**
* Get the guest form configuration provided at client construction.
* Returns undefined if no guestForm config was provided.
*/
getGuestFormConfig(): GuestFormConfig | undefined;
/**
* Create both a database agent and a customer agent from a single config.
* Convenience factory for apps that serve both internal users and end-customers.
*
* @example
* const { db, support } = SyncAgentClient.createDual({
* apiKey: "sa_your_key",
* connectionString: "postgresql://...",
* externalUserId: currentUser.id,
* });
*
* // Admin: direct database queries
* const result = await db.chat([{ role: "user", content: "Show all orders" }]);
*
* // Customer: support pipeline
* const reply = await support.customerChat("I need help with my order");
*/
static createDual(config: DualClientConfig): DualClient;
}

@@ -229,2 +416,64 @@

export { type ChatOptions, type ChatResult, type CollectionSchema, type Message, type SchemaField, SyncAgentClient, type SyncAgentConfig, type ToolData, type ToolDefinition, type ToolParameter, detectPageContext };
/**
* Thrown when customerChat() is called without guest identification in guest mode.
* Framework layers catch this error to display the guest identification form.
*/
declare class GuestIdentificationRequiredError extends Error {
constructor();
}
/**
* Generate a deterministic guest identifier from an email address.
*
* Applies FNV-1a 32-bit hash to the lowercase-trimmed email and returns
* a string in the format: "guest_" + hex hash.
*
* This is a pure function with no side effects.
*
* @param email - The guest's email address
* @returns A deterministic identifier string prefixed with "guest_"
*/
declare function generateGuestIdentifier(email: string): string;
/**
* Manages persistence of guest identity data.
* Uses localStorage when available, falls back to in-memory storage
* for private browsing or quota-exceeded scenarios.
*/
declare class GuestStorageManager {
private memoryFallback;
private storageAvailable;
constructor();
/** Check if localStorage is available and writable */
private checkStorageAvailability;
/** Retrieve stored guest identity, returns null if not found or corrupted */
get(): GuestIdentity | null;
/** Store guest identity, falls back to memory if localStorage unavailable */
set(identity: GuestIdentity): void;
/** Clear stored identity from both localStorage and memory fallback */
clear(): void;
}
/**
* Validate name: must contain at least one non-whitespace character.
*/
declare function validateName(name: string): FieldValidationResult;
/**
* Validate email: must match local@domain.tld pattern.
* Requires non-empty local part, @ symbol, non-empty domain, dot, and non-empty TLD.
*/
declare function validateEmail(email: string): FieldValidationResult;
/**
* Validate the complete guest form submission.
* Returns an object with `valid` boolean and `errors` record mapping field names to error messages.
*/
declare function validateGuestForm(data: {
name: string;
email: string;
phone?: string;
}): {
valid: boolean;
errors: Record<string, string>;
};
export { type ChatOptions, type ChatResult, type CollectionSchema, type CustomerChatOptions, type CustomerChatResult, type DualChatOptions, type DualChatReturn, type DualClient, type DualClientConfig, type FieldValidationResult, type GuestFormConfig, GuestIdentificationRequiredError, type GuestIdentity, GuestStorageManager, type Message, type SchemaField, SyncAgentClient, type SyncAgentConfig, type ToolData, type ToolDefinition, type ToolParameter, type UnifiedConfig, detectPageContext, generateGuestIdentifier, validateEmail, validateGuestForm, validateName };

@@ -12,2 +12,89 @@ interface ToolParameter {

}
interface CustomerChatResult {
conversationId: string;
response: string;
escalated: boolean;
autoReply: boolean;
flowActive: boolean;
resolved: boolean;
welcomeMessage?: string;
sources?: any[];
flowSession?: {
flowId: string;
currentNodeId: string;
};
}
/** Guest identity data collected from the identification form */
interface GuestIdentity {
name: string;
email: string;
phone: string | null;
/** The generated guest_XXXX identifier */
guestId: string;
}
/** Configuration for the guest identification form */
interface GuestFormConfig {
/** Custom title text (default: "Welcome") */
title?: string;
/** Custom subtitle text (default: "Please introduce yourself to get started") */
subtitle?: string;
/** Custom submit button text (default: "Start Chat") */
submitButtonText?: string;
/** Placeholder for name field */
namePlaceholder?: string;
/** Placeholder for email field */
emailPlaceholder?: string;
/** Placeholder for phone field */
phonePlaceholder?: string;
/** Custom CSS class applied to the form container */
className?: string;
/** Called after successful form submission with the guest identity */
onSubmit?: (identity: GuestIdentity) => void;
}
/** Validation result for a single field */
interface FieldValidationResult {
valid: boolean;
error?: string;
}
interface CustomerChatOptions {
/** Existing conversation ID to continue */
conversationId?: string;
/** Additional metadata to store on the conversation */
metadata?: Record<string, any>;
/** Called when the conversation is escalated to a human agent */
onEscalated?: () => void;
/** Called when the conversation is resolved */
onResolved?: (conversationId: string) => void;
/** Called when a guest completes identification (JS-only usage) */
onGuestIdentified?: (identity: GuestIdentity) => void;
}
/**
* Shared config for `SyncAgentClient.createDual()`.
* Omits `customerMode` since both modes are created automatically.
*/
interface DualClientConfig {
apiKey: string;
connectionString: string;
/** Customer identifier — scopes customer agent conversations to this user. Required. */
externalUserId: string;
baseUrl?: string;
filter?: Record<string, any>;
operations?: ("read" | "create" | "update" | "delete")[];
tools?: Record<string, ToolDefinition>;
toolsOnly?: boolean;
autoDetectPage?: boolean;
systemInstruction?: string;
confirmWrites?: boolean;
language?: string;
maxResults?: number;
sensitiveFields?: string[];
onBeforeToolCall?: (toolName: string, args: Record<string, any>) => boolean | Promise<boolean>;
onAfterToolCall?: (toolName: string, args: Record<string, any>, result: any) => void | Promise<void>;
}
interface DualClient {
/** Database agent — for internal/admin use. Calls `chat()` for direct DB access. */
db: SyncAgentClient;
/** Customer agent — for end-user support. Calls `customerChat()` for the support pipeline. */
support: SyncAgentClient;
}
interface SyncAgentConfig {

@@ -17,2 +104,6 @@ apiKey: string;

connectionString?: string;
/** Enable customer agent mode. Routes messages through the customer support pipeline instead of direct DB agent. */
customerMode?: boolean;
/** Customer identifier for multi-tenant scoping. Required when customerMode is true. */
externalUserId?: string;
/** Custom tools the AI agent can call — executed client-side in your app */

@@ -134,2 +225,7 @@ tools?: Record<string, ToolDefinition>;

onAfterToolCall?: (toolName: string, args: Record<string, any>, result: any) => void | Promise<void>;
/**
* Configuration for the guest identification form.
* Only used when customerMode is true and no externalUserId is provided.
*/
guestForm?: GuestFormConfig;
}

@@ -183,2 +279,41 @@ interface Message {

}
/** Return type for the React useDualChat() hook and framework equivalents */
interface DualChatReturn {
db: {
messages: Message[];
isLoading: boolean;
error: Error | null;
status: {
step: string;
label: string;
} | null;
lastData: ToolData | null;
sendMessage: (content: string) => Promise<void>;
stop: () => void;
reset: () => void;
};
support: {
messages: Message[];
conversationId: string | null;
isLoading: boolean;
isEscalated: boolean;
isResolved: boolean;
error: Error | null;
welcomeMessage: string | null;
sendMessage: (content: string, metadata?: Record<string, any>) => Promise<void>;
rateConversation: (rating: number) => Promise<void>;
reset: () => void;
};
}
/** Options for useDualChat() hook/composable */
interface DualChatOptions {
context?: Record<string, any>;
onData?: (data: ToolData) => void;
onEscalated?: () => void;
onResolved?: (conversationId: string) => void;
}
/** Config type alias for unified client (SyncAgentConfig with externalUserId required) */
type UnifiedConfig = SyncAgentConfig & {
externalUserId: string;
};

@@ -201,2 +336,6 @@ declare class SyncAgentClient {

private onAfterToolCall;
private customerMode;
private externalUserId;
private guestStorage;
private guestFormConfig;
constructor(config: SyncAgentConfig);

@@ -213,2 +352,50 @@ private validateConnectionString;

getSchema(): Promise<CollectionSchema[]>;
/**
* Send a message in customer agent mode.
* Routes through the customer support pipeline (persona → flow → KB → escalation → AI).
*
* When no externalUserId is configured (guest mode):
* - Checks GuestStorageManager for a previously stored identity
* - If found, uses the stored guestId as externalUserId
* - If not found, throws GuestIdentificationRequiredError (caught by framework layers to show form)
*/
customerChat(message: string, options?: CustomerChatOptions): Promise<CustomerChatResult>;
/**
* Submit a satisfaction rating (1-5) for a resolved conversation.
*/
rateConversation(conversationId: string, rating: number): Promise<void>;
/**
* Set guest identity after form completion.
* Persists the identity to storage and sets the externalUserId for subsequent API calls.
* Called by framework layers (React hook, etc.) after the guest identification form is submitted.
*/
setGuestIdentity(identity: GuestIdentity): void;
/**
* Get the current guest identity from storage.
* Returns null if no guest identity has been stored.
*/
getGuestIdentity(): GuestIdentity | null;
/**
* Get the guest form configuration provided at client construction.
* Returns undefined if no guestForm config was provided.
*/
getGuestFormConfig(): GuestFormConfig | undefined;
/**
* Create both a database agent and a customer agent from a single config.
* Convenience factory for apps that serve both internal users and end-customers.
*
* @example
* const { db, support } = SyncAgentClient.createDual({
* apiKey: "sa_your_key",
* connectionString: "postgresql://...",
* externalUserId: currentUser.id,
* });
*
* // Admin: direct database queries
* const result = await db.chat([{ role: "user", content: "Show all orders" }]);
*
* // Customer: support pipeline
* const reply = await support.customerChat("I need help with my order");
*/
static createDual(config: DualClientConfig): DualClient;
}

@@ -229,2 +416,64 @@

export { type ChatOptions, type ChatResult, type CollectionSchema, type Message, type SchemaField, SyncAgentClient, type SyncAgentConfig, type ToolData, type ToolDefinition, type ToolParameter, detectPageContext };
/**
* Thrown when customerChat() is called without guest identification in guest mode.
* Framework layers catch this error to display the guest identification form.
*/
declare class GuestIdentificationRequiredError extends Error {
constructor();
}
/**
* Generate a deterministic guest identifier from an email address.
*
* Applies FNV-1a 32-bit hash to the lowercase-trimmed email and returns
* a string in the format: "guest_" + hex hash.
*
* This is a pure function with no side effects.
*
* @param email - The guest's email address
* @returns A deterministic identifier string prefixed with "guest_"
*/
declare function generateGuestIdentifier(email: string): string;
/**
* Manages persistence of guest identity data.
* Uses localStorage when available, falls back to in-memory storage
* for private browsing or quota-exceeded scenarios.
*/
declare class GuestStorageManager {
private memoryFallback;
private storageAvailable;
constructor();
/** Check if localStorage is available and writable */
private checkStorageAvailability;
/** Retrieve stored guest identity, returns null if not found or corrupted */
get(): GuestIdentity | null;
/** Store guest identity, falls back to memory if localStorage unavailable */
set(identity: GuestIdentity): void;
/** Clear stored identity from both localStorage and memory fallback */
clear(): void;
}
/**
* Validate name: must contain at least one non-whitespace character.
*/
declare function validateName(name: string): FieldValidationResult;
/**
* Validate email: must match local@domain.tld pattern.
* Requires non-empty local part, @ symbol, non-empty domain, dot, and non-empty TLD.
*/
declare function validateEmail(email: string): FieldValidationResult;
/**
* Validate the complete guest form submission.
* Returns an object with `valid` boolean and `errors` record mapping field names to error messages.
*/
declare function validateGuestForm(data: {
name: string;
email: string;
phone?: string;
}): {
valid: boolean;
errors: Record<string, string>;
};
export { type ChatOptions, type ChatResult, type CollectionSchema, type CustomerChatOptions, type CustomerChatResult, type DualChatOptions, type DualChatReturn, type DualClient, type DualClientConfig, type FieldValidationResult, type GuestFormConfig, GuestIdentificationRequiredError, type GuestIdentity, GuestStorageManager, type Message, type SchemaField, SyncAgentClient, type SyncAgentConfig, type ToolData, type ToolDefinition, type ToolParameter, type UnifiedConfig, detectPageContext, generateGuestIdentifier, validateEmail, validateGuestForm, validateName };

@@ -23,4 +23,10 @@ "use strict";

__export(index_exports, {
GuestIdentificationRequiredError: () => GuestIdentificationRequiredError,
GuestStorageManager: () => GuestStorageManager,
SyncAgentClient: () => SyncAgentClient,
detectPageContext: () => detectPageContext
detectPageContext: () => detectPageContext,
generateGuestIdentifier: () => generateGuestIdentifier,
validateEmail: () => validateEmail,
validateGuestForm: () => validateGuestForm,
validateName: () => validateName
});

@@ -112,5 +118,81 @@ module.exports = __toCommonJS(index_exports);

// src/guest-storage.ts
var STORAGE_KEY = "syncagent_guest_identity";
var GuestStorageManager = class {
constructor() {
this.memoryFallback = null;
this.storageAvailable = this.checkStorageAvailability();
}
/** Check if localStorage is available and writable */
checkStorageAvailability() {
try {
const testKey = "__syncagent_storage_test__";
localStorage.setItem(testKey, "1");
localStorage.removeItem(testKey);
return true;
} catch {
return false;
}
}
/** Retrieve stored guest identity, returns null if not found or corrupted */
get() {
if (!this.storageAvailable) {
return this.memoryFallback;
}
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) {
return null;
}
const parsed = JSON.parse(raw);
if (typeof parsed.name !== "string" || parsed.name.length === 0 || typeof parsed.email !== "string" || parsed.email.length === 0 || typeof parsed.guestId !== "string" || !parsed.guestId.startsWith("guest_") || parsed.phone !== null && typeof parsed.phone !== "string") {
this.clear();
return null;
}
return {
name: parsed.name,
email: parsed.email,
phone: parsed.phone ?? null,
guestId: parsed.guestId
};
} catch {
this.clear();
return null;
}
}
/** Store guest identity, falls back to memory if localStorage unavailable */
set(identity) {
if (!this.storageAvailable) {
this.memoryFallback = identity;
return;
}
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(identity));
} catch {
this.memoryFallback = identity;
}
}
/** Clear stored identity from both localStorage and memory fallback */
clear() {
this.memoryFallback = null;
if (this.storageAvailable) {
try {
localStorage.removeItem(STORAGE_KEY);
} catch {
}
}
}
};
// src/guest-identification-error.ts
var GuestIdentificationRequiredError = class extends Error {
constructor() {
super("Guest identification required before sending messages");
this.name = "GuestIdentificationRequiredError";
}
};
// src/client.ts
var SYNCAGENT_API_URL = "https://syncagentdev.vercel.app";
var SyncAgentClient = class {
var SyncAgentClient = class _SyncAgentClient {
constructor(config) {

@@ -137,2 +219,6 @@ if (!config.apiKey) throw new Error("SyncAgent: apiKey is required");

this.onAfterToolCall = config.onAfterToolCall || null;
this.customerMode = !!(config.customerMode || config.externalUserId);
this.externalUserId = config.externalUserId || null;
this.guestStorage = new GuestStorageManager();
this.guestFormConfig = config.guestForm;
}

@@ -338,7 +424,221 @@ validateConnectionString(cs) {

}
/**
* Send a message in customer agent mode.
* Routes through the customer support pipeline (persona → flow → KB → escalation → AI).
*
* When no externalUserId is configured (guest mode):
* - Checks GuestStorageManager for a previously stored identity
* - If found, uses the stored guestId as externalUserId
* - If not found, throws GuestIdentificationRequiredError (caught by framework layers to show form)
*/
async customerChat(message, options = {}) {
if (!this.customerMode) {
throw new Error("SyncAgent: customerChat() requires externalUserId in config (or customerMode: true)");
}
if (!this.externalUserId) {
const stored = this.guestStorage.get();
if (stored) {
this.externalUserId = stored.guestId;
} else {
throw new GuestIdentificationRequiredError();
}
}
const body = {
message,
connectionString: this.connectionString
};
if (options.conversationId) body.conversationId = options.conversationId;
if (this.externalUserId) body.externalUserId = this.externalUserId;
if (this.filter && Object.keys(this.filter).length > 0) body.filter = this.filter;
const guestIdentity = this.guestStorage.get();
const guestMetadata = guestIdentity ? { guestName: guestIdentity.name, guestEmail: guestIdentity.email } : {};
body.metadata = { ...guestMetadata, ...options.metadata || {} };
let res;
try {
res = await fetch(`${this.baseUrl}/api/v1/customer-chat`, {
method: "POST",
headers: this.headers(),
body: JSON.stringify(body)
});
} catch (networkErr) {
if (networkErr.name === "AbortError") throw networkErr;
try {
await new Promise((r) => setTimeout(r, 1e3));
res = await fetch(`${this.baseUrl}/api/v1/customer-chat`, {
method: "POST",
headers: this.headers(),
body: JSON.stringify(body)
});
} catch (retryErr) {
throw new Error(`Network error: ${retryErr.message || "Failed to connect to SyncAgent"}`);
}
}
if (!res.ok) {
const err = await res.json().catch(() => ({ error: res.statusText }));
throw new Error(err.error || `HTTP ${res.status}`);
}
const result = await res.json();
if (result.escalated && options.onEscalated) {
options.onEscalated();
}
if (result.resolved && options.onResolved) {
options.onResolved(result.conversationId);
}
return result;
}
/**
* Submit a satisfaction rating (1-5) for a resolved conversation.
*/
async rateConversation(conversationId, rating) {
if (!this.customerMode) {
throw new Error("SyncAgent: rateConversation() requires externalUserId in config");
}
if (!conversationId) throw new Error("SyncAgent: conversationId is required");
if (!Number.isInteger(rating) || rating < 1 || rating > 5) {
throw new Error("SyncAgent: rating must be an integer between 1 and 5");
}
let res;
try {
res = await fetch(`${this.baseUrl}/api/v1/conversations/${conversationId}/rate`, {
method: "POST",
headers: this.headers(),
body: JSON.stringify({ rating })
});
} catch (networkErr) {
try {
await new Promise((r) => setTimeout(r, 1e3));
res = await fetch(`${this.baseUrl}/api/v1/conversations/${conversationId}/rate`, {
method: "POST",
headers: this.headers(),
body: JSON.stringify({ rating })
});
} catch (retryErr) {
throw new Error(`Network error: ${retryErr.message || "Failed to connect to SyncAgent"}`);
}
}
if (!res.ok) {
const err = await res.json().catch(() => ({ error: res.statusText }));
throw new Error(err.error || `HTTP ${res.status}`);
}
}
/**
* Set guest identity after form completion.
* Persists the identity to storage and sets the externalUserId for subsequent API calls.
* Called by framework layers (React hook, etc.) after the guest identification form is submitted.
*/
setGuestIdentity(identity) {
this.guestStorage.set(identity);
this.externalUserId = identity.guestId;
}
/**
* Get the current guest identity from storage.
* Returns null if no guest identity has been stored.
*/
getGuestIdentity() {
return this.guestStorage.get();
}
/**
* Get the guest form configuration provided at client construction.
* Returns undefined if no guestForm config was provided.
*/
getGuestFormConfig() {
return this.guestFormConfig;
}
/**
* Create both a database agent and a customer agent from a single config.
* Convenience factory for apps that serve both internal users and end-customers.
*
* @example
* const { db, support } = SyncAgentClient.createDual({
* apiKey: "sa_your_key",
* connectionString: "postgresql://...",
* externalUserId: currentUser.id,
* });
*
* // Admin: direct database queries
* const result = await db.chat([{ role: "user", content: "Show all orders" }]);
*
* // Customer: support pipeline
* const reply = await support.customerChat("I need help with my order");
*/
static createDual(config) {
console.warn(
"[SyncAgent] createDual() is deprecated. Pass externalUserId directly to new SyncAgentClient() to enable both modes on a single instance."
);
if (!config.externalUserId) {
throw new Error("SyncAgent.createDual: externalUserId is required");
}
const { externalUserId, ...shared } = config;
const db = new _SyncAgentClient({
...shared,
customerMode: false
});
const support = new _SyncAgentClient({
...shared,
customerMode: true,
externalUserId
});
return { db, support };
}
};
// src/guest-identifier.ts
function fnv1a32(input) {
const FNV_OFFSET_BASIS = 2166136261;
const FNV_PRIME = 16777619;
let hash = FNV_OFFSET_BASIS;
for (let i = 0; i < input.length; i++) {
hash ^= input.charCodeAt(i);
hash = Math.imul(hash, FNV_PRIME);
}
return hash >>> 0;
}
function generateGuestIdentifier(email) {
const normalized = email.trim().toLowerCase();
const hash = fnv1a32(normalized);
return `guest_${hash.toString(16)}`;
}
// src/guest-validation.ts
function validateName(name) {
if (!name || name.trim().length === 0) {
return { valid: false, error: "Name is required" };
}
return { valid: true };
}
function validateEmail(email) {
if (!email) {
return { valid: false, error: "Email is required" };
}
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailPattern.test(email)) {
return { valid: false, error: "Please enter a valid email address" };
}
return { valid: true };
}
function validateGuestForm(data) {
const errors = {};
const nameResult = validateName(data.name);
if (!nameResult.valid && nameResult.error) {
errors.name = nameResult.error;
}
const emailResult = validateEmail(data.email);
if (!emailResult.valid && emailResult.error) {
errors.email = emailResult.error;
}
return {
valid: Object.keys(errors).length === 0,
errors
};
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
GuestIdentificationRequiredError,
GuestStorageManager,
SyncAgentClient,
detectPageContext
detectPageContext,
generateGuestIdentifier,
validateEmail,
validateGuestForm,
validateName
});

@@ -84,5 +84,81 @@ // src/stream.ts

// src/guest-storage.ts
var STORAGE_KEY = "syncagent_guest_identity";
var GuestStorageManager = class {
constructor() {
this.memoryFallback = null;
this.storageAvailable = this.checkStorageAvailability();
}
/** Check if localStorage is available and writable */
checkStorageAvailability() {
try {
const testKey = "__syncagent_storage_test__";
localStorage.setItem(testKey, "1");
localStorage.removeItem(testKey);
return true;
} catch {
return false;
}
}
/** Retrieve stored guest identity, returns null if not found or corrupted */
get() {
if (!this.storageAvailable) {
return this.memoryFallback;
}
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) {
return null;
}
const parsed = JSON.parse(raw);
if (typeof parsed.name !== "string" || parsed.name.length === 0 || typeof parsed.email !== "string" || parsed.email.length === 0 || typeof parsed.guestId !== "string" || !parsed.guestId.startsWith("guest_") || parsed.phone !== null && typeof parsed.phone !== "string") {
this.clear();
return null;
}
return {
name: parsed.name,
email: parsed.email,
phone: parsed.phone ?? null,
guestId: parsed.guestId
};
} catch {
this.clear();
return null;
}
}
/** Store guest identity, falls back to memory if localStorage unavailable */
set(identity) {
if (!this.storageAvailable) {
this.memoryFallback = identity;
return;
}
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(identity));
} catch {
this.memoryFallback = identity;
}
}
/** Clear stored identity from both localStorage and memory fallback */
clear() {
this.memoryFallback = null;
if (this.storageAvailable) {
try {
localStorage.removeItem(STORAGE_KEY);
} catch {
}
}
}
};
// src/guest-identification-error.ts
var GuestIdentificationRequiredError = class extends Error {
constructor() {
super("Guest identification required before sending messages");
this.name = "GuestIdentificationRequiredError";
}
};
// src/client.ts
var SYNCAGENT_API_URL = "https://syncagentdev.vercel.app";
var SyncAgentClient = class {
var SyncAgentClient = class _SyncAgentClient {
constructor(config) {

@@ -109,2 +185,6 @@ if (!config.apiKey) throw new Error("SyncAgent: apiKey is required");

this.onAfterToolCall = config.onAfterToolCall || null;
this.customerMode = !!(config.customerMode || config.externalUserId);
this.externalUserId = config.externalUserId || null;
this.guestStorage = new GuestStorageManager();
this.guestFormConfig = config.guestForm;
}

@@ -310,6 +390,220 @@ validateConnectionString(cs) {

}
/**
* Send a message in customer agent mode.
* Routes through the customer support pipeline (persona → flow → KB → escalation → AI).
*
* When no externalUserId is configured (guest mode):
* - Checks GuestStorageManager for a previously stored identity
* - If found, uses the stored guestId as externalUserId
* - If not found, throws GuestIdentificationRequiredError (caught by framework layers to show form)
*/
async customerChat(message, options = {}) {
if (!this.customerMode) {
throw new Error("SyncAgent: customerChat() requires externalUserId in config (or customerMode: true)");
}
if (!this.externalUserId) {
const stored = this.guestStorage.get();
if (stored) {
this.externalUserId = stored.guestId;
} else {
throw new GuestIdentificationRequiredError();
}
}
const body = {
message,
connectionString: this.connectionString
};
if (options.conversationId) body.conversationId = options.conversationId;
if (this.externalUserId) body.externalUserId = this.externalUserId;
if (this.filter && Object.keys(this.filter).length > 0) body.filter = this.filter;
const guestIdentity = this.guestStorage.get();
const guestMetadata = guestIdentity ? { guestName: guestIdentity.name, guestEmail: guestIdentity.email } : {};
body.metadata = { ...guestMetadata, ...options.metadata || {} };
let res;
try {
res = await fetch(`${this.baseUrl}/api/v1/customer-chat`, {
method: "POST",
headers: this.headers(),
body: JSON.stringify(body)
});
} catch (networkErr) {
if (networkErr.name === "AbortError") throw networkErr;
try {
await new Promise((r) => setTimeout(r, 1e3));
res = await fetch(`${this.baseUrl}/api/v1/customer-chat`, {
method: "POST",
headers: this.headers(),
body: JSON.stringify(body)
});
} catch (retryErr) {
throw new Error(`Network error: ${retryErr.message || "Failed to connect to SyncAgent"}`);
}
}
if (!res.ok) {
const err = await res.json().catch(() => ({ error: res.statusText }));
throw new Error(err.error || `HTTP ${res.status}`);
}
const result = await res.json();
if (result.escalated && options.onEscalated) {
options.onEscalated();
}
if (result.resolved && options.onResolved) {
options.onResolved(result.conversationId);
}
return result;
}
/**
* Submit a satisfaction rating (1-5) for a resolved conversation.
*/
async rateConversation(conversationId, rating) {
if (!this.customerMode) {
throw new Error("SyncAgent: rateConversation() requires externalUserId in config");
}
if (!conversationId) throw new Error("SyncAgent: conversationId is required");
if (!Number.isInteger(rating) || rating < 1 || rating > 5) {
throw new Error("SyncAgent: rating must be an integer between 1 and 5");
}
let res;
try {
res = await fetch(`${this.baseUrl}/api/v1/conversations/${conversationId}/rate`, {
method: "POST",
headers: this.headers(),
body: JSON.stringify({ rating })
});
} catch (networkErr) {
try {
await new Promise((r) => setTimeout(r, 1e3));
res = await fetch(`${this.baseUrl}/api/v1/conversations/${conversationId}/rate`, {
method: "POST",
headers: this.headers(),
body: JSON.stringify({ rating })
});
} catch (retryErr) {
throw new Error(`Network error: ${retryErr.message || "Failed to connect to SyncAgent"}`);
}
}
if (!res.ok) {
const err = await res.json().catch(() => ({ error: res.statusText }));
throw new Error(err.error || `HTTP ${res.status}`);
}
}
/**
* Set guest identity after form completion.
* Persists the identity to storage and sets the externalUserId for subsequent API calls.
* Called by framework layers (React hook, etc.) after the guest identification form is submitted.
*/
setGuestIdentity(identity) {
this.guestStorage.set(identity);
this.externalUserId = identity.guestId;
}
/**
* Get the current guest identity from storage.
* Returns null if no guest identity has been stored.
*/
getGuestIdentity() {
return this.guestStorage.get();
}
/**
* Get the guest form configuration provided at client construction.
* Returns undefined if no guestForm config was provided.
*/
getGuestFormConfig() {
return this.guestFormConfig;
}
/**
* Create both a database agent and a customer agent from a single config.
* Convenience factory for apps that serve both internal users and end-customers.
*
* @example
* const { db, support } = SyncAgentClient.createDual({
* apiKey: "sa_your_key",
* connectionString: "postgresql://...",
* externalUserId: currentUser.id,
* });
*
* // Admin: direct database queries
* const result = await db.chat([{ role: "user", content: "Show all orders" }]);
*
* // Customer: support pipeline
* const reply = await support.customerChat("I need help with my order");
*/
static createDual(config) {
console.warn(
"[SyncAgent] createDual() is deprecated. Pass externalUserId directly to new SyncAgentClient() to enable both modes on a single instance."
);
if (!config.externalUserId) {
throw new Error("SyncAgent.createDual: externalUserId is required");
}
const { externalUserId, ...shared } = config;
const db = new _SyncAgentClient({
...shared,
customerMode: false
});
const support = new _SyncAgentClient({
...shared,
customerMode: true,
externalUserId
});
return { db, support };
}
};
// src/guest-identifier.ts
function fnv1a32(input) {
const FNV_OFFSET_BASIS = 2166136261;
const FNV_PRIME = 16777619;
let hash = FNV_OFFSET_BASIS;
for (let i = 0; i < input.length; i++) {
hash ^= input.charCodeAt(i);
hash = Math.imul(hash, FNV_PRIME);
}
return hash >>> 0;
}
function generateGuestIdentifier(email) {
const normalized = email.trim().toLowerCase();
const hash = fnv1a32(normalized);
return `guest_${hash.toString(16)}`;
}
// src/guest-validation.ts
function validateName(name) {
if (!name || name.trim().length === 0) {
return { valid: false, error: "Name is required" };
}
return { valid: true };
}
function validateEmail(email) {
if (!email) {
return { valid: false, error: "Email is required" };
}
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailPattern.test(email)) {
return { valid: false, error: "Please enter a valid email address" };
}
return { valid: true };
}
function validateGuestForm(data) {
const errors = {};
const nameResult = validateName(data.name);
if (!nameResult.valid && nameResult.error) {
errors.name = nameResult.error;
}
const emailResult = validateEmail(data.email);
if (!emailResult.valid && emailResult.error) {
errors.email = emailResult.error;
}
return {
valid: Object.keys(errors).length === 0,
errors
};
}
export {
GuestIdentificationRequiredError,
GuestStorageManager,
SyncAgentClient,
detectPageContext
detectPageContext,
generateGuestIdentifier,
validateEmail,
validateGuestForm,
validateName
};
+18
-5
{
"name": "@syncagent/js",
"version": "0.3.4",
"version": "0.4.0",
"description": "SyncAgent JavaScript SDK — AI database agent for any app",

@@ -28,7 +28,13 @@ "homepage": "https://syncagentdev.vercel.app/docs",

"*": {
".": ["./dist/index.d.ts"],
"*": ["./dist/index.d.ts"]
".": [
"./dist/index.d.ts"
],
"*": [
"./dist/index.d.ts"
]
}
},
"files": ["dist"],
"files": [
"dist"
],
"scripts": {

@@ -43,3 +49,10 @@ "build": "tsup",

"license": "MIT",
"keywords": ["syncagent", "ai", "database", "agent", "sdk", "chat"]
"keywords": [
"syncagent",
"ai",
"database",
"agent",
"sdk",
"chat"
]
}
+392
-0

@@ -237,2 +237,384 @@ # @syncagent/js

## Customer Agent Mode
Route messages through the customer support pipeline — with persona, knowledge base, conversation flows, escalation, and AI fallback — instead of the direct database agent.
```typescript
import { SyncAgentClient } from "@syncagent/js";
const client = new SyncAgentClient({
apiKey: "sa_your_api_key",
externalUserId: "customer_123",
});
```
> **Note:** `customerMode` is automatically enabled when `externalUserId` is provided. You do not need to set `customerMode: true` explicitly.
### Configuration
| Option | Type | Required | Description |
|--------|------|----------|-------------|
| `customerMode` | `boolean` | — | Enable customer agent mode. Routes messages through the customer support pipeline. |
| `externalUserId` | `string` | — | Customer identifier for multi-tenant scoping. When provided, `customerMode` is automatically enabled and both `chat()` and `customerChat()` become available on the same instance. |
### `client.customerChat(message, options?)`
Send a message through the customer support pipeline.
**Error:** Throws `"SyncAgent: customerChat() requires externalUserId in config (or customerMode: true)"` if called without `externalUserId` in the client config.
```typescript
const result = await client.customerChat("How do I reset my password?", {
conversationId: "conv_abc123",
metadata: { page: "/settings" },
onEscalated: () => console.log("Escalated to human agent"),
onResolved: (id) => console.log(`Conversation ${id} resolved`),
});
```
**Parameters**
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `message` | `string` | ✅ | The customer's message |
| `options` | `CustomerChatOptions` | — | Optional configuration (see below) |
**Returns** `Promise<CustomerChatResult>`
#### `CustomerChatOptions`
| Field | Type | Description |
|-------|------|-------------|
| `conversationId` | `string` | Existing conversation ID to continue |
| `metadata` | `Record<string, any>` | Additional metadata to store on the conversation |
| `onEscalated` | `() => void` | Called when the conversation is escalated to a human agent |
| `onResolved` | `(conversationId: string) => void` | Called when the conversation is resolved |
#### `CustomerChatResult`
| Field | Type | Description |
|-------|------|-------------|
| `conversationId` | `string` | The conversation ID (new or existing) |
| `response` | `string` | The agent's reply text |
| `escalated` | `boolean` | Whether the conversation was escalated to a human agent |
| `autoReply` | `boolean` | Whether this was an automatic reply (e.g. welcome message) |
| `flowActive` | `boolean` | Whether a conversation flow is currently active |
| `resolved` | `boolean` | Whether the conversation has been resolved |
| `welcomeMessage` | `string \| undefined` | Welcome message if this is a new conversation |
| `sources` | `any[] \| undefined` | Knowledge base sources used to generate the response |
| `flowSession` | `{ flowId: string; currentNodeId: string } \| undefined` | Active flow session state |
### `client.rateConversation(conversationId, rating)`
Submit a satisfaction rating for a completed conversation.
**Error:** Throws `"SyncAgent: rateConversation() requires externalUserId in config"` if called without `externalUserId` in the client config.
```typescript
await client.rateConversation("conv_abc123", 5);
```
**Parameters**
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `conversationId` | `string` | ✅ | The conversation ID to rate |
| `rating` | `number` | ✅ | Integer between 1 and 5 |
**Validation rules:**
- `conversationId` must be a non-empty string
- `rating` must be an integer between 1 and 5 (inclusive)
- Throws an error if validation fails
**Returns** `Promise<void>`
### Conversation Flows
When a customer's message matches a configured flow's trigger phrases, the agent enters a **conversation flow** — a scripted branching path that guides the customer through a series of decision points, responses, and terminal actions (resolve or escalate).
You can detect an active flow by checking the `flowActive` and `flowSession` fields in the `CustomerChatResult` response:
| Field | Type | Description |
|-------|------|-------------|
| `flowActive` | `boolean` | `true` when the conversation is currently inside a flow |
| `flowSession` | `{ flowId: string; currentNodeId: string } \| undefined` | Identifies which flow and node the conversation is on |
#### Detecting and Handling Active Flows
```typescript
const result = await client.customerChat("I want to check my order status");
if (result.flowActive && result.flowSession) {
console.log(`Flow active: ${result.flowSession.flowId}`);
console.log(`Current node: ${result.flowSession.currentNodeId}`);
// The response contains the current node's content (e.g. decision options)
console.log(result.response);
// Example: "How would you like to check your order?\n1. By order number\n2. By email address"
// Present the options to the user in your UI, then send their choice
// as the next message to continue the flow:
const followUp = await client.customerChat("By order number", {
conversationId: result.conversationId,
});
// Check if the flow is still active or has reached a terminal node
if (!followUp.flowActive) {
console.log("Flow completed");
}
}
```
When `flowActive` is `false`, the conversation is handled by the standard AI agent (persona, knowledge base, or escalation). Flows take priority over the AI agent when a trigger phrase matches.
### `SyncAgentClient.createDual(config)`
> ⚠️ **Deprecated:** `createDual()` will be removed in a future major version. Use the unified `SyncAgentClient` constructor instead — pass `externalUserId` directly to enable both modes on a single instance.
Create both a database agent and a customer agent from a single config. Useful for apps that serve both internal users (admins) and end-customers from the same codebase.
**Legacy pattern (deprecated):**
```typescript
import { SyncAgentClient } from "@syncagent/js";
const { db, support } = SyncAgentClient.createDual({
apiKey: "sa_your_api_key",
connectionString: "postgresql://user:pass@host:5432/mydb",
externalUserId: currentUser.id,
});
// Admin: direct database queries
const result = await db.chat([{ role: "user", content: "Show all overdue invoices" }]);
// Customer: support pipeline (persona, flows, KB, escalation)
const reply = await support.customerChat("I need help with my order");
await support.rateConversation(reply.conversationId, 5);
```
#### Migration
**Before** — using `createDual()`:
```typescript
import { SyncAgentClient } from "@syncagent/js";
const { db, support } = SyncAgentClient.createDual({
apiKey: "sa_your_api_key",
connectionString: "postgresql://user:pass@host:5432/mydb",
externalUserId: "customer_123",
});
const dbResult = await db.chat([{ role: "user", content: "Show all overdue invoices" }]);
const supportResult = await support.customerChat("I need help with my order");
```
**After** — unified constructor:
```typescript
import { SyncAgentClient } from "@syncagent/js";
const client = new SyncAgentClient({
apiKey: "sa_your_api_key",
connectionString: "postgresql://user:pass@host:5432/mydb",
externalUserId: "customer_123",
});
const dbResult = await client.chat([{ role: "user", content: "Show all overdue invoices" }]);
const supportResult = await client.customerChat("I need help with my order");
```
**Parameters**
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `config` | `DualClientConfig` | ✅ | Shared configuration for both clients |
#### `DualClientConfig`
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `apiKey` | `string` | ✅ | Your SyncAgent API key |
| `connectionString` | `string` | ✅ | Database connection string |
| `externalUserId` | `string` | ✅ | Customer identifier — scopes customer agent conversations |
| `baseUrl` | `string` | — | Override API base URL |
| `filter` | `Record<string, any>` | — | Mandatory query filter for multi-tenant scoping |
| `operations` | `string[]` | — | Restrict allowed operations (read, create, update, delete) |
| `tools` | `Record<string, ToolDefinition>` | — | Custom tools for the database agent |
| `systemInstruction` | `string` | — | Custom system prompt |
| `language` | `string` | — | Response language |
**Returns** `{ db: SyncAgentClient, support: SyncAgentClient }`
- `db` — Database agent instance (`customerMode: false`). Use `db.chat()` for direct DB queries.
- `support` — Customer agent instance (`customerMode: true`). Use `support.customerChat()` for the support pipeline.
## Guest Identification
When using Customer Agent Mode without an `externalUserId`, the SDK supports a guest identification flow. Guests provide their name and email before chatting, and their identity is persisted to localStorage for returning visits.
### Client Methods
#### `client.getGuestIdentity()`
Returns the stored guest identity, or `null` if no identity has been saved.
```typescript
const identity = client.getGuestIdentity();
// GuestIdentity | null
```
#### `client.setGuestIdentity(identity)`
Persists a guest identity to localStorage and sets the `externalUserId` for subsequent API calls.
```typescript
import { generateGuestIdentifier } from "@syncagent/js";
client.setGuestIdentity({
name: "Jane Doe",
email: "jane@example.com",
phone: null,
guestId: generateGuestIdentifier("jane@example.com"),
});
```
#### `client.getGuestFormConfig()`
Returns the `GuestFormConfig` passed at construction (via the `guestForm` option), or `undefined` if none was provided.
```typescript
const formConfig = client.getGuestFormConfig();
// GuestFormConfig | undefined
```
### `onGuestIdentified` Callback
Use the `onGuestIdentified` callback in `CustomerChatOptions` to react when a guest completes identification:
```typescript
import { SyncAgentClient } from "@syncagent/js";
const client = new SyncAgentClient({
apiKey: "sa_your_api_key",
customerMode: true,
guestForm: {
title: "Hi there!",
subtitle: "Tell us who you are to get started",
submitButtonText: "Begin Chat",
},
});
const result = await client.customerChat("Hello", {
onGuestIdentified: (identity) => {
console.log(`Guest identified: ${identity.name} (${identity.guestId})`);
},
});
```
### Utility Functions
| Function | Signature | Description |
|----------|-----------|-------------|
| `validateName` | `(name: string) => FieldValidationResult` | Validates that name contains at least one non-whitespace character |
| `validateEmail` | `(email: string) => FieldValidationResult` | Validates email matches `local@domain.tld` pattern |
| `validateGuestForm` | `(data: { name, email, phone? }) => { valid: boolean; errors: Record<string, string> }` | Validates the complete guest form submission |
| `generateGuestIdentifier` | `(email: string) => string` | Generates a deterministic `guest_`-prefixed ID from the email (FNV-1a hash) |
```typescript
import {
validateName,
validateEmail,
validateGuestForm,
generateGuestIdentifier,
} from "@syncagent/js";
// Individual field validation
const nameResult = validateName("Jane");
// { valid: true }
const emailResult = validateEmail("not-an-email");
// { valid: false, error: "Please enter a valid email address" }
// Full form validation
const formResult = validateGuestForm({
name: "Jane Doe",
email: "jane@example.com",
phone: "+1234567890",
});
// { valid: true, errors: {} }
// Generate a deterministic guest ID
const guestId = generateGuestIdentifier("jane@example.com");
// "guest_a1b2c3d4"
```
### Types
```typescript
import type {
GuestIdentity,
GuestFormConfig,
FieldValidationResult,
} from "@syncagent/js";
```
#### `GuestIdentity`
| Field | Type | Description |
|-------|------|-------------|
| `name` | `string` | Guest's display name |
| `email` | `string` | Guest's email address |
| `phone` | `string \| null` | Optional phone number |
| `guestId` | `string` | Deterministic identifier (`guest_` + FNV-1a hex hash of email) |
#### `GuestFormConfig`
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `title` | `string` | `"Welcome"` | Form heading text |
| `subtitle` | `string` | `"Please introduce yourself to get started"` | Form description text |
| `submitButtonText` | `string` | `"Start Chat"` | Submit button label |
| `namePlaceholder` | `string` | — | Placeholder for the name input |
| `emailPlaceholder` | `string` | — | Placeholder for the email input |
| `phonePlaceholder` | `string` | — | Placeholder for the phone input |
| `className` | `string` | — | Custom CSS class for the form container |
| `onSubmit` | `(identity: GuestIdentity) => void` | — | Called after successful form submission |
#### `FieldValidationResult`
| Field | Type | Description |
|-------|------|-------------|
| `valid` | `boolean` | Whether the field passed validation |
| `error` | `string \| undefined` | Error message if validation failed |
## Unified Dual Mode
By passing `externalUserId` to the `SyncAgentClient` constructor alongside your `connectionString`, both `chat()` and `customerChat()` become available on the same instance — no need to call `createDual()` or manage separate clients.
This is the simplest way to build apps that serve both internal (admin/database) and customer-facing use cases from a single client.
```typescript
import { SyncAgentClient } from "@syncagent/js";
const client = new SyncAgentClient({
apiKey: "sa_your_api_key",
connectionString: process.env.DATABASE_URL,
externalUserId: "customer_123",
});
// Database agent — direct queries
const dbResult = await client.chat([
{ role: "user", content: "Show all overdue invoices" }
]);
console.log(dbResult.text);
// Customer agent — support pipeline (persona, flows, KB, escalation)
const supportResult = await client.customerChat("I need help with my order", {
onEscalated: () => console.log("Escalated to human agent"),
onResolved: (id) => console.log(`Conversation ${id} resolved`),
});
console.log(supportResult.response);
```
## Abort / Cancel

@@ -280,5 +662,15 @@

CollectionSchema, SchemaField, ToolDefinition, ToolParameter, ToolData,
CustomerChatResult, CustomerChatOptions, DualClientConfig, DualClient,
DualChatReturn, DualChatOptions, UnifiedConfig,
} from "@syncagent/js";
```
### New Types Reference
| Type | Description | Import |
|------|-------------|--------|
| `DualChatReturn` | Return type for the `useDualChat()` hook and framework equivalents — contains `db` and `support` namespaces with messages, state, and actions | `import type { DualChatReturn } from "@syncagent/js"` |
| `DualChatOptions` | Options for the `useDualChat()` hook/composable — includes `context`, `onData`, `onEscalated`, and `onResolved` callbacks | `import type { DualChatOptions } from "@syncagent/js"` |
| `UnifiedConfig` | Config type alias for the unified client — `SyncAgentConfig` with `externalUserId` required | `import type { UnifiedConfig } from "@syncagent/js"` |
## Security

@@ -285,0 +677,0 @@