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.
- package/dist/chunk-FMNRCP5X.mjs +20 -0
- package/dist/{chunk-KB57IV4K.mjs → chunk-TAATEHKF.mjs} +4 -18
- package/dist/chunk-VNU4GNW3.mjs +45 -0
- package/dist/index.d.mts +28 -2
- package/dist/index.d.ts +28 -2
- package/dist/index.js +121 -46
- package/dist/index.mjs +48 -21
- package/dist/test-mode-JMXZSAJS.mjs +7 -0
- package/dist/tunnel-HYSU7EF2.mjs +7 -0
- package/package.json +8 -2
- package/dist/test-mode-RTQAK5CP.mjs +0 -6
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
|
|
322
|
-
|
|
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
|
-
|
|
402
|
+
log2.info(` [Transfer -> ${number}]`);
|
|
354
403
|
},
|
|
355
404
|
hangup: async () => {
|
|
356
405
|
ended = true;
|
|
357
|
-
|
|
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
|
-
|
|
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
|
-
|
|
427
|
+
log2.info(" [Session ended]");
|
|
379
428
|
break;
|
|
380
429
|
} else if (userInput === "/hangup") {
|
|
381
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
463
|
+
log2.info("");
|
|
415
464
|
}
|
|
416
465
|
} catch (e) {
|
|
417
|
-
|
|
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
|
-
|
|
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
|
-
|
|
485
|
+
log2.info("");
|
|
437
486
|
} else {
|
|
438
|
-
|
|
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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''');
|
|
1625
1675
|
}
|
|
1626
|
-
function fmt
|
|
1627
|
-
function fmtMs(v) { return v
|
|
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 (
|
|
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
|
|
1694
|
+
$('#stat-cost').textContent = fmt$(d.total_cost);
|
|
1645
1695
|
const cb = d.cost_breakdown;
|
|
1646
1696
|
$('#stat-cost-breakdown').textContent =
|
|
1647
|
-
'STT '+fmt
|
|
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) || '-')+' → '+(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
|
|
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
|
|
1727
|
-
'<div class="detail-row"><span class="k">LLM</span><span class="cost">'+fmt
|
|
1728
|
-
'<div class="detail-row"><span class="k">TTS</span><span class="cost">'+fmt
|
|
1729
|
-
'<div class="detail-row"><span class="k">Telephony</span><span class="cost">'+fmt
|
|
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
|
|
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 =
|
|
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
|
|
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 ??
|
|
3954
|
+
opts.dashboard ?? true,
|
|
3892
3955
|
opts.dashboardToken ?? ""
|
|
3893
3956
|
);
|
|
3894
|
-
await this.embeddedServer.start(
|
|
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-
|
|
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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''');
|
|
1124
1129
|
}
|
|
1125
|
-
function fmt
|
|
1126
|
-
function fmtMs(v) { return v
|
|
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 (
|
|
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
|
|
1148
|
+
$('#stat-cost').textContent = fmt$(d.total_cost);
|
|
1144
1149
|
const cb = d.cost_breakdown;
|
|
1145
1150
|
$('#stat-cost-breakdown').textContent =
|
|
1146
|
-
'STT '+fmt
|
|
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) || '-')+' → '+(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
|
|
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
|
|
1226
|
-
'<div class="detail-row"><span class="k">LLM</span><span class="cost">'+fmt
|
|
1227
|
-
'<div class="detail-row"><span class="k">TTS</span><span class="cost">'+fmt
|
|
1228
|
-
'<div class="detail-row"><span class="k">Telephony</span><span class="cost">'+fmt
|
|
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
|
|
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 =
|
|
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
|
|
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 ??
|
|
3401
|
+
opts.dashboard ?? true,
|
|
3384
3402
|
opts.dashboardToken ?? ""
|
|
3385
3403
|
);
|
|
3386
|
-
await this.embeddedServer.start(
|
|
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-
|
|
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
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "getpatter",
|
|
3
|
-
"version": "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.
|
|
69
|
+
"vitest": "^3.2.4"
|
|
64
70
|
},
|
|
65
71
|
"engines": {
|
|
66
72
|
"node": ">=18.0.0"
|