getpatter 0.3.0 → 0.4.1

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.
@@ -0,0 +1,20 @@
1
+ // src/logger.ts
2
+ var defaultLogger = {
3
+ info: (msg, ...args) => console.log(`[PATTER] ${msg}`, ...args),
4
+ warn: (msg, ...args) => console.warn(`[PATTER] WARNING: ${msg}`, ...args),
5
+ error: (msg, ...args) => console.error(`[PATTER] ERROR: ${msg}`, ...args),
6
+ debug: () => {
7
+ }
8
+ };
9
+ var currentLogger = defaultLogger;
10
+ function getLogger() {
11
+ return currentLogger;
12
+ }
13
+ function setLogger(logger) {
14
+ currentLogger = logger;
15
+ }
16
+
17
+ export {
18
+ getLogger,
19
+ setLogger
20
+ };
@@ -1,22 +1,10 @@
1
+ import {
2
+ getLogger
3
+ } from "./chunk-FMNRCP5X.mjs";
4
+
1
5
  // src/test-mode.ts
2
6
  import { createInterface } from "readline";
3
7
 
4
- // src/logger.ts
5
- var defaultLogger = {
6
- info: (msg, ...args) => console.log(`[PATTER] ${msg}`, ...args),
7
- warn: (msg, ...args) => console.warn(`[PATTER] WARNING: ${msg}`, ...args),
8
- error: (msg, ...args) => console.error(`[PATTER] ERROR: ${msg}`, ...args),
9
- debug: () => {
10
- }
11
- };
12
- var currentLogger = defaultLogger;
13
- function getLogger() {
14
- return currentLogger;
15
- }
16
- function setLogger(logger) {
17
- currentLogger = logger;
18
- }
19
-
20
8
  // src/llm-loop.ts
21
9
  var OpenAILLMProvider = class {
22
10
  apiKey;
@@ -402,8 +390,6 @@ var TestSession = class {
402
390
  };
403
391
 
404
392
  export {
405
- getLogger,
406
- setLogger,
407
393
  OpenAILLMProvider,
408
394
  LLMLoop,
409
395
  TestSession
@@ -0,0 +1,45 @@
1
+ import {
2
+ getLogger
3
+ } from "./chunk-FMNRCP5X.mjs";
4
+
5
+ // src/tunnel.ts
6
+ var log = getLogger();
7
+ async function startTunnel(port, timeoutMs = 3e4) {
8
+ let tunnelMod;
9
+ try {
10
+ tunnelMod = await import("cloudflared");
11
+ } catch {
12
+ throw new Error(
13
+ 'Built-in tunnel requires the "cloudflared" package. Install it with:\n\n npm install cloudflared\n\nOr provide your own webhookUrl instead of using tunnel: true.'
14
+ );
15
+ }
16
+ log.info("Starting tunnel to localhost:%d ...", port);
17
+ const result = tunnelMod.tunnel({
18
+ "--url": `http://localhost:${port}`
19
+ });
20
+ const tunnelUrl = await Promise.race([
21
+ result.url,
22
+ new Promise(
23
+ (_, reject) => setTimeout(() => reject(new Error(
24
+ `Tunnel failed to start within ${timeoutMs / 1e3}s. Check your internet connection or provide webhookUrl manually.`
25
+ )), timeoutMs)
26
+ )
27
+ ]);
28
+ const hostname = tunnelUrl.replace(/^https?:\/\//, "").replace(/\/$/, "");
29
+ log.info("Tunnel ready: https://%s", hostname);
30
+ result.connections.then(() => {
31
+ log.info("Tunnel connections established");
32
+ }).catch(() => {
33
+ });
34
+ return {
35
+ hostname,
36
+ stop: () => {
37
+ log.info("Stopping tunnel...");
38
+ result.stop();
39
+ }
40
+ };
41
+ }
42
+
43
+ export {
44
+ startTunnel
45
+ };
package/dist/index.d.mts CHANGED
@@ -104,7 +104,7 @@ interface LocalOptions {
104
104
  twilioToken?: string;
105
105
  openaiKey?: string;
106
106
  phoneNumber: string;
107
- webhookUrl: string;
107
+ webhookUrl?: string;
108
108
  telephonyProvider?: 'twilio' | 'telnyx';
109
109
  telnyxKey?: string;
110
110
  telnyxConnectionId?: string;
@@ -148,6 +148,8 @@ type PipelineMessageHandler = (data: Record<string, unknown>) => Promise<string>
148
148
  interface ServeOptions {
149
149
  agent: AgentOptions;
150
150
  port?: number;
151
+ /** When true, start a cloudflared tunnel automatically (requires `cloudflared` npm package). */
152
+ tunnel?: boolean;
151
153
  onCallStart?: (data: Record<string, unknown>) => Promise<void>;
152
154
  onCallEnd?: (data: Record<string, unknown>) => Promise<void>;
153
155
  onTranscript?: (data: Record<string, unknown>) => Promise<void>;
@@ -206,6 +208,7 @@ declare class Patter {
206
208
  private readonly mode;
207
209
  private readonly localConfig;
208
210
  private embeddedServer;
211
+ private tunnelHandle;
209
212
  constructor(options: PatterOptions | LocalOptions);
210
213
  agent(opts: AgentOptions): AgentOptions;
211
214
  serve(opts: ServeOptions): Promise<void>;
@@ -863,4 +866,27 @@ declare function resample16kTo8k(pcm16k: Buffer): Buffer;
863
866
  */
864
867
  declare function resample24kTo16k(pcm24k: Buffer): Buffer;
865
868
 
866
- export { type Agent, type AgentOptions, AuthenticationError, type Call, type CallControl, type CallEventHandler, type CallMetrics, CallMetricsAccumulator, type CallOptions, type CallRecord, type ConnectOptions, type CostBreakdown, type CreateAgentOptions, DEFAULT_PRICING, DeepgramSTT, ElevenLabsConvAIAdapter, ElevenLabsTTS, type Guardrail, type IncomingMessage, type LLMChunk, LLMLoop, type LLMProvider, type LatencyBreakdown, type LocalCallOptions, type LocalConfig, type LocalOptions, type Logger, type MessageHandler, MetricsStore, OpenAILLMProvider, OpenAIRealtimeAdapter, OpenAITTS, Patter, PatterConnectionError, PatterError, type PatterOptions, type PhoneNumber, type PipelineMessageHandler, type ProviderPricing, ProvisionError, RemoteMessageHandler, type SSEEvent, type STTConfig, type ServeOptions, type TTSConfig, TestSession, type ToolDefinition, type TurnMetrics, WhisperSTT, calculateRealtimeCost, calculateSttCost, calculateTelephonyCost, calculateTtsCost, callsToCsv, callsToJson, deepgram, elevenlabs, getLogger, isRemoteUrl, isWebSocketUrl, makeAuthMiddleware, mergePricing, mountApi, mountDashboard, mulawToPcm16, openaiTts, pcm16ToMulaw, resample16kTo8k, resample24kTo16k, resample8kTo16k, setLogger, whisper };
869
+ /**
870
+ * Built-in tunnel support via cloudflared.
871
+ *
872
+ * Spawns a Cloudflare Quick Tunnel that exposes a local port to the internet.
873
+ * Zero account required — uses Cloudflare's free trycloudflare.com service.
874
+ *
875
+ * Install: npm install cloudflared
876
+ */
877
+ interface TunnelHandle {
878
+ /** Public hostname (no protocol), e.g. "random-name.trycloudflare.com" */
879
+ hostname: string;
880
+ /** Stop the tunnel process */
881
+ stop: () => void;
882
+ }
883
+ /**
884
+ * Start a cloudflared quick tunnel pointing to the given local port.
885
+ *
886
+ * @param port - Local port to tunnel to
887
+ * @param timeoutMs - How long to wait for the tunnel URL (default 30s)
888
+ * @returns A handle with the public hostname and a stop function
889
+ */
890
+ declare function startTunnel(port: number, timeoutMs?: number): Promise<TunnelHandle>;
891
+
892
+ export { type Agent, type AgentOptions, AuthenticationError, type Call, type CallControl, type CallEventHandler, type CallMetrics, CallMetricsAccumulator, type CallOptions, type CallRecord, type ConnectOptions, type CostBreakdown, type CreateAgentOptions, DEFAULT_PRICING, DeepgramSTT, ElevenLabsConvAIAdapter, ElevenLabsTTS, type Guardrail, type IncomingMessage, type LLMChunk, LLMLoop, type LLMProvider, type LatencyBreakdown, type LocalCallOptions, type LocalConfig, type LocalOptions, type Logger, type MessageHandler, MetricsStore, OpenAILLMProvider, OpenAIRealtimeAdapter, OpenAITTS, Patter, PatterConnectionError, PatterError, type PatterOptions, type PhoneNumber, type PipelineMessageHandler, type ProviderPricing, ProvisionError, RemoteMessageHandler, type SSEEvent, type STTConfig, type ServeOptions, type TTSConfig, TestSession, type ToolDefinition, type TunnelHandle, type TurnMetrics, WhisperSTT, calculateRealtimeCost, calculateSttCost, calculateTelephonyCost, calculateTtsCost, callsToCsv, callsToJson, deepgram, elevenlabs, getLogger, isRemoteUrl, isWebSocketUrl, makeAuthMiddleware, mergePricing, mountApi, mountDashboard, mulawToPcm16, openaiTts, pcm16ToMulaw, resample16kTo8k, resample24kTo16k, resample8kTo16k, setLogger, startTunnel, whisper };
package/dist/index.d.ts CHANGED
@@ -104,7 +104,7 @@ interface LocalOptions {
104
104
  twilioToken?: string;
105
105
  openaiKey?: string;
106
106
  phoneNumber: string;
107
- webhookUrl: string;
107
+ webhookUrl?: string;
108
108
  telephonyProvider?: 'twilio' | 'telnyx';
109
109
  telnyxKey?: string;
110
110
  telnyxConnectionId?: string;
@@ -148,6 +148,8 @@ type PipelineMessageHandler = (data: Record<string, unknown>) => Promise<string>
148
148
  interface ServeOptions {
149
149
  agent: AgentOptions;
150
150
  port?: number;
151
+ /** When true, start a cloudflared tunnel automatically (requires `cloudflared` npm package). */
152
+ tunnel?: boolean;
151
153
  onCallStart?: (data: Record<string, unknown>) => Promise<void>;
152
154
  onCallEnd?: (data: Record<string, unknown>) => Promise<void>;
153
155
  onTranscript?: (data: Record<string, unknown>) => Promise<void>;
@@ -206,6 +208,7 @@ declare class Patter {
206
208
  private readonly mode;
207
209
  private readonly localConfig;
208
210
  private embeddedServer;
211
+ private tunnelHandle;
209
212
  constructor(options: PatterOptions | LocalOptions);
210
213
  agent(opts: AgentOptions): AgentOptions;
211
214
  serve(opts: ServeOptions): Promise<void>;
@@ -863,4 +866,27 @@ declare function resample16kTo8k(pcm16k: Buffer): Buffer;
863
866
  */
864
867
  declare function resample24kTo16k(pcm24k: Buffer): Buffer;
865
868
 
866
- export { type Agent, type AgentOptions, AuthenticationError, type Call, type CallControl, type CallEventHandler, type CallMetrics, CallMetricsAccumulator, type CallOptions, type CallRecord, type ConnectOptions, type CostBreakdown, type CreateAgentOptions, DEFAULT_PRICING, DeepgramSTT, ElevenLabsConvAIAdapter, ElevenLabsTTS, type Guardrail, type IncomingMessage, type LLMChunk, LLMLoop, type LLMProvider, type LatencyBreakdown, type LocalCallOptions, type LocalConfig, type LocalOptions, type Logger, type MessageHandler, MetricsStore, OpenAILLMProvider, OpenAIRealtimeAdapter, OpenAITTS, Patter, PatterConnectionError, PatterError, type PatterOptions, type PhoneNumber, type PipelineMessageHandler, type ProviderPricing, ProvisionError, RemoteMessageHandler, type SSEEvent, type STTConfig, type ServeOptions, type TTSConfig, TestSession, type ToolDefinition, type TurnMetrics, WhisperSTT, calculateRealtimeCost, calculateSttCost, calculateTelephonyCost, calculateTtsCost, callsToCsv, callsToJson, deepgram, elevenlabs, getLogger, isRemoteUrl, isWebSocketUrl, makeAuthMiddleware, mergePricing, mountApi, mountDashboard, mulawToPcm16, openaiTts, pcm16ToMulaw, resample16kTo8k, resample24kTo16k, resample8kTo16k, setLogger, whisper };
869
+ /**
870
+ * Built-in tunnel support via cloudflared.
871
+ *
872
+ * Spawns a Cloudflare Quick Tunnel that exposes a local port to the internet.
873
+ * Zero account required — uses Cloudflare's free trycloudflare.com service.
874
+ *
875
+ * Install: npm install cloudflared
876
+ */
877
+ interface TunnelHandle {
878
+ /** Public hostname (no protocol), e.g. "random-name.trycloudflare.com" */
879
+ hostname: string;
880
+ /** Stop the tunnel process */
881
+ stop: () => void;
882
+ }
883
+ /**
884
+ * Start a cloudflared quick tunnel pointing to the given local port.
885
+ *
886
+ * @param port - Local port to tunnel to
887
+ * @param timeoutMs - How long to wait for the tunnel URL (default 30s)
888
+ * @returns A handle with the public hostname and a stop function
889
+ */
890
+ declare function startTunnel(port: number, timeoutMs?: number): Promise<TunnelHandle>;
891
+
892
+ export { type Agent, type AgentOptions, AuthenticationError, type Call, type CallControl, type CallEventHandler, type CallMetrics, CallMetricsAccumulator, type CallOptions, type CallRecord, type ConnectOptions, type CostBreakdown, type CreateAgentOptions, DEFAULT_PRICING, DeepgramSTT, ElevenLabsConvAIAdapter, ElevenLabsTTS, type Guardrail, type IncomingMessage, type LLMChunk, LLMLoop, type LLMProvider, type LatencyBreakdown, type LocalCallOptions, type LocalConfig, type LocalOptions, type Logger, type MessageHandler, MetricsStore, OpenAILLMProvider, OpenAIRealtimeAdapter, OpenAITTS, Patter, PatterConnectionError, PatterError, type PatterOptions, type PhoneNumber, type PipelineMessageHandler, type ProviderPricing, ProvisionError, RemoteMessageHandler, type SSEEvent, type STTConfig, type ServeOptions, type TTSConfig, TestSession, type ToolDefinition, type TunnelHandle, type TurnMetrics, WhisperSTT, calculateRealtimeCost, calculateSttCost, calculateTelephonyCost, calculateTtsCost, callsToCsv, callsToJson, deepgram, elevenlabs, getLogger, isRemoteUrl, isWebSocketUrl, makeAuthMiddleware, mergePricing, mountApi, mountDashboard, mulawToPcm16, openaiTts, pcm16ToMulaw, resample16kTo8k, resample24kTo16k, resample8kTo16k, setLogger, startTunnel, whisper };
package/dist/index.js CHANGED
@@ -277,6 +277,55 @@ var init_llm_loop = __esm({
277
277
  }
278
278
  });
279
279
 
280
+ // src/tunnel.ts
281
+ var tunnel_exports = {};
282
+ __export(tunnel_exports, {
283
+ startTunnel: () => startTunnel
284
+ });
285
+ async function startTunnel(port, timeoutMs = 3e4) {
286
+ let tunnelMod;
287
+ try {
288
+ tunnelMod = await import("cloudflared");
289
+ } catch {
290
+ throw new Error(
291
+ 'Built-in tunnel requires the "cloudflared" package. Install it with:\n\n npm install cloudflared\n\nOr provide your own webhookUrl instead of using tunnel: true.'
292
+ );
293
+ }
294
+ log.info("Starting tunnel to localhost:%d ...", port);
295
+ const result = tunnelMod.tunnel({
296
+ "--url": `http://localhost:${port}`
297
+ });
298
+ const tunnelUrl = await Promise.race([
299
+ result.url,
300
+ new Promise(
301
+ (_, reject) => setTimeout(() => reject(new Error(
302
+ `Tunnel failed to start within ${timeoutMs / 1e3}s. Check your internet connection or provide webhookUrl manually.`
303
+ )), timeoutMs)
304
+ )
305
+ ]);
306
+ const hostname = tunnelUrl.replace(/^https?:\/\//, "").replace(/\/$/, "");
307
+ log.info("Tunnel ready: https://%s", hostname);
308
+ result.connections.then(() => {
309
+ log.info("Tunnel connections established");
310
+ }).catch(() => {
311
+ });
312
+ return {
313
+ hostname,
314
+ stop: () => {
315
+ log.info("Stopping tunnel...");
316
+ result.stop();
317
+ }
318
+ };
319
+ }
320
+ var log;
321
+ var init_tunnel = __esm({
322
+ "src/tunnel.ts"() {
323
+ "use strict";
324
+ init_logger();
325
+ log = getLogger();
326
+ }
327
+ });
328
+
280
329
  // src/test-mode.ts
281
330
  var test_mode_exports = {};
282
331
  __export(test_mode_exports, {
@@ -296,19 +345,19 @@ var init_test_mode = __esm({
296
345
  const caller = "+15550000001";
297
346
  const callee = "+15550000002";
298
347
  const conversationHistory = [];
299
- const log = getLogger();
300
- log.info("");
301
- log.info("=".repeat(60));
302
- log.info(" PATTER TEST MODE");
303
- log.info("=".repeat(60));
304
- log.info(` Agent: ${agent.model || "default"} / ${agent.voice || "default"}`);
305
- log.info(` Provider: ${agent.provider || "openai_realtime"}`);
306
- log.info(` Call ID: ${callId}`);
307
- log.info(` Caller: ${caller} -> Callee: ${callee}`);
308
- log.info("-".repeat(60));
309
- log.info(" Commands: /quit /transfer <number> /hangup /history");
310
- log.info("=".repeat(60));
311
- log.info("");
348
+ const log2 = getLogger();
349
+ log2.info("");
350
+ log2.info("=".repeat(60));
351
+ log2.info(" PATTER TEST MODE");
352
+ log2.info("=".repeat(60));
353
+ log2.info(` Agent: ${agent.model || "default"} / ${agent.voice || "default"}`);
354
+ log2.info(` Provider: ${agent.provider || "openai_realtime"}`);
355
+ log2.info(` Call ID: ${callId}`);
356
+ log2.info(` Caller: ${caller} -> Callee: ${callee}`);
357
+ log2.info("-".repeat(60));
358
+ log2.info(" Commands: /quit /transfer <number> /hangup /history");
359
+ log2.info("=".repeat(60));
360
+ log2.info("");
312
361
  if (onCallStart) {
313
362
  await onCallStart({
314
363
  call_id: callId,
@@ -318,8 +367,8 @@ var init_test_mode = __esm({
318
367
  });
319
368
  }
320
369
  if (agent.firstMessage) {
321
- log.info(` Agent: ${agent.firstMessage}`);
322
- log.info("");
370
+ log2.info(` Agent: ${agent.firstMessage}`);
371
+ log2.info("");
323
372
  conversationHistory.push({
324
373
  role: "assistant",
325
374
  text: agent.firstMessage,
@@ -350,11 +399,11 @@ var init_test_mode = __esm({
350
399
  callee,
351
400
  transfer: async (number) => {
352
401
  ended = true;
353
- log.info(` [Transfer -> ${number}]`);
402
+ log2.info(` [Transfer -> ${number}]`);
354
403
  },
355
404
  hangup: async () => {
356
405
  ended = true;
357
- log.info(" [Call ended by agent]");
406
+ log2.info(" [Call ended by agent]");
358
407
  }
359
408
  };
360
409
  void _callControl;
@@ -369,25 +418,25 @@ var init_test_mode = __esm({
369
418
  try {
370
419
  userInput = await askQuestion(" You: ");
371
420
  } catch {
372
- log.info("\n [Session ended]");
421
+ log2.info("\n [Session ended]");
373
422
  break;
374
423
  }
375
424
  userInput = userInput.trim();
376
425
  if (!userInput) continue;
377
426
  if (userInput === "/quit") {
378
- log.info(" [Session ended]");
427
+ log2.info(" [Session ended]");
379
428
  break;
380
429
  } else if (userInput === "/hangup") {
381
- log.info(" [You hung up]");
430
+ log2.info(" [You hung up]");
382
431
  break;
383
432
  } else if (userInput.startsWith("/transfer ")) {
384
433
  const number = userInput.slice(10).trim();
385
- log.info(` [Transfer -> ${number}]`);
434
+ log2.info(` [Transfer -> ${number}]`);
386
435
  break;
387
436
  } else if (userInput === "/history") {
388
437
  for (const entry of conversationHistory) {
389
438
  const role = entry.role.charAt(0).toUpperCase() + entry.role.slice(1);
390
- log.info(` ${role}: ${entry.text}`);
439
+ log2.info(` ${role}: ${entry.text}`);
391
440
  }
392
441
  continue;
393
442
  }
@@ -405,16 +454,16 @@ var init_test_mode = __esm({
405
454
  history: [...conversationHistory]
406
455
  });
407
456
  if (responseText) {
408
- log.info(` Agent: ${responseText}`);
457
+ log2.info(` Agent: ${responseText}`);
409
458
  conversationHistory.push({
410
459
  role: "assistant",
411
460
  text: responseText,
412
461
  timestamp: Date.now()
413
462
  });
414
- log.info("");
463
+ log2.info("");
415
464
  }
416
465
  } catch (e) {
417
- log.error(` [Error: ${String(e)}]`);
466
+ log2.error(` [Error: ${String(e)}]`);
418
467
  }
419
468
  } else if (llmLoop) {
420
469
  const callCtx = { call_id: callId, caller, callee };
@@ -424,7 +473,7 @@ var init_test_mode = __esm({
424
473
  parts.push(token);
425
474
  process.stdout.write(token);
426
475
  }
427
- log.info("");
476
+ log2.info("");
428
477
  const responseText = parts.join("");
429
478
  if (responseText) {
430
479
  conversationHistory.push({
@@ -433,9 +482,9 @@ var init_test_mode = __esm({
433
482
  timestamp: Date.now()
434
483
  });
435
484
  }
436
- log.info("");
485
+ log2.info("");
437
486
  } else {
438
- log.info(" [No onMessage handler or LLM loop configured]");
487
+ log2.info(" [No onMessage handler or LLM loop configured]");
439
488
  }
440
489
  if (ended) break;
441
490
  }
@@ -499,6 +548,7 @@ __export(index_exports, {
499
548
  resample24kTo16k: () => resample24kTo16k,
500
549
  resample8kTo16k: () => resample8kTo16k,
501
550
  setLogger: () => setLogger,
551
+ startTunnel: () => startTunnel,
502
552
  whisper: () => whisper
503
553
  });
504
554
  module.exports = __toCommonJS(index_exports);
@@ -1623,10 +1673,10 @@ function esc(s) {
1623
1673
  if (!s) return '';
1624
1674
  return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;');
1625
1675
  }
1626
- function fmt\\$(v) { return v >= 0.01 ? '$'+v.toFixed(4) : v > 0 ? '$'+v.toFixed(6) : '$0.00'; }
1627
- function fmtMs(v) { return v > 0 ? Math.round(v)+'ms' : '-'; }
1676
+ function fmt$(v) { return v >= 0.01 ? '$'+v.toFixed(4) : v > 0 ? '$'+v.toFixed(6) : '$0.00'; }
1677
+ function fmtMs(v) { return v != null && v >= 0 ? Math.round(v)+'ms' : '-'; }
1628
1678
  function fmtDur(s) {
1629
- if (!s) return '-';
1679
+ if (s == null || s < 0) return '-';
1630
1680
  if (s < 60) return Math.round(s)+'s';
1631
1681
  return Math.floor(s/60)+'m '+Math.round(s%60)+'s';
1632
1682
  }
@@ -1641,10 +1691,10 @@ async function refreshAggregates() {
1641
1691
  const d = await fetchJSON('/api/dashboard/aggregates');
1642
1692
  $('#stat-total').textContent = d.total_calls;
1643
1693
  $('#stat-active').textContent = d.active_calls;
1644
- $('#stat-cost').textContent = fmt\\$(d.total_cost);
1694
+ $('#stat-cost').textContent = fmt$(d.total_cost);
1645
1695
  const cb = d.cost_breakdown;
1646
1696
  $('#stat-cost-breakdown').textContent =
1647
- 'STT '+fmt\\$(cb.stt)+' | LLM '+fmt\\$(cb.llm)+' | TTS '+fmt\\$(cb.tts)+' | Tel '+fmt\\$(cb.telephony);
1697
+ 'STT '+fmt$(cb.stt)+' | LLM '+fmt$(cb.llm)+' | TTS '+fmt$(cb.tts)+' | Tel '+fmt$(cb.telephony);
1648
1698
  $('#stat-duration').textContent = fmtDur(d.avg_duration);
1649
1699
  $('#stat-latency').textContent = fmtMs(d.avg_latency_ms);
1650
1700
  }
@@ -1669,7 +1719,7 @@ async function refreshCalls() {
1669
1719
  '<td>'+(esc(c.caller) || '-')+' &rarr; '+(esc(c.callee) || '-')+'</td>'+
1670
1720
  '<td>'+fmtDur(m.duration_seconds)+'</td>'+
1671
1721
  '<td><span class="badge '+modeClass+'">'+esc(mode)+'</span></td>'+
1672
- '<td class="cost">'+fmt\\$(cost.total || 0)+'</td>'+
1722
+ '<td class="cost">'+fmt$(cost.total || 0)+'</td>'+
1673
1723
  '<td class="latency">'+fmtMs(lat.total_ms || 0)+'</td>'+
1674
1724
  '<td>'+turns+'</td></tr>';
1675
1725
  }).join('');
@@ -1723,12 +1773,12 @@ async function showCall(callId) {
1723
1773
  '</div>'+
1724
1774
  '<div class="detail-card">'+
1725
1775
  '<h3>Cost Breakdown</h3>'+
1726
- '<div class="detail-row"><span class="k">STT</span><span class="cost">'+fmt\\$(cost.stt || 0)+'</span></div>'+
1727
- '<div class="detail-row"><span class="k">LLM</span><span class="cost">'+fmt\\$(cost.llm || 0)+'</span></div>'+
1728
- '<div class="detail-row"><span class="k">TTS</span><span class="cost">'+fmt\\$(cost.tts || 0)+'</span></div>'+
1729
- '<div class="detail-row"><span class="k">Telephony</span><span class="cost">'+fmt\\$(cost.telephony || 0)+'</span></div>'+
1776
+ '<div class="detail-row"><span class="k">STT</span><span class="cost">'+fmt$(cost.stt || 0)+'</span></div>'+
1777
+ '<div class="detail-row"><span class="k">LLM</span><span class="cost">'+fmt$(cost.llm || 0)+'</span></div>'+
1778
+ '<div class="detail-row"><span class="k">TTS</span><span class="cost">'+fmt$(cost.tts || 0)+'</span></div>'+
1779
+ '<div class="detail-row"><span class="k">Telephony</span><span class="cost">'+fmt$(cost.telephony || 0)+'</span></div>'+
1730
1780
  '<div class="detail-row" style="border-top:1px solid var(--border);padding-top:6px;margin-top:4px">'+
1731
- '<span class="k" style="font-weight:600">Total</span><span class="cost" style="font-weight:700">'+fmt\\$(cost.total || 0)+'</span>'+
1781
+ '<span class="k" style="font-weight:600">Total</span><span class="cost" style="font-weight:700">'+fmt$(cost.total || 0)+'</span>'+
1732
1782
  '</div>'+
1733
1783
  '<h3 style="margin-top:14px">Latency (avg / p95)</h3>'+
1734
1784
  '<div class="detail-row"><span class="k">STT</span><span class="latency">'+fmtMs(latAvg.stt_ms)+' / '+fmtMs(latP95.stt_ms)+'</span></div>'+
@@ -3387,7 +3437,7 @@ var TelnyxBridge = class {
3387
3437
  };
3388
3438
  var GRACEFUL_SHUTDOWN_TIMEOUT_MS = 1e4;
3389
3439
  var EmbeddedServer = class {
3390
- constructor(config, agent, onCallStart, onCallEnd, onTranscript, onMessage, recording = false, voicemailMessage = "", onMetrics, pricingOverrides, dashboard = false, dashboardToken = "") {
3440
+ constructor(config, agent, onCallStart, onCallEnd, onTranscript, onMessage, recording = false, voicemailMessage = "", onMetrics, pricingOverrides, dashboard = true, dashboardToken = "") {
3391
3441
  this.config = config;
3392
3442
  this.agent = agent;
3393
3443
  this.onCallStart = onCallStart;
@@ -3795,15 +3845,13 @@ var Patter = class {
3795
3845
  mode;
3796
3846
  localConfig;
3797
3847
  embeddedServer = null;
3848
+ tunnelHandle = null;
3798
3849
  constructor(options) {
3799
3850
  if ("mode" in options && options.mode === "local") {
3800
3851
  const local = options;
3801
3852
  if (!local.phoneNumber) {
3802
3853
  throw new Error("Local mode requires phoneNumber");
3803
3854
  }
3804
- if (!local.webhookUrl) {
3805
- throw new Error("Local mode requires webhookUrl (e.g., your ngrok URL)");
3806
- }
3807
3855
  if (!local.twilioSid && !local.telnyxKey) {
3808
3856
  throw new Error("Local mode requires twilioSid or telnyxKey");
3809
3857
  }
@@ -3867,13 +3915,28 @@ var Patter = class {
3867
3915
  if (opts.agent.provider && !validProviders.includes(opts.agent.provider)) {
3868
3916
  throw new Error(`agent.provider must be one of: ${validProviders.join(", ")}`);
3869
3917
  }
3918
+ let webhookUrl = this.localConfig.webhookUrl ?? "";
3919
+ const port = opts.port ?? 8e3;
3920
+ if (opts.tunnel && webhookUrl) {
3921
+ throw new Error("Cannot use both tunnel: true and webhookUrl. Pick one.");
3922
+ }
3923
+ if (opts.tunnel) {
3924
+ const { startTunnel: startTunnel2 } = await Promise.resolve().then(() => (init_tunnel(), tunnel_exports));
3925
+ this.tunnelHandle = await startTunnel2(port);
3926
+ webhookUrl = this.tunnelHandle.hostname;
3927
+ }
3928
+ if (!webhookUrl) {
3929
+ throw new Error(
3930
+ "No webhookUrl configured. Either:\n - Pass webhookUrl in the Patter constructor\n - Use tunnel: true in serve() to auto-create a tunnel"
3931
+ );
3932
+ }
3870
3933
  this.embeddedServer = new EmbeddedServer(
3871
3934
  {
3872
3935
  twilioSid: this.localConfig.twilioSid,
3873
3936
  twilioToken: this.localConfig.twilioToken,
3874
3937
  openaiKey: this.localConfig.openaiKey,
3875
3938
  phoneNumber: this.localConfig.phoneNumber,
3876
- webhookUrl: this.localConfig.webhookUrl,
3939
+ webhookUrl,
3877
3940
  telephonyProvider: this.localConfig.telephonyProvider,
3878
3941
  telnyxKey: this.localConfig.telnyxKey,
3879
3942
  telnyxConnectionId: this.localConfig.telnyxConnectionId,
@@ -3888,10 +3951,10 @@ var Patter = class {
3888
3951
  opts.voicemailMessage ?? "",
3889
3952
  opts.onMetrics,
3890
3953
  opts.pricing,
3891
- opts.dashboard ?? false,
3954
+ opts.dashboard ?? true,
3892
3955
  opts.dashboardToken ?? ""
3893
3956
  );
3894
- await this.embeddedServer.start(opts.port ?? 8e3);
3957
+ await this.embeddedServer.start(port);
3895
3958
  }
3896
3959
  async test(opts) {
3897
3960
  if (this.mode !== "local") {
@@ -4011,6 +4074,14 @@ var Patter = class {
4011
4074
  );
4012
4075
  }
4013
4076
  async disconnect() {
4077
+ if (this.tunnelHandle) {
4078
+ this.tunnelHandle.stop();
4079
+ this.tunnelHandle = null;
4080
+ }
4081
+ if (this.embeddedServer) {
4082
+ await this.embeddedServer.stop();
4083
+ this.embeddedServer = null;
4084
+ }
4014
4085
  await this.connection.disconnect();
4015
4086
  }
4016
4087
  // === Agent Management ===
@@ -4241,6 +4312,9 @@ function resample24kTo16k(pcm24k) {
4241
4312
  }
4242
4313
  return out;
4243
4314
  }
4315
+
4316
+ // src/index.ts
4317
+ init_tunnel();
4244
4318
  // Annotate the CommonJS export names for ESM import in node:
4245
4319
  0 && (module.exports = {
4246
4320
  AuthenticationError,
@@ -4283,5 +4357,6 @@ function resample24kTo16k(pcm24k) {
4283
4357
  resample24kTo16k,
4284
4358
  resample8kTo16k,
4285
4359
  setLogger,
4360
+ startTunnel,
4286
4361
  whisper
4287
4362
  });
package/dist/index.mjs CHANGED
@@ -1,10 +1,15 @@
1
+ import {
2
+ startTunnel
3
+ } from "./chunk-VNU4GNW3.mjs";
1
4
  import {
2
5
  LLMLoop,
3
6
  OpenAILLMProvider,
4
- TestSession,
7
+ TestSession
8
+ } from "./chunk-TAATEHKF.mjs";
9
+ import {
5
10
  getLogger,
6
11
  setLogger
7
- } from "./chunk-KB57IV4K.mjs";
12
+ } from "./chunk-FMNRCP5X.mjs";
8
13
 
9
14
  // src/connection.ts
10
15
  import WebSocket from "ws";
@@ -1122,10 +1127,10 @@ function esc(s) {
1122
1127
  if (!s) return '';
1123
1128
  return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;');
1124
1129
  }
1125
- function fmt\\$(v) { return v >= 0.01 ? '$'+v.toFixed(4) : v > 0 ? '$'+v.toFixed(6) : '$0.00'; }
1126
- function fmtMs(v) { return v > 0 ? Math.round(v)+'ms' : '-'; }
1130
+ function fmt$(v) { return v >= 0.01 ? '$'+v.toFixed(4) : v > 0 ? '$'+v.toFixed(6) : '$0.00'; }
1131
+ function fmtMs(v) { return v != null && v >= 0 ? Math.round(v)+'ms' : '-'; }
1127
1132
  function fmtDur(s) {
1128
- if (!s) return '-';
1133
+ if (s == null || s < 0) return '-';
1129
1134
  if (s < 60) return Math.round(s)+'s';
1130
1135
  return Math.floor(s/60)+'m '+Math.round(s%60)+'s';
1131
1136
  }
@@ -1140,10 +1145,10 @@ async function refreshAggregates() {
1140
1145
  const d = await fetchJSON('/api/dashboard/aggregates');
1141
1146
  $('#stat-total').textContent = d.total_calls;
1142
1147
  $('#stat-active').textContent = d.active_calls;
1143
- $('#stat-cost').textContent = fmt\\$(d.total_cost);
1148
+ $('#stat-cost').textContent = fmt$(d.total_cost);
1144
1149
  const cb = d.cost_breakdown;
1145
1150
  $('#stat-cost-breakdown').textContent =
1146
- 'STT '+fmt\\$(cb.stt)+' | LLM '+fmt\\$(cb.llm)+' | TTS '+fmt\\$(cb.tts)+' | Tel '+fmt\\$(cb.telephony);
1151
+ 'STT '+fmt$(cb.stt)+' | LLM '+fmt$(cb.llm)+' | TTS '+fmt$(cb.tts)+' | Tel '+fmt$(cb.telephony);
1147
1152
  $('#stat-duration').textContent = fmtDur(d.avg_duration);
1148
1153
  $('#stat-latency').textContent = fmtMs(d.avg_latency_ms);
1149
1154
  }
@@ -1168,7 +1173,7 @@ async function refreshCalls() {
1168
1173
  '<td>'+(esc(c.caller) || '-')+' &rarr; '+(esc(c.callee) || '-')+'</td>'+
1169
1174
  '<td>'+fmtDur(m.duration_seconds)+'</td>'+
1170
1175
  '<td><span class="badge '+modeClass+'">'+esc(mode)+'</span></td>'+
1171
- '<td class="cost">'+fmt\\$(cost.total || 0)+'</td>'+
1176
+ '<td class="cost">'+fmt$(cost.total || 0)+'</td>'+
1172
1177
  '<td class="latency">'+fmtMs(lat.total_ms || 0)+'</td>'+
1173
1178
  '<td>'+turns+'</td></tr>';
1174
1179
  }).join('');
@@ -1222,12 +1227,12 @@ async function showCall(callId) {
1222
1227
  '</div>'+
1223
1228
  '<div class="detail-card">'+
1224
1229
  '<h3>Cost Breakdown</h3>'+
1225
- '<div class="detail-row"><span class="k">STT</span><span class="cost">'+fmt\\$(cost.stt || 0)+'</span></div>'+
1226
- '<div class="detail-row"><span class="k">LLM</span><span class="cost">'+fmt\\$(cost.llm || 0)+'</span></div>'+
1227
- '<div class="detail-row"><span class="k">TTS</span><span class="cost">'+fmt\\$(cost.tts || 0)+'</span></div>'+
1228
- '<div class="detail-row"><span class="k">Telephony</span><span class="cost">'+fmt\\$(cost.telephony || 0)+'</span></div>'+
1230
+ '<div class="detail-row"><span class="k">STT</span><span class="cost">'+fmt$(cost.stt || 0)+'</span></div>'+
1231
+ '<div class="detail-row"><span class="k">LLM</span><span class="cost">'+fmt$(cost.llm || 0)+'</span></div>'+
1232
+ '<div class="detail-row"><span class="k">TTS</span><span class="cost">'+fmt$(cost.tts || 0)+'</span></div>'+
1233
+ '<div class="detail-row"><span class="k">Telephony</span><span class="cost">'+fmt$(cost.telephony || 0)+'</span></div>'+
1229
1234
  '<div class="detail-row" style="border-top:1px solid var(--border);padding-top:6px;margin-top:4px">'+
1230
- '<span class="k" style="font-weight:600">Total</span><span class="cost" style="font-weight:700">'+fmt\\$(cost.total || 0)+'</span>'+
1235
+ '<span class="k" style="font-weight:600">Total</span><span class="cost" style="font-weight:700">'+fmt$(cost.total || 0)+'</span>'+
1231
1236
  '</div>'+
1232
1237
  '<h3 style="margin-top:14px">Latency (avg / p95)</h3>'+
1233
1238
  '<div class="detail-row"><span class="k">STT</span><span class="latency">'+fmtMs(latAvg.stt_ms)+' / '+fmtMs(latP95.stt_ms)+'</span></div>'+
@@ -2879,7 +2884,7 @@ var TelnyxBridge = class {
2879
2884
  };
2880
2885
  var GRACEFUL_SHUTDOWN_TIMEOUT_MS = 1e4;
2881
2886
  var EmbeddedServer = class {
2882
- constructor(config, agent, onCallStart, onCallEnd, onTranscript, onMessage, recording = false, voicemailMessage = "", onMetrics, pricingOverrides, dashboard = false, dashboardToken = "") {
2887
+ constructor(config, agent, onCallStart, onCallEnd, onTranscript, onMessage, recording = false, voicemailMessage = "", onMetrics, pricingOverrides, dashboard = true, dashboardToken = "") {
2883
2888
  this.config = config;
2884
2889
  this.agent = agent;
2885
2890
  this.onCallStart = onCallStart;
@@ -3287,15 +3292,13 @@ var Patter = class {
3287
3292
  mode;
3288
3293
  localConfig;
3289
3294
  embeddedServer = null;
3295
+ tunnelHandle = null;
3290
3296
  constructor(options) {
3291
3297
  if ("mode" in options && options.mode === "local") {
3292
3298
  const local = options;
3293
3299
  if (!local.phoneNumber) {
3294
3300
  throw new Error("Local mode requires phoneNumber");
3295
3301
  }
3296
- if (!local.webhookUrl) {
3297
- throw new Error("Local mode requires webhookUrl (e.g., your ngrok URL)");
3298
- }
3299
3302
  if (!local.twilioSid && !local.telnyxKey) {
3300
3303
  throw new Error("Local mode requires twilioSid or telnyxKey");
3301
3304
  }
@@ -3359,13 +3362,28 @@ var Patter = class {
3359
3362
  if (opts.agent.provider && !validProviders.includes(opts.agent.provider)) {
3360
3363
  throw new Error(`agent.provider must be one of: ${validProviders.join(", ")}`);
3361
3364
  }
3365
+ let webhookUrl = this.localConfig.webhookUrl ?? "";
3366
+ const port = opts.port ?? 8e3;
3367
+ if (opts.tunnel && webhookUrl) {
3368
+ throw new Error("Cannot use both tunnel: true and webhookUrl. Pick one.");
3369
+ }
3370
+ if (opts.tunnel) {
3371
+ const { startTunnel: startTunnel2 } = await import("./tunnel-HYSU7EF2.mjs");
3372
+ this.tunnelHandle = await startTunnel2(port);
3373
+ webhookUrl = this.tunnelHandle.hostname;
3374
+ }
3375
+ if (!webhookUrl) {
3376
+ throw new Error(
3377
+ "No webhookUrl configured. Either:\n - Pass webhookUrl in the Patter constructor\n - Use tunnel: true in serve() to auto-create a tunnel"
3378
+ );
3379
+ }
3362
3380
  this.embeddedServer = new EmbeddedServer(
3363
3381
  {
3364
3382
  twilioSid: this.localConfig.twilioSid,
3365
3383
  twilioToken: this.localConfig.twilioToken,
3366
3384
  openaiKey: this.localConfig.openaiKey,
3367
3385
  phoneNumber: this.localConfig.phoneNumber,
3368
- webhookUrl: this.localConfig.webhookUrl,
3386
+ webhookUrl,
3369
3387
  telephonyProvider: this.localConfig.telephonyProvider,
3370
3388
  telnyxKey: this.localConfig.telnyxKey,
3371
3389
  telnyxConnectionId: this.localConfig.telnyxConnectionId,
@@ -3380,16 +3398,16 @@ var Patter = class {
3380
3398
  opts.voicemailMessage ?? "",
3381
3399
  opts.onMetrics,
3382
3400
  opts.pricing,
3383
- opts.dashboard ?? false,
3401
+ opts.dashboard ?? true,
3384
3402
  opts.dashboardToken ?? ""
3385
3403
  );
3386
- await this.embeddedServer.start(opts.port ?? 8e3);
3404
+ await this.embeddedServer.start(port);
3387
3405
  }
3388
3406
  async test(opts) {
3389
3407
  if (this.mode !== "local") {
3390
3408
  throw new Error("test() is only available in local mode");
3391
3409
  }
3392
- const { TestSession: TestSession2 } = await import("./test-mode-RTQAK5CP.mjs");
3410
+ const { TestSession: TestSession2 } = await import("./test-mode-JMXZSAJS.mjs");
3393
3411
  const session = new TestSession2();
3394
3412
  await session.run({
3395
3413
  agent: opts.agent,
@@ -3503,6 +3521,14 @@ var Patter = class {
3503
3521
  );
3504
3522
  }
3505
3523
  async disconnect() {
3524
+ if (this.tunnelHandle) {
3525
+ this.tunnelHandle.stop();
3526
+ this.tunnelHandle = null;
3527
+ }
3528
+ if (this.embeddedServer) {
3529
+ await this.embeddedServer.stop();
3530
+ this.embeddedServer = null;
3531
+ }
3506
3532
  await this.connection.disconnect();
3507
3533
  }
3508
3534
  // === Agent Management ===
@@ -3769,5 +3795,6 @@ export {
3769
3795
  resample24kTo16k,
3770
3796
  resample8kTo16k,
3771
3797
  setLogger,
3798
+ startTunnel,
3772
3799
  whisper
3773
3800
  };
@@ -0,0 +1,7 @@
1
+ import {
2
+ TestSession
3
+ } from "./chunk-TAATEHKF.mjs";
4
+ import "./chunk-FMNRCP5X.mjs";
5
+ export {
6
+ TestSession
7
+ };
@@ -0,0 +1,7 @@
1
+ import {
2
+ startTunnel
3
+ } from "./chunk-VNU4GNW3.mjs";
4
+ import "./chunk-FMNRCP5X.mjs";
5
+ export {
6
+ startTunnel
7
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "getpatter",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "description": "Connect AI agents to phone numbers with 10 lines of code",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -54,13 +54,19 @@
54
54
  "express": "^5.2.1",
55
55
  "ws": "^8.18.0"
56
56
  },
57
+ "optionalDependencies": {
58
+ "cloudflared": "^1.0.0"
59
+ },
57
60
  "devDependencies": {
61
+ "@playwright/test": "^1.59.1",
58
62
  "@types/express": "^5.0.6",
59
63
  "@types/ws": "^8.5.0",
64
+ "@vitest/coverage-v8": "^3.2.4",
65
+ "playwright": "^1.59.1",
60
66
  "tsup": "^8.0.0",
61
67
  "tsx": "^4.21.0",
62
68
  "typescript": "^5.4.0",
63
- "vitest": "^2.0.0"
69
+ "vitest": "^3.2.4"
64
70
  },
65
71
  "engines": {
66
72
  "node": ">=18.0.0"
@@ -1,6 +0,0 @@
1
- import {
2
- TestSession
3
- } from "./chunk-KB57IV4K.mjs";
4
- export {
5
- TestSession
6
- };