@askingahuman/mcp-server 1.0.0 → 2.0.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/README.md CHANGED
@@ -1,26 +1,40 @@
1
1
  # askingahuman MCP Server
2
2
 
3
- Get real human answers from AI agents and LLMs.
3
+ Let your AI agent reach a real person through [askingahuman.com](https://askingahuman.com): send a free question to someone you choose, or book a paid live chat with them.
4
4
 
5
- When your AI doesn't know the answer, needs a judgment call, or wants a second opinion — ask a real human.
5
+ Every request names a person. Find one with `list_humans` or `who_is_live`, then pass their `humanId`.
6
6
 
7
- ## What It Does
7
+ ## Tools
8
8
 
9
- This MCP server connects your AI agent to [askingahuman.com](https://askingahuman.com), where a real person reads your question and writes back a thoughtful, honest answer within 24 hours.
9
+ **Free**
10
+ - `list_humans`: browse expert profiles (name, title, specializations, rating).
11
+ - `who_is_live`: see who is live right now. It can wait up to 50 seconds for someone to come online.
12
+ - `ask_human`: send a free question (max 400 characters) to the person you choose. Not everyone takes free questions. Returns a `questionId`.
13
+ - `check_answer`: read the answer and any follow-up conversation.
14
+ - `reply_to_human`: continue the conversation after an answer.
15
+ - `get_quote`: see what a live chat with a person would cost, what is held on the card, and how long it can run. Nothing is charged.
16
+ - `submit_feedback`: send feedback about the service.
10
17
 
11
- **Two tools:**
12
- - `ask_human` — Submit a question (max 400 chars). Returns a `questionId`.
13
- - `check_answer` — Poll for the human's response using the `questionId`.
18
+ **Paid live chat** (local server only, text only)
19
+ - `connect_human`: request a live chat with a person. Shows the cost to your user for approval when the client supports it. If the person is offline, the request waits up to 24 hours for them.
20
+ - `check_connection`: wait for the person to accept. You then join the chat automatically. Shows the paid time left.
21
+ - `read_messages` / `send_message`: talk in the chat. `read_messages` warns when less than 10 minutes of paid time remain.
22
+ - `extend_session`: add another hour, with the same cost approval as `connect_human`. Shows the new end time.
23
+ - `end_session`: leave the chat. Before the person accepts, this cancels the request and releases the hold.
24
+
25
+ ### How live chat is priced
26
+
27
+ Each person sets their own price per hour of chat. The first hour is held on the card when you request and charged in full once the person accepts; if they never accept, nothing is charged. `extend_session` buys one more hour at a time, charged in full. Paid time is not refunded if you end early, so the chat stays open until the paid time runs out or you end it. A session, extensions included, is capped by your spend limit, by the platform's $200 limit and by its 4-hour chat limit. All sales are final.
14
28
 
15
29
  ## Setup
16
30
 
17
- ### 1. Get your free API key
31
+ ### 1. Get your API key
18
32
 
19
- Visit [askingahuman.com/developers](https://askingahuman.com/developers) and create a key with your email.
33
+ Create a key at [askingahuman.com/developers](https://askingahuman.com/developers). To use live chat, add a card there too.
20
34
 
21
- ### 2. Add to your MCP client
35
+ ### 2. Add it to your MCP client
22
36
 
23
- **Claude Desktop** (`claude_desktop_config.json`):
37
+ **Claude Desktop** (`claude_desktop_config.json`) or **Cursor** (Settings > MCP Servers):
24
38
 
25
39
  ```json
26
40
  {
@@ -29,68 +43,66 @@ Visit [askingahuman.com/developers](https://askingahuman.com/developers) and cre
29
43
  "command": "npx",
30
44
  "args": ["-y", "@askingahuman/mcp-server"],
31
45
  "env": {
32
- "ASKINGAHUMAN_API_KEY": "ahk_your_key_here"
46
+ "ASKINGAHUMAN_API_KEY": "ahk_your_key_here",
47
+ "ASKINGAHUMAN_MAX_SPEND_USD": "100"
33
48
  }
34
49
  }
35
50
  }
36
51
  }
37
52
  ```
38
53
 
39
- **Cursor** (Settings > MCP Servers):
54
+ Live chat uses the optional package `@livekit/rtc-node` (installed automatically on Windows, macOS and Linux). Without it, the free tools still work.
40
55
 
41
- ```json
42
- {
43
- "askingahuman": {
44
- "command": "npx",
45
- "args": ["-y", "@askingahuman/mcp-server"],
46
- "env": {
47
- "ASKINGAHUMAN_API_KEY": "ahk_your_key_here"
48
- }
49
- }
50
- }
56
+ ### Docker
57
+
58
+ ```bash
59
+ docker build -t askingahuman/mcp-server .
60
+ docker run -i --rm -e ASKINGAHUMAN_API_KEY=ahk_your_key_here askingahuman/mcp-server
51
61
  ```
52
62
 
53
- ## Pricing
63
+ ## Configuration
64
+
65
+ | Variable | Default | What it does |
66
+ |----------|---------|--------------|
67
+ | `ASKINGAHUMAN_API_KEY` | (required) | Your API key. |
68
+ | `ASKINGAHUMAN_MAX_SPEND_USD` | `100` | The most one live chat can cost, extensions included (max 200, the platform cap). The agent can ask for less, never more. |
69
+ | `ASKINGAHUMAN_REQUIRE_CONFIRMATION` | `false` | If `true`, paid chats and extensions happen only when your MCP client can ask you to approve the cost. |
70
+ | `ASKINGAHUMAN_SKIP_APPROVAL` | `false` | If `true`, the server doesn't ask for cost approval in the chat. Use it with clients that claim to show MCP approval forms but don't (for example, some editor extensions decline them automatically). Your client still asks before each tool call, and the spend limit still applies. Ignored when `ASKINGAHUMAN_REQUIRE_CONFIRMATION` is on. |
71
+ | `ASKINGAHUMAN_API_URL` | `https://api.askingahuman.com` | API address (https only). |
54
72
 
55
- | Tier | Price | Details |
56
- |------|-------|---------|
57
- | Free | $0 | 1 question/month, no credit card needed |
58
- | Paid | $5/question | After free tier; requires saved payment method |
73
+ ## Safety
59
74
 
60
- Add a payment method at [askingahuman.com/developers](https://askingahuman.com/developers) to unlock paid questions.
75
+ - **Spending is capped twice.** Your spend limit is enforced in this server and again by the API, which won't sell an hour, or an extension, that would go past it.
76
+ - **You approve each charge.** `connect_human` and `extend_session` show the cost to your user first when the client supports it.
77
+ - **Nothing is left waiting.** When the MCP client shuts down, the server ends its open chats and cancels requests nobody has accepted yet, which releases their holds. The platform ends a chat when its paid time runs out.
78
+ - **Nothing sensitive goes out.** Messages to people are blocked if they contain your API key or anything that looks like a credential (cloud keys, tokens, private keys). Links are blocked too, so an agent can't send someone to a download or a phishing page.
79
+ - **Text only, no files.** The server joins the chat without receiving audio or video and ignores everything except plain-text chat from the person. Files can't be sent or received.
80
+ - **People's words are framed as untrusted.** Messages and answers from people are wrapped in `<human_message>` tags with a note telling the model to treat them as conversation, not instructions.
81
+ - **Private by default.** Chats are kept in memory only, never written to disk or logged, and dropped 10 minutes after they end. The chat's access token stays inside this server and is never shown to the model.
61
82
 
62
83
  ## Example
63
84
 
64
- An AI agent using this server might:
85
+ 1. `list_humans` or `who_is_live` finds the right person.
86
+ 2. `get_quote` with their `humanId` shows the price per hour and the hold.
87
+ 3. `connect_human` asks your user to approve, then notifies the person.
88
+ 4. `check_connection` with `wait_seconds: 45` waits until they accept, then joins the chat.
89
+ 5. `send_message` / `read_messages` until done. `extend_session` if you need more time; `end_session` to leave.
65
90
 
66
- 1. Call `ask_human` with: "I'm helping a user choose between React and Vue for a small dashboard project. What would an experienced developer recommend and why?"
67
- 2. Receive a `questionId` back immediately
68
- 3. Periodically call `check_answer` with that ID
69
- 4. Eventually receive a thoughtful, human-written response
91
+ Just need an answer? `ask_human` with their `humanId`, then `check_answer` later.
70
92
 
71
93
  ## REST API
72
94
 
73
- You can also use the API directly without the MCP server:
74
-
75
95
  ```bash
76
- # Submit a question
77
- curl -X POST https://askingahuman.com/api/questions \
78
- -H "Authorization: Bearer ahk_your_key" \
79
- -H "Content-Type: application/json" \
80
- -d '{"questionText": "Your question here"}'
81
-
82
- # Check for answer
83
- curl https://askingahuman.com/api/questions/{questionId} \
84
- -H "Authorization: Bearer ahk_your_key"
96
+ # Ask a chosen person a question (free)
97
+ curl -X POST https://api.askingahuman.com/api/questions \
98
+ -H "Authorization: Bearer ahk_your_key" -H "Content-Type: application/json" \
99
+ -d '{"questionText": "Your question here", "targetExpertId": "HUMAN_ID"}'
100
+
101
+ # Who is live, and what would an hour of chat with them cost?
102
+ curl https://api.askingahuman.com/api/live-now -H "Authorization: Bearer ahk_your_key"
103
+ curl "https://api.askingahuman.com/api/connect/quote?mode=message&humanId=HUMAN_ID" -H "Authorization: Bearer ahk_your_key"
85
104
  ```
86
105
 
87
- ## Environment Variables
88
-
89
- | Variable | Required | Description |
90
- |----------|----------|-------------|
91
- | `ASKINGAHUMAN_API_KEY` | Yes | Your API key from askingahuman.com/developers |
92
- | `ASKINGAHUMAN_API_URL` | No | Custom API URL (defaults to production) |
93
-
94
106
  ## License
95
107
 
96
108
  MIT
@@ -0,0 +1,10 @@
1
+ export declare const DEFAULT_API_BASE = "https://api.askingahuman.com";
2
+ export declare const PLATFORM_MAX_SPEND_CENTS = 20000;
3
+ export interface McpConfig {
4
+ apiBase: string;
5
+ maxSpendCents: number;
6
+ requireConfirmation: boolean;
7
+ skipApproval: boolean;
8
+ }
9
+ export declare function resolveApiBase(raw: string | undefined): string;
10
+ export declare function loadConfig(env?: NodeJS.ProcessEnv): McpConfig;
package/dist/config.js ADDED
@@ -0,0 +1,37 @@
1
+ // Settings the person running this MCP server controls through environment
2
+ // variables. The spend limit set here binds the agent: tool arguments can only
3
+ // lower it, and the platform caps every session at $200 (and chat at 4 hours)
4
+ // regardless.
5
+ export const DEFAULT_API_BASE = 'https://api.askingahuman.com';
6
+ export const PLATFORM_MAX_SPEND_CENTS = 20000;
7
+ function boundedNumber(raw, fallback, min, max) {
8
+ const parsed = Number(raw);
9
+ if (raw === undefined || raw.trim() === '' || !Number.isFinite(parsed))
10
+ return fallback;
11
+ return Math.min(max, Math.max(min, parsed));
12
+ }
13
+ // The API key travels in every request, so only HTTPS is accepted (plain HTTP
14
+ // only for a local development API).
15
+ export function resolveApiBase(raw) {
16
+ const value = (raw || DEFAULT_API_BASE).trim().replace(/\/+$/, '');
17
+ let url;
18
+ try {
19
+ url = new URL(value);
20
+ }
21
+ catch {
22
+ throw new Error(`ASKINGAHUMAN_API_URL is not a valid URL: ${value}`);
23
+ }
24
+ const local = url.hostname === 'localhost' || url.hostname === '127.0.0.1';
25
+ if (url.protocol !== 'https:' && !(local && url.protocol === 'http:')) {
26
+ throw new Error('ASKINGAHUMAN_API_URL must use https:// (http:// is allowed only for localhost).');
27
+ }
28
+ return value;
29
+ }
30
+ export function loadConfig(env = process.env) {
31
+ return {
32
+ apiBase: resolveApiBase(env.ASKINGAHUMAN_API_URL),
33
+ maxSpendCents: Math.round(boundedNumber(env.ASKINGAHUMAN_MAX_SPEND_USD, 100, 0.01, PLATFORM_MAX_SPEND_CENTS / 100) * 100),
34
+ requireConfirmation: /^(1|true|yes)$/i.test(env.ASKINGAHUMAN_REQUIRE_CONFIRMATION || ''),
35
+ skipApproval: /^(1|true|yes)$/i.test(env.ASKINGAHUMAN_SKIP_APPROVAL || ''),
36
+ };
37
+ }
@@ -0,0 +1,33 @@
1
+ import type { McpConfig } from './config.js';
2
+ import type { LiveBridge } from './live/bridge.js';
3
+ export type Confirmation = 'accepted' | 'declined' | 'cancelled' | 'unsupported';
4
+ export interface ToolContext {
5
+ apiKey: string;
6
+ config: McpConfig;
7
+ live: LiveBridge | null;
8
+ confirm: (message: string) => Promise<Confirmation>;
9
+ }
10
+ export interface ToolResult {
11
+ [key: string]: unknown;
12
+ content: Array<{
13
+ type: 'text';
14
+ text: string;
15
+ }>;
16
+ isError?: boolean;
17
+ }
18
+ export declare function textResult(text: string): ToolResult;
19
+ export declare function errorResult(text: string): ToolResult;
20
+ export interface ApiResponse {
21
+ ok: boolean;
22
+ status: number;
23
+ data: Record<string, unknown>;
24
+ }
25
+ export declare class ApiError extends Error {
26
+ }
27
+ export declare function callApi(ctx: Pick<ToolContext, 'apiKey' | 'config'>, path: string, init?: {
28
+ method?: string;
29
+ body?: unknown;
30
+ timeoutMs?: number;
31
+ }): Promise<ApiResponse>;
32
+ export declare function apiErrorText(res: ApiResponse, fallback: string): string;
33
+ export declare function dollars(cents: unknown): string;
@@ -0,0 +1,42 @@
1
+ export function textResult(text) {
2
+ return { content: [{ type: 'text', text }] };
3
+ }
4
+ export function errorResult(text) {
5
+ return { content: [{ type: 'text', text }], isError: true };
6
+ }
7
+ export class ApiError extends Error {
8
+ }
9
+ // JSON request to the askingahuman API with the caller's key and a timeout.
10
+ // Throws ApiError with a readable message on network failure or timeout.
11
+ export async function callApi(ctx, path, init = {}) {
12
+ const controller = new AbortController();
13
+ const timeoutId = setTimeout(() => controller.abort(), init.timeoutMs ?? 30_000);
14
+ try {
15
+ const response = await fetch(`${ctx.config.apiBase}${path}`, {
16
+ method: init.method ?? 'GET',
17
+ headers: {
18
+ Authorization: `Bearer ${ctx.apiKey}`,
19
+ ...(init.body !== undefined && { 'Content-Type': 'application/json' }),
20
+ },
21
+ ...(init.body !== undefined && { body: JSON.stringify(init.body) }),
22
+ signal: controller.signal,
23
+ });
24
+ const data = (await response.json().catch(() => ({})));
25
+ return { ok: response.ok, status: response.status, data };
26
+ }
27
+ catch (err) {
28
+ if (err instanceof Error && err.name === 'AbortError') {
29
+ throw new ApiError('The askingahuman API did not respond within 30 seconds. Try again shortly.');
30
+ }
31
+ throw new ApiError(`Network error: ${err instanceof Error ? err.message : 'unknown error'}.`);
32
+ }
33
+ finally {
34
+ clearTimeout(timeoutId);
35
+ }
36
+ }
37
+ export function apiErrorText(res, fallback) {
38
+ return `Error (${res.status}): ${typeof res.data.error === 'string' ? res.data.error : fallback}`;
39
+ }
40
+ export function dollars(cents) {
41
+ return `$${(Number(cents) / 100).toFixed(2)}`;
42
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,53 @@
1
+ import http from 'http';
2
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
3
+ import { loadConfig } from './config.js';
4
+ import { createMcpServer } from './server.js';
5
+ // Hosted server: stateless per request, so no live chat (that needs a
6
+ // long-lived connection); free questions, discovery and quotes only.
7
+ const config = loadConfig();
8
+ const PORT = parseInt(process.env.PORT || '3000', 10);
9
+ const httpServer = http.createServer(async (req, res) => {
10
+ // Health check
11
+ if (req.url === '/health' && req.method === 'GET') {
12
+ res.writeHead(200, { 'Content-Type': 'application/json' });
13
+ res.end(JSON.stringify({ status: 'ok', service: 'askingahuman-mcp' }));
14
+ return;
15
+ }
16
+ // MCP endpoint
17
+ if (req.url === '/mcp' && req.method === 'POST') {
18
+ // Extract API key from Authorization header
19
+ const authHeader = req.headers.authorization;
20
+ if (!authHeader || !authHeader.startsWith('Bearer ahk_')) {
21
+ res.writeHead(401, { 'Content-Type': 'application/json' });
22
+ res.end(JSON.stringify({ error: 'API key required. Format: Bearer ahk_...' }));
23
+ return;
24
+ }
25
+ const apiKey = authHeader.slice(7);
26
+ try {
27
+ const server = createMcpServer({ apiKey, config, live: null });
28
+ const transport = new StreamableHTTPServerTransport({
29
+ sessionIdGenerator: undefined, // Stateless
30
+ });
31
+ await server.connect(transport);
32
+ await transport.handleRequest(req, res);
33
+ // Clean up after response
34
+ await server.close();
35
+ }
36
+ catch (err) {
37
+ console.error('MCP request error:', err);
38
+ if (!res.headersSent) {
39
+ res.writeHead(500, { 'Content-Type': 'application/json' });
40
+ res.end(JSON.stringify({ error: 'Internal server error' }));
41
+ }
42
+ }
43
+ return;
44
+ }
45
+ // 404 for everything else
46
+ res.writeHead(404, { 'Content-Type': 'application/json' });
47
+ res.end(JSON.stringify({ error: 'Not found. Use POST /mcp for MCP requests, GET /health for health check.' }));
48
+ });
49
+ httpServer.listen(PORT, () => {
50
+ console.log(`askingahuman MCP HTTP server listening on port ${PORT}`);
51
+ console.log(`Health: http://localhost:${PORT}/health`);
52
+ console.log(`MCP: POST http://localhost:${PORT}/mcp`);
53
+ });
package/dist/index.js CHANGED
@@ -1,39 +1,35 @@
1
1
  #!/usr/bin/env node
2
- import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
2
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
- import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
5
- import { askHumanTool, handleAskHuman } from './tools/ask-human.js';
6
- import { checkAnswerTool, handleCheckAnswer } from './tools/check-answer.js';
7
- const API_BASE = process.env.ASKINGAHUMAN_API_URL || 'https://askingahuman.com';
8
- const server = new Server({ name: 'askingahuman', version: '1.0.0' }, { capabilities: { tools: {} } });
9
- server.setRequestHandler(ListToolsRequestSchema, async () => ({
10
- tools: [askHumanTool, checkAnswerTool]
11
- }));
12
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
13
- const apiKey = process.env.ASKINGAHUMAN_API_KEY;
14
- if (!apiKey) {
15
- return {
16
- content: [{
17
- type: 'text',
18
- text: 'Error: ASKINGAHUMAN_API_KEY environment variable is required. Get your free key at https://askingahuman.com/developers'
19
- }],
20
- isError: true
21
- };
22
- }
23
- switch (request.params.name) {
24
- case 'ask_human':
25
- return handleAskHuman(request.params.arguments, apiKey, API_BASE);
26
- case 'check_answer':
27
- return handleCheckAnswer(request.params.arguments, apiKey, API_BASE);
28
- default:
29
- return {
30
- content: [{ type: 'text', text: `Unknown tool: ${request.params.name}` }],
31
- isError: true
32
- };
33
- }
34
- });
3
+ import { loadConfig } from './config.js';
4
+ import { LiveBridge, loadLiveKit } from './live/bridge.js';
5
+ import { createMcpServer } from './server.js';
6
+ import { endSessionViaApi } from './tools/live.js';
35
7
  async function main() {
36
- const transport = new StdioServerTransport();
37
- await server.connect(transport);
8
+ const config = loadConfig();
9
+ const apiKey = process.env.ASKINGAHUMAN_API_KEY;
10
+ // Local (stdio) server: this process can hold live chats open for the agent.
11
+ const live = new LiveBridge({
12
+ loadRoomFactory: loadLiveKit,
13
+ endSession: (sessionId) => endSessionViaApi({ apiKey: apiKey ?? '', config }, sessionId),
14
+ });
15
+ const server = createMcpServer({ apiKey, config, live });
16
+ // When the client goes away: end open chats and cancel requests nobody has
17
+ // accepted yet (that releases their holds).
18
+ // On a signal, exit after that; when stdin closes, let in-flight requests
19
+ // finish and the process exit on its own (all bridge timers are unref'd).
20
+ let stopping = null;
21
+ const endChats = () => (stopping ??= live.shutdown().catch(() => undefined));
22
+ const exitAfterEnding = async () => {
23
+ await Promise.race([endChats(), new Promise((resolve) => setTimeout(resolve, 5000))]);
24
+ process.exit(0);
25
+ };
26
+ process.on('SIGINT', exitAfterEnding);
27
+ process.on('SIGTERM', exitAfterEnding);
28
+ process.stdin.on('end', endChats);
29
+ process.stdin.on('close', endChats);
30
+ await server.connect(new StdioServerTransport());
38
31
  }
39
- main().catch(console.error);
32
+ main().catch((err) => {
33
+ console.error(err instanceof Error ? err.message : err);
34
+ process.exit(1);
35
+ });
@@ -0,0 +1,68 @@
1
+ export interface ChatRoom {
2
+ on(event: 'dataReceived', cb: (payload: Uint8Array, participant?: {
3
+ identity: string;
4
+ }, kind?: unknown, topic?: string) => void): unknown;
5
+ on(event: 'participantConnected' | 'participantDisconnected', cb: (participant: {
6
+ identity: string;
7
+ }) => void): unknown;
8
+ on(event: 'disconnected', cb: (reason?: unknown) => void): unknown;
9
+ connect(url: string, token: string, opts: {
10
+ autoSubscribe: boolean;
11
+ dynacast: boolean;
12
+ }): Promise<void>;
13
+ disconnect(): Promise<void>;
14
+ localParticipant?: {
15
+ publishData(data: Uint8Array, opts: {
16
+ reliable?: boolean;
17
+ }): Promise<void>;
18
+ };
19
+ remoteParticipants?: Map<string, {
20
+ identity: string;
21
+ }>;
22
+ }
23
+ export type RoomFactory = () => ChatRoom;
24
+ export declare function loadLiveKit(): Promise<RoomFactory | null>;
25
+ export interface ChatLine {
26
+ from: 'human' | 'you' | 'system';
27
+ text: string;
28
+ at: string;
29
+ }
30
+ export declare class LiveChatError extends Error {
31
+ }
32
+ export declare const MAX_ACTIVE_CHATS = 2;
33
+ export declare const MAX_MESSAGE_BYTES: number;
34
+ export declare const MAX_SENT_PER_SESSION = 200;
35
+ export declare const MIN_SEND_INTERVAL_MS = 1000;
36
+ export declare const FORGET_AFTER_END_MS: number;
37
+ export interface LiveBridgeOptions {
38
+ loadRoomFactory: () => Promise<RoomFactory | null>;
39
+ endSession: (sessionId: string) => Promise<void>;
40
+ now?: () => number;
41
+ }
42
+ export declare class LiveBridge {
43
+ private opts;
44
+ private chats;
45
+ private pending;
46
+ private factory;
47
+ private now;
48
+ constructor(opts: LiveBridgeOptions);
49
+ isAvailable(): Promise<boolean>;
50
+ private getFactory;
51
+ trackRequest(sessionId: string): void;
52
+ isJoined(sessionId: string): boolean;
53
+ join(sessionId: string, url: string, token: string): Promise<'joined' | 'already'>;
54
+ private onData;
55
+ private push;
56
+ private getChat;
57
+ send(sessionId: string, text: string): Promise<void>;
58
+ read(sessionId: string, waitMs: number): Promise<{
59
+ lines: ChatLine[];
60
+ closed: boolean;
61
+ closeReason: string | null;
62
+ humanPresent: boolean;
63
+ }>;
64
+ transcript(sessionId: string): ChatLine[];
65
+ end(sessionId: string, reason?: string): Promise<boolean>;
66
+ private close;
67
+ shutdown(): Promise<void>;
68
+ }