# @hiyve/cloud

Framework-agnostic HTTP client and types for Hiyve Cloud AI services. Works with any JavaScript runtime -- Node.js, browsers, Deno, or edge functions. For React applications, use `@hiyve/react-intelligence` which wraps this package with React context and hooks.

## Installation

```bash
npm install @hiyve/cloud
```

## Quick Start

```typescript
import { CloudClient } from '@hiyve/cloud';

// Create a client with a token generator (most common pattern)
const client = new CloudClient({
  generateCloudToken: async () => {
    const res = await fetch('/api/generate-cloud-token', { method: 'POST' });
    const data = await res.json();
    return data.cloudToken;
  },
});

// AI query
const response = await client.query('Summarize the discussion', {
  userId: 'user@example.com',
  roomName: 'team-standup',
});
console.log(response.content);

// Generate a meeting summary
const summary = await client.getSummary(transcript, {
  userId: 'user@example.com',
  maxLength: 500,
});

// Extract action items
const items = await client.getActionItems(transcript, {
  userId: 'user@example.com',
});
```

## Authentication

`CloudClient` requires either a `cloudToken` or a `generateCloudToken` function. Tokens are generated lazily on first request and automatically refreshed when expired.

### 1. Token Generator (Recommended)

Provide a function that fetches a cloud token from your backend. Tokens are generated lazily on first request and automatically refreshed when expired. When your server uses `@hiyve/admin` middleware, your endpoint can delegate to the built-in token route.

```typescript
const client = new CloudClient({
  generateCloudToken: async () => {
    const res = await fetch('/api/generate-cloud-token', { method: 'POST' });
    return (await res.json()).cloudToken;
  },
});
```

### 2. Static Cloud Token

A pre-generated short-lived token (prefixed `ct_`). Obtain from your backend by calling `POST https://api.hiyve.dev/cloud-token` with your API key.

```typescript
const client = new CloudClient({
  cloudToken: 'ct_abc123...',
});
```

You can update the token later with `client.setCloudToken(newToken)` or force a refresh with `client.clearToken()`.

## API Reference

### Constructor

```typescript
new CloudClient(config: CloudConfig)
```

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `cloudToken` | `string` | - | Pre-generated cloud token |
| `generateCloudToken` | `(params) => Promise<CloudTokenResponse>` | - | Token generator function |
| `userId` | `string` | - | User email for token generation. Required when using `generateCloudToken` |
| `environment` | `'production' \| 'development'` | `'production'` | Target environment |
| `baseUrl` | `string` | - | Custom API URL (overrides environment) |
| `timeout` | `number` | `30000` | Request timeout in milliseconds |
| `presence` | `boolean \| { intervalMs?: number }` | `false` | Enable automatic presence heartbeat. `true` uses 10s interval; pass `{ intervalMs }` to customize |

### Token Management

| Method | Description |
|--------|-------------|
| `setCloudToken(token)` | Update the cached cloud token |
| `clearToken()` | Clear cached token, forcing re-generation on next request |
| `setUserId(userId)` | Set or change the current user's email. Forces a token refresh on next request |

### AI Intelligence

#### `query(text, options): Promise<AIResponse>`

Send a general AI query.

```typescript
const response = await client.query('What were the key takeaways?', {
  userId: 'user@example.com',
  roomName: 'team-standup',
  model: 'gpt-4o-mini',
  systemPrompt: 'You are a meeting analyst.',
  context: 'Additional context here...',
});
console.log(response.content);
```

#### `getSummary(transcript, options): Promise<string>`

Generate a summary from a transcript.

```typescript
const summary = await client.getSummary(transcript, {
  userId: 'user@example.com',
  maxLength: 200,
});
```

#### `getActionItems(transcript, options): Promise<ActionItem[]>`

Extract action items from a transcript.

```typescript
const items = await client.getActionItems(transcript, {
  userId: 'user@example.com',
});

items.forEach(item => {
  console.log(`[${item.priority}] ${item.action} (${item.category})`);
});
```

#### `enhanceNote(text, options): Promise<string>`

Enhance or polish text content.

```typescript
const enhanced = await client.enhanceNote(roughNotes, {
  userId: 'user@example.com',
  style: 'professional', // 'professional' | 'casual' | 'academic' etc.
});
```

### Response Context

#### `queryResponse(responseId, userId, query, options?): Promise<AIResponse>`

Query against accumulated meeting context identified by a response ID.

```typescript
const response = await client.queryResponse(
  'resp_abc123',
  'user@example.com',
  'Summarize the first 10 minutes.'
);
```

#### `pushMoodToResponse(responseId, userId, sentimentContext): Promise<{ responseId: string }>`

Push mood data to a specific response context.

```typescript
await client.pushMoodToResponse('resp_abc123', 'user@example.com', [
  { participant: 'user@example.com', sentiment: 'positive', confidence: 0.9 },
]);
```

### Alerts

#### `getAlertSettings(roomId): Promise<AlertSettings>`

Retrieve alert settings for a room.

#### `updateAlertSettings(roomId, settings): Promise<AlertSettings>`

Update alert settings for a room.

```typescript
const updated = await client.updateAlertSettings('room-123', {
  enabled: true,
  audioAlerts: false,
  categories: {
    action_item: { enabled: true, sensitivity: 'high', minConfidence: 0.6 },
  },
});
```

#### `getAlertHistory(roomId, options?): Promise<MeetingAlert[]>`

Retrieve alert history with optional filtering.

```typescript
const alerts = await client.getAlertHistory('room-123', {
  limit: 50,
  priority: 'high',
  type: 'action_item',
  since: '2024-01-01T00:00:00Z',
});
```

#### `acknowledgeAlert(alertId, note?): Promise<MeetingAlert>`

Acknowledge a meeting alert.

```typescript
await client.acknowledgeAlert('alert-456', 'Reviewed and assigned to team.');
```

### Scheduling

#### `createMeeting(options): Promise<Meeting>`

Create a scheduled meeting.

```typescript
const meeting = await client.createMeeting({
  title: 'Weekly Standup',
  startTime: new Date('2024-06-01T09:00:00Z'),
  endTime: new Date('2024-06-01T09:30:00Z'),
  timezone: 'America/New_York',
  participants: [
    { email: 'john@example.com', name: 'John', role: 'organizer' },
    { email: 'jane@example.com', name: 'Jane', role: 'attendee' },
  ],
  recurrence: { pattern: 'weekly', interval: 1 },
});
```

#### `getMeetings(options?): Promise<Meeting[]>`

List meetings with optional filters.

```typescript
const meetings = await client.getMeetings({ limit: 20, status: 'scheduled' });
```

#### `getMeeting(meetingId): Promise<Meeting>`

Get details for a specific meeting.

#### `updateMeeting(meetingId, updates): Promise<Meeting>`

Update a meeting.

```typescript
await client.updateMeeting('meeting-123', { title: 'Updated Standup' });
```

#### `cancelMeeting(meetingId): Promise<void>`

Cancel a meeting.

#### `sendMeetingNotification(meetingId, type?): Promise<void>`

Send a notification to meeting participants.

```typescript
await client.sendMeetingNotification('meeting-123', 'reminder');
// type: 'reminder' | 'invitation' | 'update' | 'cancellation'
```

### Search

Semantic search across meeting transcriptions and documents.

#### `search(query, options?): Promise<SearchResults>`

Search across all sources.

```typescript
const results = await client.search('budget discussion', {
  userId: 'user@example.com',
  roomNames: ['team-standup'],
  limit: 10,
  threshold: 0.6,
});

results.results.forEach(r => {
  console.log(`[${r.source}] ${r.content} (score: ${r.similarity})`);
});
```

#### `searchTranscriptions(query, options?): Promise<SearchResults>`

Search within transcriptions only.

#### `searchDocuments(query, options?): Promise<SearchResults>`

Search within documents only.

#### `ask(query, options?): Promise<AskResponse>`

Search for relevant context, then generate an AI answer from the results.

```typescript
const answer = await client.ask('What was decided about the launch date?', {
  userId: 'user@example.com',
  roomNames: ['product-review'],
  model: 'gpt-4o-mini',
});
console.log(answer.content);
console.log(`Sources: ${answer.sources.length}`);
```

**SearchOptions**

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `sources` | `('transcriptions' \| 'documents')[]` | all | Filter by source type |
| `roomNames` | `string[]` | - | Filter by room names |
| `dateRange` | `{ start?, end? }` | - | Filter by date range |
| `limit` | `number` | `20` | Maximum results |
| `threshold` | `number` | `0.5` | Minimum similarity score (0-1) |
| `userId` | `string` | - | User ID for scoping |
| `roomName` | `string` | - | Room name for scoping |
| `recordingId` | `string` | - | Recording ID for scoping |

### Analysis

Analyze meeting transcripts with purpose-built scorecards.

#### `analyzeSalesCall(options): Promise<AnalysisResponse<SalesScorecard>>`

Score a sales call across discovery, qualification, objection handling, closing, and next steps.

```typescript
const result = await client.analyzeSalesCall({
  transcript,
  userId: 'user@example.com',
});
console.log(`Overall: ${result.analysis.overallScore}/100`);
console.log(`Discovery: ${result.analysis.dimensions.discovery.score}/100`);
```

#### `analyzeInterview(options): Promise<AnalysisResponse<InterviewAssessment>>`

Assess an interview across technical competency, communication, problem solving, culture fit, and enthusiasm.

```typescript
const result = await client.analyzeInterview({ transcript, userId: 'user@example.com' });
console.log(`Recommendation: ${result.analysis.hiringRecommendation}`);
```

#### `analyzeSession(options): Promise<AnalysisResponse<SessionSummary>>`

Summarize a session including primary concerns, topics discussed, recommendations, and follow-ups.

#### `analyzeCoaching(options): Promise<AnalysisResponse<CoachingAnalysis>>`

Analyze communication patterns including talk/listen ratio, filler words, interruptions, and question quality.

#### `analyzeCustom(options): Promise<AnalysisResponse<CustomAnalysis>>`

Run a custom analysis with your own rubric dimensions.

```typescript
const result = await client.analyzeCustom({
  transcript,
  userId: 'user@example.com',
  rubric: {
    dimensions: [
      { name: 'Clarity', description: 'How clearly were points communicated?' },
      { name: 'Engagement', description: 'How engaged were participants?' },
    ],
  },
});
console.log(`Clarity: ${result.analysis.dimensions['Clarity'].score}/100`);
```

**AnalysisOptions**

| Option | Type | Description |
|--------|------|-------------|
| `transcript` | `string` | Meeting transcript text (required) |
| `userId` | `string` | User ID |
| `roomName` | `string` | Room name |
| `model` | `string` | AI model override |

### Presence

#### `checkPresence(callerProfileId, match): Promise<string[]>`

Check which connected users match a metadata-based relationship with the caller. The server derives match values from the caller's profile.

```typescript
const onlineUserIds = await client.checkPresence('profile_123', {
  userField: 'organizationId',
  callerField: 'organizationId',
});
console.log('Online teammates:', onlineUserIds);
```

### Active Room Discovery

Advertise rooms and discover active rooms in real time.

#### `advertiseRoom(options): Promise<void>`

Make a room discoverable by targeted users.

```typescript
await client.advertiseRoom({
  name: 'team-standup',
  ownerDisplayName: 'John',
  targetUserIds: ['jane@example.com', 'bob@example.com'],
  metadata: { passwordRequired: false },
});
```

#### `removeAdvertisedRoom(roomName): Promise<void>`

Remove a room from discovery.

#### `updateAdvertisedRoom(roomName, updates): Promise<void>`

Update an advertised room's metadata or target users.

#### `getActiveRooms(userId?): Promise<ActiveRoom[]>`

List active rooms. When `userId` is provided, only rooms targeting that user are returned.

```typescript
const rooms = await client.getActiveRooms('jane@example.com');
rooms.forEach(room => {
  console.log(`${room.name} by ${room.ownerDisplayName}`);
});
```

#### `getActiveRoomsStreamUrl(userId): Promise<string>`

Get a signed SSE stream URL for active room discovery. The URL includes a single-use ticket for authentication (since EventSource does not support custom headers).

```typescript
const url = await client.getActiveRoomsStreamUrl('jane@example.com');
const eventSource = new EventSource(url);
```

#### `connectActiveRoomsStream(userId, handlers?): Promise<ActiveRoomsStreamHandle>`

Open a real-time stream for active room updates. Returns a handle with `close()` to disconnect.

```typescript
const stream = await client.connectActiveRoomsStream('jane@example.com', {
  onSnapshot: (rooms) => console.log('All rooms:', rooms),
  onRoomAdded: (room) => console.log('New room:', room.name),
  onRoomRemoved: (name) => console.log('Room closed:', name),
  onRoomUpdated: (room) => console.log('Room updated:', room.name),
  onError: (err) => console.error('Stream error:', err),
});

// Later: disconnect
stream.close();
```

### Chat

Persistent chat with real-time streaming via SSE.

#### `getConversations(userId): Promise<ChatConversation[]>`

Get all conversations the user has participated in, sorted by most recent activity.

```typescript
const conversations = await client.getConversations('user@example.com');
conversations.forEach(c => {
  console.log(`${c.roomName}: ${c.messageCount} messages`);
});
```

#### `getChatHistory(roomName, options?): Promise<ChatHistoryResponse>`

Get paginated message history for a room. Use cursor-based pagination for older messages.

```typescript
const history = await client.getChatHistory('team-standup', { limit: 50 });
history.messages.forEach(m => {
  console.log(`[${m.userId}] ${m.message}`);
});

// Load older messages
if (history.nextCursor) {
  const older = await client.getChatHistory('team-standup', {
    cursor: history.nextCursor,
    limit: 50,
  });
}
```

#### `sendChatMessage(roomName, message, userId): Promise<{ messageId: string }>`

Send a message to a room. Returns the persisted message ID.

```typescript
const { messageId } = await client.sendChatMessage(
  'team-standup',
  'Hello team!',
  'user@example.com'
);
```

#### `connectChatStream(roomName, handlers?): Promise<ChatStreamHandle>`

Open a real-time SSE stream for chat messages. Returns a handle with `close()` to disconnect.

```typescript
const stream = await client.connectChatStream('team-standup', {
  onSnapshot: (messages) => console.log('Recent messages:', messages),
  onMessage: (msg) => console.log('New message:', msg.message),
  onError: (err) => console.error('Stream error:', err),
});

// Later: disconnect
stream.close();
```

#### `getChatStreamUrl(roomName): Promise<string>`

Get a signed SSE stream URL for chat messages. Useful when you need to manage the `EventSource` directly.

```typescript
const url = await client.getChatStreamUrl('team-standup');
const eventSource = new EventSource(url);
```

## Admin Client (Server-Side)

For server-side admin operations (managing user profiles, organizations, API keys), use the dedicated [`@hiyve/admin`](../admin) package. It provides three layers of server-only enforcement to prevent accidental client-side usage.

```bash
npm install @hiyve/admin
```

## Mood Bridge Utilities

Utility functions for converting emotion data from `@hiyve/mood-analysis` into the sentiment format used by the cloud API.

### `emotionToSentiment(emotion): 'positive' | 'negative' | 'neutral'`

Maps an emotion string to a sentiment classification.

| Emotion | Sentiment |
|---------|-----------|
| `happy`, `surprised` | `positive` |
| `angry`, `fearful`, `disgusted`, `sad` | `negative` |
| `neutral` (and all others) | `neutral` |

```typescript
import { emotionToSentiment } from '@hiyve/cloud';

emotionToSentiment('happy');    // 'positive'
emotionToSentiment('angry');    // 'negative'
emotionToSentiment('neutral');  // 'neutral'
```

### `moodStateToSentimentEntry(participantId, moodState): SentimentEntry`

Converts a mood state object into a `SentimentEntry` for use with `pushMoodToResponse`.

```typescript
import { moodStateToSentimentEntry } from '@hiyve/cloud';

const entry = moodStateToSentimentEntry('user@example.com', {
  emotion: 'happy',
  confidence: 0.92,
  timestamp: Date.now(),
});
// { participant: 'user@example.com', sentiment: 'positive', confidence: 0.92, timestamp: ... }

await client.pushMoodToResponse('resp_abc123', 'user@example.com', [entry]);
```

The `moodState` parameter accepts any object matching the `MoodStateLike` interface, including the full `MoodState` type from `@hiyve/mood-analysis`.

## Validation Utilities

Client-side validation helpers to catch invalid input before making API requests.

```typescript
import {
  validateLength,
  validateRoomName,
  validateTranscriptBatch,
  validateSearchQuery,
  MAX_QUERY_LENGTH,
  MAX_TRANSCRIPT_LENGTH,
  MAX_ROOM_NAME_LENGTH,
  MAX_TRANSCRIPT_BATCH_SIZE,
} from '@hiyve/cloud';
```

| Function | Description |
|----------|-------------|
| `validateLength(value, maxLength, fieldName)` | Throws if string exceeds max length |
| `validateRoomName(roomName)` | Throws if room name is empty or exceeds 256 characters |
| `validateTranscriptBatch(segments)` | Throws if batch exceeds 1000 segments |
| `validateSearchQuery(query)` | Throws if search query is empty or exceeds max length |

| Constant | Value |
|----------|-------|
| `MAX_QUERY_LENGTH` | 32,000 characters |
| `MAX_TRANSCRIPT_LENGTH` | 500,000 characters |
| `MAX_ROOM_NAME_LENGTH` | 256 characters |
| `MAX_TRANSCRIPT_BATCH_SIZE` | 1,000 segments |

## Environment Configuration

```typescript
import { CloudClient, ENVIRONMENT_URLS } from '@hiyve/cloud';

// Use production (default)
const client = new CloudClient({ cloudToken: 'ct_...' });

// Use development environment
const client = new CloudClient({
  cloudToken: 'ct_...',
  environment: 'development',
});

// Use a custom URL (overrides environment)
const client = new CloudClient({
  cloudToken: 'ct_...',
  baseUrl: 'https://custom-api.example.com',
});
```

| Environment | Description |
|-------------|-------------|
| `production` (default) | Production API endpoint |
| `development` | Development/staging API endpoint |

The correct URL for each environment is resolved automatically via `ENVIRONMENT_URLS`. You can override with `baseUrl` if needed. HTTPS is required for all URLs. HTTP is only permitted for `localhost` and `127.0.0.1` during development.

## React Users

For React applications, use [`@hiyve/react-intelligence`](../react-intelligence) instead. It wraps `CloudClient` with React context and provides hooks like `useIntelligence`, `useLiveContext`, `useAlerts`, and `useScheduling`. When used with `IdentityProvider` and `@hiyve/admin` server middleware, cloud tokens are generated automatically -- no props needed.

```tsx
import { IdentityProvider } from '@hiyve/react-identity';
import { CloudProvider } from '@hiyve/react-intelligence';

function App() {
  return (
    <IdentityProvider>
      <CloudProvider>
        <MeetingRoom />
      </CloudProvider>
    </IdentityProvider>
  );
}
```

## Default Constants

| Constant | Value | Description |
|----------|-------|-------------|
| `DEFAULT_BASE_URL` | `ENVIRONMENT_URLS.production` | Default API base URL (production endpoint) |
| `DEFAULT_TIMEOUT` | `30000` | Default request timeout in milliseconds |
| `DEFAULT_ENVIRONMENT` | `'production'` | Default cloud environment |
| `DEFAULT_MODEL` | `'gpt-4o-mini'` | Default AI model |
| `DEFAULT_SUMMARY_MAX_LENGTH` | `100` | Default summary max length (words) |
| `DEFAULT_ENHANCE_STYLE` | `'professional'` | Default note enhancement style |
| `DEFAULT_ALERT_SETTINGS` | `{ enabled: true, ... }` | Default alert settings with medium sensitivity for all categories |

## Types

All types are exported from the package entry point. Key types by category:

**Configuration:** `CloudConfig`, `CloudEnvironment`, `CloudTokenResponse`, `GenerateCloudTokenParams`

**AI Intelligence:** `QueryOptions`, `AIResponse`, `SummaryOptions`, `ActionItem`, `ActionItemsOptions`, `EnhanceOptions`

**Transcription & Sentiment:** `TranscriptSegment`, `SentimentEntry`, `SentimentContext`, `MoodStateLike`, `TranscriptionMoodData`

**Alerts:** `MeetingAlert`, `AlertSettings`, `AlertHistoryOptions`

**Scheduling:** `Meeting`, `MeetingParticipant`, `CreateMeetingOptions`, `NotificationType`

**Search:** `SearchOptions`, `SearchResult`, `SearchResults`, `AskOptions`, `AskResponse`

**Analysis:** `AnalysisOptions`, `CustomAnalysisOptions`, `AnalysisResponse`, `DimensionScore`, `SalesScorecard`, `InterviewAssessment`, `SessionSummary`, `CoachingAnalysis`, `CustomAnalysis`

**Active Rooms:** `CloudActiveRoom`, `CloudAdvertiseRoomOptions`, `ActiveRoomsStreamHandle`

**Chat:** `ChatConversation`, `ChatHistoryResponse`, `ChatHistoryMessage`, `ChatStreamMessage`, `ChatStreamHandle`

**Pagination:** `Pagination`

## License

Proprietary - Hiyve SDK
