@guava-ai/guava-sdk 0.3.0 → 0.4.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.
Files changed (57) hide show
  1. package/bin/example-runner.js +16 -7
  2. package/dist/examples/credit-card-activation.js +94 -112
  3. package/dist/examples/credit-card-activation.js.map +1 -1
  4. package/dist/examples/property-insurance.js +12 -26
  5. package/dist/examples/property-insurance.js.map +1 -1
  6. package/dist/examples/scheduling-outbound.d.ts +1 -0
  7. package/dist/examples/scheduling-outbound.js +62 -0
  8. package/dist/examples/scheduling-outbound.js.map +1 -0
  9. package/dist/examples/thai-palace.js +25 -43
  10. package/dist/examples/thai-palace.js.map +1 -1
  11. package/dist/package.json +7 -5
  12. package/dist/src/action_item.d.ts +34 -12
  13. package/dist/src/action_item.js +34 -7
  14. package/dist/src/action_item.js.map +1 -1
  15. package/dist/src/call-controller.d.ts +137 -0
  16. package/dist/src/call-controller.js +433 -0
  17. package/dist/src/call-controller.js.map +1 -0
  18. package/dist/src/commands.d.ts +67 -27
  19. package/dist/src/commands.js +41 -27
  20. package/dist/src/commands.js.map +1 -1
  21. package/dist/src/events.d.ts +47 -30
  22. package/dist/src/events.js +42 -36
  23. package/dist/src/events.js.map +1 -1
  24. package/dist/src/example_data.d.ts +1 -0
  25. package/dist/src/example_data.js +33 -0
  26. package/dist/src/example_data.js.map +1 -1
  27. package/dist/src/helpers/openai.d.ts +12 -1
  28. package/dist/src/helpers/openai.js +168 -68
  29. package/dist/src/helpers/openai.js.map +1 -1
  30. package/dist/src/index.d.ts +6 -121
  31. package/dist/src/index.js +249 -483
  32. package/dist/src/index.js.map +1 -1
  33. package/dist/src/logging.d.ts +2 -1
  34. package/dist/src/logging.js +32 -7
  35. package/dist/src/logging.js.map +1 -1
  36. package/dist/src/telemetry.d.ts +23 -0
  37. package/dist/src/telemetry.js +98 -0
  38. package/dist/src/telemetry.js.map +1 -0
  39. package/dist/src/utils.d.ts +3 -0
  40. package/dist/src/utils.js +28 -0
  41. package/dist/src/utils.js.map +1 -0
  42. package/examples/biome.json +5 -0
  43. package/examples/credit-card-activation.ts +20 -26
  44. package/examples/property-insurance.ts +6 -16
  45. package/examples/scheduling-outbound.ts +80 -0
  46. package/examples/thai-palace.ts +10 -13
  47. package/package.json +7 -5
  48. package/src/action_item.ts +53 -13
  49. package/src/call-controller.ts +451 -0
  50. package/src/commands.ts +58 -42
  51. package/src/events.ts +66 -51
  52. package/src/example_data.ts +42 -0
  53. package/src/helpers/openai.ts +73 -18
  54. package/src/index.ts +81 -403
  55. package/src/logging.ts +39 -7
  56. package/src/telemetry.ts +125 -0
  57. package/src/utils.ts +32 -0
package/src/index.ts CHANGED
@@ -1,358 +1,37 @@
1
1
  import WebSocket from "ws";
2
- import { type Logger, getConsoleLogger } from "./logging.ts";
2
+ import { type Logger, getDefaultLogger } from "./logging.ts";
3
3
  import {
4
- acceptInboundCallCommand,
5
- type Command,
6
- startOutboundCallCommand,
7
- setPersona,
8
- setTaskCommand,
9
- answerQuestionCommand,
10
- sendInstructionCommand,
11
- readScriptCommand,
12
- rejectInboundCallCommand,
13
- transferCommand,
14
- listenInboundCommand,
15
- inboundTunnelCommand,
4
+ StartOutboundCallCommand,
5
+ ListenInboundCommand,
6
+ InboundTunnelCommand,
16
7
  } from "./commands.ts";
17
8
  import * as z from "zod";
18
- import {
19
- errorEvent,
20
- type GuavaEvent,
21
- sessionStartedEvent,
22
- decodeEvent,
23
- type CallerSpeechEvent,
24
- type AgentSpeechEvent,
25
- inboundTunnelEvent,
26
- } from "./events.ts";
27
- import type { ActionItem, Field, Say, Todo } from "./action_item.ts";
9
+ import { ErrorEvent, SessionStartedEvent, decodeEvent, InboundTunnelEvent } from "./events.ts";
28
10
  import pkgdata from "../package.json" with { type: "json" };
29
11
  import os from "node:os";
12
+ import { getBaseUrl, fetchOrThrow } from "./utils.ts";
13
+ import { telemetryClient } from "./telemetry.ts";
14
+ import type { CallController } from "./call-controller.ts";
15
+ export { CallController, type TaskObjective } from "./call-controller.ts";
16
+ export { Say, Field } from "./action_item.ts";
17
+
18
+ const SDK_NAME = "typescript-sdk";
30
19
 
31
- const DEFAULT_BASE_URL = "https://guava-dev.gridspace.com/";
32
- const DEFAULT_LOG_LEVEL = "debug";
20
+ let firstClient = false;
33
21
 
34
22
  /**
35
23
  * @description convenience function for stringifying data according to a schema
36
24
  */
37
- function stringifyZod<Schema extends z.ZodType>(
38
- schema: Schema,
39
- data: z.input<Schema>,
40
- ): string {
25
+ function stringifyZod<Schema extends z.ZodType>(schema: Schema, data: z.input<Schema>): string {
41
26
  return JSON.stringify(schema.parse(data));
42
27
  }
43
28
 
44
- /**
45
- * Interface between Guava services and user-supplied code
46
- */
47
- export class CallController {
48
- private _commandQueue: Command[] = [];
49
- private _on_complete_current_task?: () => void;
50
- // private _field_values: Record<string, any>;
51
- private _current_task_id?: string;
52
- /**
53
- * @protected
54
- * @description logger used to emit diagnostics
55
- */
56
- protected logger: Logger;
57
- // drain functions are expected to cleanup
58
- // the part of the queue that is successfully sent from its
59
- // input (mutating it) (i.e. _drain should use Array.splice)
60
- private _drain?: (_: Command[]) => Promise<void>;
61
- private _fieldValues: Record<string, unknown> = {};
62
-
63
- constructor(logger: Logger) {
64
- // Set up the default logger.
65
- this.logger = logger;
66
- }
67
-
68
- /**
69
- * @description Supply a function used to consume commands from the internal command queue.
70
- *
71
- * The function is expected to remove from the argument array commands that it has handled (iterating
72
- * through the result of `Array.splice(0)` is sufficient)
73
- */
74
- setDrain(newDrain: (_: Command[]) => Promise<void>) {
75
- this._drain = newDrain;
76
- this.flush();
77
- }
78
-
79
- /**
80
- * @description [inbound] receive a call, and process further.
81
- */
82
- protected async acceptCall() {
83
- await this.sendCommand(acceptInboundCallCommand, {
84
- command_type: "accept-inbound",
85
- });
86
- }
87
-
88
- /**
89
- * @description read a span of text verbatim
90
- */
91
- protected async readScript(script: string) {
92
- await this.sendCommand(readScriptCommand, {
93
- command_type: "read-script",
94
- script: script,
95
- });
96
- }
97
-
98
- /**
99
- * @description [inbound] reject a call
100
- */
101
- protected async rejectCall() {
102
- await this.sendCommand(rejectInboundCallCommand, {
103
- command_type: "reject-inbound",
104
- });
105
- }
106
-
107
- protected async addInfo(_info: string) {
108
- throw new Error("not implemeneted");
109
- }
110
-
111
- /**
112
- * @description read a span of text non-verbatim
113
- */
114
- protected async sendInstruction(instruction: string) {
115
- await this.sendCommand(sendInstructionCommand, {
116
- command_type: "send-instruction",
117
- instruction: instruction,
118
- });
119
- }
120
-
121
- /**
122
- * @description provide identifiers the agent will use to identify the virtual agent
123
- */
124
- protected async setPersona(args: {
125
- organizationName?: string;
126
- agentName?: string;
127
- agentPurpose?: string;
128
- }) {
129
- await this.sendCommand(setPersona, {
130
- command_type: "set-persona",
131
- organization_name: args.organizationName,
132
- agent_name: args.agentName,
133
- agent_purpose: args.agentPurpose,
134
- });
135
- }
136
-
137
- /**
138
- * @description direct the agent to collect information
139
- * @param goal {} an objective string and/or a checklist of information to collect
140
- * @param on_complete {} a callback to call once the information is available from the agent
141
- * @param args {} arguments to pass through to the `on_complete` callback
142
- */
143
- protected setTask(
144
- goal: TaskObjective,
145
- on_complete: (...c: any[]) => void = () => {},
146
- ...args: any[]
147
- ) {
148
- this._current_task_id = Math.random().toString(16).substring(2, 8);
149
- this._on_complete_current_task = on_complete.bind(this, ...args);
150
- if (!("checklist" in goal)) {
151
- this.sendCommand(setTaskCommand, {
152
- command_type: "set-task",
153
- task_id: this._current_task_id,
154
- objective: goal.objective,
155
- action_items: [],
156
- });
157
- } else {
158
- const action_items = goal.checklist.map((item) =>
159
- typeof item == "string"
160
- ? ({
161
- item_type: "todo",
162
- description: item,
163
- } satisfies Todo)
164
- : item,
165
- ) satisfies ActionItem[];
166
- this.sendCommand(setTaskCommand, {
167
- command_type: "set-task",
168
- task_id: this._current_task_id,
169
- objective: goal.objective ?? "",
170
- action_items,
171
- });
172
- }
173
- }
174
-
175
- /**
176
- * @description direct the agent to collect information, continuing execution once the agent has collected the information
177
- * @param goal {} an objective string and/or a checklist of information to collect
178
- */
179
- protected async awaitTask(goal: TaskObjective): Promise<void> {
180
- return new Promise((resolve) => {
181
- this.setTask(goal, (_args) => {
182
- resolve();
183
- });
184
- });
185
- }
186
-
187
- /**
188
- * @description retrieve a piece of information that the agent has collected
189
- * @param key {string} key of the field checklist item
190
- */
191
- protected getField(key: string) {
192
- if (key in this._fieldValues) {
193
- return this._fieldValues[key];
194
- } else {
195
- return null;
196
- }
197
- }
198
-
199
- /**
200
- * @description [inbound] hang up an accepted call
201
- */
202
- protected async hangup(final_instructions: string = "") {
203
- let instructions: string;
204
- if (final_instructions) {
205
- instructions = `Start ending the conversation. Here are your final instructions: ${final_instructions} Once you've completed the final instructions, naturally end the conversation and hang up the call.`;
206
- } else {
207
- instructions = "Naturally end the conversation and hang up the call.";
208
- }
209
-
210
- this.sendInstruction(instructions);
211
- }
212
-
213
- /**
214
- * @description transfer an accepted call
215
- */
216
- protected transfer(to_number: string, transfer_message?: string) {
217
- const message = transfer_message ?? "I'm transferring you now";
218
- this.sendCommand(transferCommand, {
219
- command_type: "transfer-call",
220
- transfer_message: message,
221
- to_number: to_number,
222
- });
223
- }
224
-
225
- private async sendCommand<C extends Command, Schema extends z.ZodType<C>>(
226
- schema: Schema,
227
- data: z.input<Schema>,
228
- ) {
229
- const command = schema.parse(data);
230
- this._commandQueue.push(command);
231
- this.logger.debug(`Command queued: ${JSON.stringify(command)}`);
232
- await this.flush();
233
- }
234
-
235
- private async flush() {
236
- await this._drain?.call(this, this._commandQueue);
237
- }
238
-
239
- async onEvent(event: GuavaEvent) {
240
- if (event.event_type == "caller-speech") {
241
- this.onCallerSpeech(event);
242
- } else if (event.event_type == "agent-speech") {
243
- this.onAgentSpeech(event);
244
- } else if (event.event_type == "agent-question") {
245
- try {
246
- this.logger.info(`Received question from bot: ${event.question}`);
247
- const answer = await this.onQuestion(event.question);
248
- await this.sendCommand(answerQuestionCommand, {
249
- command_type: "answer-question",
250
- question_id: event.question_id,
251
- answer: answer,
252
- });
253
- } catch (e) {
254
- this.logger.error("Error occured while answering question.");
255
- await this.sendCommand(answerQuestionCommand, {
256
- command_type: "answer-question",
257
- question_id: event.question_id,
258
- answer: "An error occured and the question could not be answered.",
259
- });
260
- }
261
- } else if (event.event_type == "intent") {
262
- this.logger.info(
263
- `Received intent ${event.intent_id} from bot: ${event.intent_summary}`,
264
- );
265
- const intent_response = await this.onIntent(event.intent_summary);
266
- if (intent_response) {
267
- const response_str = `Responding to intent ${event.intent_id}: ${intent_response}`;
268
- this.logger.info(response_str);
269
- this.sendInstruction(intent_response);
270
- }
271
- } else if (event.event_type == "task-done") {
272
- // ignore obsolete task_completed events
273
- if (event.task_id == this._current_task_id) {
274
- // assertion is implied
275
- const on_complete = this._on_complete_current_task;
276
- this._on_complete_current_task = undefined;
277
- if (on_complete) {
278
- on_complete();
279
- }
280
- }
281
- } else if (event.event_type == "action-item-done") {
282
- this._fieldValues[event.key] = event.payload;
283
- if (event.key && event.payload) {
284
- this.logger.info(
285
- `Field ${event.key} updated with value: ${event.payload}`,
286
- );
287
- }
288
- } else if (event.event_type == "inbound-call") {
289
- this.onIncomingCall(event.caller_number);
290
- } else if (
291
- event.event_type == "outbound-call-connected" ||
292
- event.event_type == "bot-session-ended"
293
- ) {
294
- // no-op, don't warn
295
- } else if (event.event_type == "error") {
296
- this.logger.error(`The Guava agent reported an error: ${event.content}`);
297
- } else {
298
- this.logger.warn(`Unhandled event: ${JSON.stringify(event)}`);
299
- }
300
- }
301
-
302
- // callbacks
303
-
304
- /**
305
- * @abstract
306
- * @description called when an inbound call is received. The overriding function must start
307
- * with `await super.onIncomingCall(from_number)`
308
- */
309
- async onIncomingCall(from_number?: string) {
310
- await this.onCallStart();
311
- }
312
-
313
- /**
314
- * @abstract
315
- * @description called when a call is connected by the API, whether inbound or outbound
316
- */
317
- async onCallStart(): Promise<void> {}
318
-
319
- /**
320
- * @abstract
321
- * @description called when the caller speaks to the agent.
322
- */
323
- async onCallerSpeech(event: CallerSpeechEvent) {}
324
- /**
325
- * @abstract
326
- * @description called when the agent speaks to the caller.
327
- */
328
- async onAgentSpeech(event: AgentSpeechEvent) {}
329
- /**
330
- * @abstract
331
- * @description called when the caller expresses a task they wish to execute
332
- */
333
- async onIntent(intent: string): Promise<string | null> {
334
- return "Unfortunately I'm not able to help with that.";
335
- }
336
- /**
337
- * @abstract
338
- * @description called when the agent needs to respond to a question that it doesn't know
339
- * the answer to.
340
- */
341
- async onQuestion(question: string): Promise<string> {
342
- return "I don't have an answer to that question.";
343
- }
344
- }
345
-
346
- export type InboundConnection =
347
- | { agent_number: string }
348
- | { webrtc_code: string };
349
-
350
- export type TaskObjective =
351
- | { objective: string }
352
- | { objective?: string; checklist: (Field | Say | string)[] };
29
+ export type InboundConnection = { agent_number: string } | { webrtc_code: string };
353
30
 
354
31
  const http_start = /^http:\/\//;
355
32
  const https_start = /^https:\/\//;
33
+
34
+ @telemetryClient.trackClass()
356
35
  export class Client {
357
36
  private _apiKey: string;
358
37
  private _baseUrl: string;
@@ -361,21 +40,19 @@ export class Client {
361
40
  private _controller?: CallController;
362
41
  private messageHandler?: (_: WebSocket.MessageEvent) => void;
363
42
 
364
- constructor(apiKey?: string, baseUrl?: string, logger?: Logger) {
43
+ constructor(apiKey?: string, baseUrl?: string, logger?: Logger, captureWarnings: boolean = true) {
365
44
  // Set up the default logger.
366
45
  if (logger) {
367
46
  this._logger = logger;
368
47
  } else {
369
- this._logger = getConsoleLogger(DEFAULT_LOG_LEVEL);
48
+ this._logger = getDefaultLogger();
370
49
  }
371
50
 
372
51
  // Resolve the API base URL.
373
52
  if (baseUrl) {
374
53
  this._baseUrl = baseUrl;
375
- } else if (process.env.GUAVA_BASE_URL) {
376
- this._baseUrl = process.env.GUAVA_BASE_URL;
377
54
  } else {
378
- this._baseUrl = DEFAULT_BASE_URL;
55
+ this._baseUrl = getBaseUrl();
379
56
  }
380
57
 
381
58
  // Resolve the API key.
@@ -388,6 +65,19 @@ export class Client {
388
65
  "Guava API key must be provided either as argument to client constructor, or in environment variable GUAVA_API_KEY.",
389
66
  );
390
67
  }
68
+
69
+ if (!firstClient) {
70
+ firstClient = true;
71
+
72
+ if (captureWarnings) {
73
+ process.on("warning", (warning) => {
74
+ this._logger.warn(warning.toString());
75
+ });
76
+ }
77
+
78
+ telemetryClient.setSdkHeaders(this.headers());
79
+ this._checkSdkDeprecation();
80
+ }
391
81
  }
392
82
 
393
83
  private getWebsocketBase() {
@@ -410,37 +100,66 @@ export class Client {
410
100
  "x-guava-platform": os.platform(),
411
101
  "x-guava-runtime": process.release.name,
412
102
  "x-guava-runtime-version": process.version,
413
- "x-guava-sdk": "typescript-sdk",
103
+ "x-guava-sdk": SDK_NAME,
414
104
  "x-guava-sdk-version": pkgdata.version,
415
105
  };
416
106
  }
417
107
 
108
+ private async _checkSdkDeprecation() {
109
+ this._logger.debug(`Checking deprecation for SDK ${SDK_NAME}, ${pkgdata.version}.`);
110
+ try {
111
+ const url = new URL("v1/check-sdk-deprecation", this.getHttpBase());
112
+ url.searchParams.set("sdk_name", SDK_NAME);
113
+ url.searchParams.set("sdk_version", pkgdata.version);
114
+ const response = await fetchOrThrow(url, {
115
+ method: "POST",
116
+ headers: this.headers(),
117
+ });
118
+ const body = (await response.json()) as { deprecation_status: string };
119
+ if (body.deprecation_status === "supported") {
120
+ this._logger.info("SDK version still supported.");
121
+ } else if (body.deprecation_status === "deprecated") {
122
+ process.emitWarning(
123
+ "This SDK version is deprecated. Please update to a newer version of the SDK.",
124
+ );
125
+ } else {
126
+ this._logger.warn("SDK deprecation status unknown.");
127
+ }
128
+ } catch (e) {
129
+ this._logger.error("Encountered issue while checking for deprecation.");
130
+ }
131
+ }
132
+
418
133
  /**
419
134
  * @description use the Guava API to call out to a number
420
135
  */
421
- createOutbound(
422
- fromNumber: string | undefined,
423
- toNumber: string,
424
- callControllerFactory?: (logger: Logger) => CallController,
425
- ) {
136
+ createOutbound(fromNumber: string | undefined, toNumber: string, callController: CallController) {
426
137
  const url = new URL("v1/create-outbound", this.getWebsocketBase());
427
138
  const ws = new WebSocket(url, {
428
139
  headers: this.headers(),
429
140
  });
430
- const callController = (callControllerFactory ?? ((_) => undefined))(
431
- this._logger,
432
- );
433
141
 
434
142
  ws.addEventListener("open", async (_ev) => {
435
143
  ws.send(
436
- stringifyZod(startOutboundCallCommand, {
144
+ stringifyZod(StartOutboundCallCommand, {
437
145
  command_type: "start-outbound",
438
146
  to_number: toNumber,
439
147
  from_number: fromNumber,
440
148
  }),
441
149
  );
442
- await callController?.onCallStart();
150
+
151
+ // set the callController drain function to send all commands
152
+ // through the now open websocket
153
+ callController.setDrain(async (commands) => {
154
+ for (const command of commands.splice(0)) {
155
+ this._logger.debug(`Sending command ${JSON.stringify(command)}`);
156
+ ws.send(JSON.stringify(command));
157
+ }
158
+ });
159
+
160
+ await callController.onCallStart();
443
161
  });
162
+
444
163
  ws.addEventListener("close", (_ev) => {
445
164
  // we are closing the socket, so don't trigger any other listeners
446
165
  ws.removeAllListeners();
@@ -451,15 +170,6 @@ export class Client {
451
170
  this._ws = ws;
452
171
  this._controller = callController;
453
172
  this.replaceHandler(this.uninitializedOutbound.bind(this));
454
-
455
- // set the callController drain function to send all commands
456
- // through the websocket
457
- callController?.setDrain(async (commands) => {
458
- for (const command of commands.splice(0)) {
459
- this._logger.debug(`Sending command ${JSON.stringify(command)}`);
460
- ws.send(JSON.stringify(command));
461
- }
462
- });
463
173
  }
464
174
 
465
175
  private replaceHandler(newHandler?: (_: WebSocket.MessageEvent) => void) {
@@ -481,14 +191,12 @@ export class Client {
481
191
  }
482
192
 
483
193
  const session_started = z
484
- .union([sessionStartedEvent, errorEvent])
194
+ .union([SessionStartedEvent, ErrorEvent])
485
195
  .parse(JSON.parse(ev.data.toString("utf8")));
486
- if (session_started.event_type == "error") {
196
+ if (session_started.event_type === "error") {
487
197
  throw new Error(`Outbound call failed: ${session_started.content}`);
488
198
  } else {
489
- this._logger.info(
490
- `Started session with ID: ${session_started.session_id}`,
491
- );
199
+ this._logger.info(`Started session with ID: ${session_started.session_id}`);
492
200
  // move to next state
493
201
  this.replaceHandler(this.initializedOutbound.bind(this));
494
202
  }
@@ -506,38 +214,13 @@ export class Client {
506
214
  if (this._controller) {
507
215
  await this._controller.onEvent(event);
508
216
  }
509
- if (
510
- event.event_type == "outbound-call-failed" ||
511
- event.event_type == "bot-session-ended"
512
- ) {
217
+ if (event.event_type === "outbound-call-failed" || event.event_type === "bot-session-ended") {
513
218
  // shutdown the websocket
514
219
  this._ws.close();
515
220
  }
516
221
  }
517
222
  }
518
223
 
519
- async setInboundHandler(
520
- agent_number: string,
521
- public_url: string,
522
- inbound_token: string,
523
- ) {
524
- const response = await fetch(
525
- new URL(`v1/inbound-handler/${agent_number}`, this.getHttpBase()),
526
- {
527
- method: "PUT",
528
- headers: this.headers(),
529
- body: JSON.stringify({
530
- handler_url: public_url,
531
- handler_token: inbound_token,
532
- }),
533
- },
534
- );
535
-
536
- if (!response.ok) {
537
- throw new Error("Failed to set inbound handler");
538
- }
539
- }
540
-
541
224
  /**
542
225
  * @description use the Guava API to receive calls at a given number
543
226
  */
@@ -563,16 +246,13 @@ export class Client {
563
246
  this._logger.info(`Listening for calls to ${agent_number ?? webrtc_code}`);
564
247
 
565
248
  if (webrtc_code) {
566
- const debugurl = new URL(
567
- `debug-webrtc?webrtc_code=${webrtc_code}`,
568
- this.getHttpBase(),
569
- );
249
+ const debugurl = new URL(`debug-webrtc?webrtc_code=${webrtc_code}`, this.getHttpBase());
570
250
  this._logger.debug(`WebRTC DebugURL: ${debugurl}`);
571
251
  }
572
252
 
573
253
  ws.addEventListener("open", (_ev) => {
574
254
  ws.send(
575
- stringifyZod(listenInboundCommand, {
255
+ stringifyZod(ListenInboundCommand, {
576
256
  command_type: "listen-inbound",
577
257
  agent_number: agent_number,
578
258
  webrtc_code: webrtc_code,
@@ -585,9 +265,7 @@ export class Client {
585
265
  });
586
266
 
587
267
  ws.addEventListener("message", (ev) => {
588
- const tunnel_event = inboundTunnelEvent.parse(
589
- JSON.parse(ev.data.toString("utf8")),
590
- );
268
+ const tunnel_event = InboundTunnelEvent.parse(JSON.parse(ev.data.toString("utf8")));
591
269
  if (!(tunnel_event.call_id in callControllers)) {
592
270
  this._logger.info(
593
271
  `Received tunnel event for new call ID: ${tunnel_event.call_id}. Creating call controller.`,
@@ -600,7 +278,7 @@ export class Client {
600
278
  `Sending command: ${JSON.stringify(command)} for call ID: ${tunnel_event.call_id}`,
601
279
  );
602
280
  ws.send(
603
- stringifyZod(inboundTunnelCommand, {
281
+ stringifyZod(InboundTunnelCommand, {
604
282
  call_id: tunnel_event.call_id,
605
283
  command,
606
284
  }),
package/src/logging.ts CHANGED
@@ -8,6 +8,15 @@ const LOG_RANKS: Record<LogLevel, number> = {
8
8
  debug: 4,
9
9
  };
10
10
 
11
+ const ANSI_RESET = "\x1b[0m";
12
+ const LEVEL_COLORS: Record<LogLevel, string> = {
13
+ off: "",
14
+ debug: "\x1b[38;5;245m", // gray
15
+ info: "",
16
+ warn: "\x1b[38;5;214m", // orange
17
+ error: "\x1b[38;5;196m", // red
18
+ };
19
+
11
20
  export interface Logger {
12
21
  debug(format: string, ...args: unknown[]);
13
22
  info(format: string, ...args: unknown[]);
@@ -16,10 +25,10 @@ export interface Logger {
16
25
  }
17
26
 
18
27
  function shouldLog(messageLevel: LogLevel, loggerLevel: LogLevel) {
19
- if (!LOG_RANKS.hasOwnProperty(messageLevel)) {
28
+ if (!Object.hasOwn(LOG_RANKS, messageLevel)) {
20
29
  throw new Error(`Invalid log level: ${String(messageLevel)}`);
21
30
  }
22
- if (!LOG_RANKS.hasOwnProperty(loggerLevel)) {
31
+ if (!Object.hasOwn(LOG_RANKS, loggerLevel)) {
23
32
  throw new Error(`Invalid logger level: ${String(loggerLevel)}`);
24
33
  }
25
34
 
@@ -28,11 +37,34 @@ function shouldLog(messageLevel: LogLevel, loggerLevel: LogLevel) {
28
37
 
29
38
  function noop(format: string, ...args: unknown[]) {}
30
39
 
31
- export function getConsoleLogger(loggerLevel: LogLevel): Logger {
40
+ function makeColoredMethod(
41
+ fn: (...args: unknown[]) => void,
42
+ level: LogLevel,
43
+ useColor: boolean,
44
+ ): (format: string, ...args: unknown[]) => void {
45
+ if (!useColor) return fn.bind(console);
46
+ return (format: string, ...args: unknown[]) =>
47
+ fn(`${LEVEL_COLORS[level]}[${level}] ${format}${ANSI_RESET}`, ...args);
48
+ }
49
+
50
+ export function getConsoleLogger(loggerLevel: LogLevel, useColor = false): Logger {
32
51
  return {
33
- debug: shouldLog("debug", loggerLevel) ? console.debug.bind(console) : noop,
34
- info: shouldLog("info", loggerLevel) ? console.info.bind(console) : noop,
35
- warn: shouldLog("warn", loggerLevel) ? console.warn.bind(console) : noop,
36
- error: shouldLog("error", loggerLevel) ? console.error.bind(console) : noop,
52
+ debug: shouldLog("debug", loggerLevel)
53
+ ? makeColoredMethod(console.debug, "debug", useColor)
54
+ : noop,
55
+ info: shouldLog("info", loggerLevel) ? makeColoredMethod(console.info, "info", useColor) : noop,
56
+ warn: shouldLog("warn", loggerLevel) ? makeColoredMethod(console.warn, "warn", useColor) : noop,
57
+ error: shouldLog("error", loggerLevel)
58
+ ? makeColoredMethod(console.error, "error", useColor)
59
+ : noop,
37
60
  };
38
61
  }
62
+
63
+ export function getDefaultLogger(): Logger {
64
+ const level = (process.env.LOG_LEVEL ?? "info").toLowerCase();
65
+ if (!Object.hasOwn(LOG_RANKS, level)) {
66
+ throw new Error(`Unknown log level in LOG_LEVEL env var: ${level}`);
67
+ }
68
+ const useColor = process.stderr.isTTY === true && !process.env.NO_COLOR;
69
+ return getConsoleLogger(level as LogLevel, useColor);
70
+ }