@teamlearners/clawops 0.16.0 → 0.16.3

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.
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var chunkD25IRY5V_cjs = require('../chunk-D25IRY5V.cjs');
3
+ var chunk4QQXMZ76_cjs = require('../chunk-4QQXMZ76.cjs');
4
4
  var pino = require('pino');
5
5
  var fs = require('fs');
6
6
  var path = require('path');
@@ -331,6 +331,27 @@ function ulawToPcm16(ulaw) {
331
331
  }
332
332
  return out;
333
333
  }
334
+ function applyPcm16Gain(pcm, gain) {
335
+ if (!pcm.length) return Buffer.alloc(0);
336
+ if (gain === 1) return pcm;
337
+ if (!Number.isFinite(gain) || gain < 0) {
338
+ throw new Error("gain must be a finite number >= 0");
339
+ }
340
+ const nSamples = pcm.length >> 1;
341
+ const out = Buffer.alloc(nSamples * 2);
342
+ for (let i = 0; i < nSamples; i++) {
343
+ let value = Math.round(pcm.readInt16LE(i * 2) * gain);
344
+ if (value > 32767) value = 32767;
345
+ else if (value < -32768) value = -32768;
346
+ out.writeInt16LE(value, i * 2);
347
+ }
348
+ return out;
349
+ }
350
+ function applyUlawGain(ulaw, gain) {
351
+ if (!ulaw.length) return Buffer.alloc(0);
352
+ if (gain === 1) return ulaw;
353
+ return pcm16ToUlaw(applyPcm16Gain(ulawToPcm16(ulaw), gain));
354
+ }
334
355
  function resamplePcm16(pcm, fromRate, toRate) {
335
356
  if (fromRate === toRate || !pcm.length) return pcm;
336
357
  const nSamples = pcm.length >> 1;
@@ -919,8 +940,9 @@ var AudioRecorder = class {
919
940
  _inWritten = 0;
920
941
  _outWritten = 0;
921
942
  _mixWritten = 0;
922
- _startTime = 0;
923
943
  _started = false;
944
+ _baseTs = null;
945
+ _outCursor = 0;
924
946
  _log = NOOP_LOGGER;
925
947
  setLogger(logger) {
926
948
  this._log = logger;
@@ -937,17 +959,18 @@ var AudioRecorder = class {
937
959
  fs__namespace.writeSync(this._fdIn, header);
938
960
  fs__namespace.writeSync(this._fdOut, header);
939
961
  fs__namespace.writeSync(this._fdMix, header);
940
- this._startTime = performance.now();
941
962
  this._started = true;
942
963
  this._log.info("Recording started: %s", this._dir);
943
964
  }
944
- _expectedBytes() {
945
- const elapsed = (performance.now() - this._startTime) / 1e3;
946
- return Math.floor(elapsed * BYTES_PER_SECOND);
965
+ _timestampToBytes(mediaTsMs) {
966
+ if (this._baseTs === null) {
967
+ this._baseTs = mediaTsMs;
968
+ }
969
+ const target = Math.floor((mediaTsMs - this._baseTs) * BYTES_PER_SECOND / 1e3);
970
+ return Math.max(0, target - target % 2);
947
971
  }
948
- _padSilence(fd, written) {
949
- const expected = this._expectedBytes();
950
- let gap = expected - written;
972
+ _padSilence(fd, written, target) {
973
+ let gap = target - written;
951
974
  if (gap <= 0) return 0;
952
975
  gap = gap - gap % 2;
953
976
  if (gap > 0) {
@@ -957,9 +980,12 @@ var AudioRecorder = class {
957
980
  }
958
981
  _writeToMix(data, trackPos) {
959
982
  if (this._fdMix === null) return;
983
+ data = data.subarray(0, data.length - data.length % 2);
984
+ if (data.length === 0) return;
960
985
  const filePos = 44 + trackPos;
961
986
  if (trackPos < this._mixWritten) {
962
- const overlap = Math.min(data.length, this._mixWritten - trackPos);
987
+ let overlap = Math.min(data.length, this._mixWritten - trackPos);
988
+ overlap = overlap - overlap % 2;
963
989
  const existing = Buffer.alloc(overlap);
964
990
  fs__namespace.readSync(this._fdMix, existing, 0, overlap, filePos);
965
991
  const mixed = mixSamples(existing, data.subarray(0, overlap));
@@ -982,10 +1008,13 @@ var AudioRecorder = class {
982
1008
  this._mixWritten += data.length;
983
1009
  }
984
1010
  }
985
- writeInbound(pcm16_8k) {
1011
+ writeInbound(pcm16_8k, mediaTsMs = 0) {
986
1012
  if (!this._started || this._fdIn === null) return;
987
1013
  try {
988
- const gap = this._padSilence(this._fdIn, this._inWritten);
1014
+ pcm16_8k = pcm16_8k.subarray(0, pcm16_8k.length - pcm16_8k.length % 2);
1015
+ if (pcm16_8k.length === 0) return;
1016
+ const target = this._timestampToBytes(mediaTsMs);
1017
+ const gap = this._padSilence(this._fdIn, this._inWritten, target);
989
1018
  this._inWritten += gap;
990
1019
  const posBefore = this._inWritten;
991
1020
  fs__namespace.writeSync(this._fdIn, pcm16_8k);
@@ -995,14 +1024,21 @@ var AudioRecorder = class {
995
1024
  this._log.error({ err }, "Recording write error (inbound)");
996
1025
  }
997
1026
  }
998
- writeOutbound(pcm16_8k) {
1027
+ writeOutbound(pcm16_8k, mediaTsMs) {
999
1028
  if (!this._started || this._fdOut === null) return;
1000
1029
  try {
1001
- const gap = this._padSilence(this._fdOut, this._outWritten);
1030
+ pcm16_8k = pcm16_8k.subarray(0, pcm16_8k.length - pcm16_8k.length % 2);
1031
+ if (pcm16_8k.length === 0) return;
1032
+ let target = this._outCursor;
1033
+ if (mediaTsMs !== void 0) {
1034
+ target = Math.max(target, this._timestampToBytes(mediaTsMs));
1035
+ }
1036
+ const gap = this._padSilence(this._fdOut, this._outWritten, target);
1002
1037
  this._outWritten += gap;
1003
1038
  const posBefore = this._outWritten;
1004
1039
  fs__namespace.writeSync(this._fdOut, pcm16_8k);
1005
1040
  this._outWritten += pcm16_8k.length;
1041
+ this._outCursor = this._outWritten;
1006
1042
  this._writeToMix(pcm16_8k, posBefore);
1007
1043
  } catch (err) {
1008
1044
  this._log.error({ err }, "Recording write error (outbound)");
@@ -1043,7 +1079,7 @@ var MAX_ERROR_MESSAGE_LENGTH = 200;
1043
1079
  function getSdkInfo() {
1044
1080
  return {
1045
1081
  name: "clawops-node",
1046
- version: chunkD25IRY5V_cjs.VERSION,
1082
+ version: chunk4QQXMZ76_cjs.VERSION,
1047
1083
  runtime: `node/${process.versions.node}`,
1048
1084
  os: `${process.platform}/${os__default.default.arch()}`
1049
1085
  };
@@ -1698,7 +1734,7 @@ var ATTR_CALL_DIRECTION = "clawops.call.direction";
1698
1734
  var ATTR_AGENT_ID = "clawops.agent.id";
1699
1735
 
1700
1736
  // src/agent/agent.ts
1701
- var ClawOpsAgent = class {
1737
+ var ClawOpsAgent = class _ClawOpsAgent {
1702
1738
  _apiKey;
1703
1739
  _accountId;
1704
1740
  _baseUrl;
@@ -1721,10 +1757,12 @@ var ClawOpsAgent = class {
1721
1757
  _pipelineLog;
1722
1758
  _isPipelineSession = false;
1723
1759
  _holdAudioChunks = null;
1760
+ _rxGain;
1761
+ _txGain;
1724
1762
  constructor(options) {
1725
1763
  this._apiKey = options.apiKey ?? process.env["CLAWOPS_API_KEY"] ?? "";
1726
1764
  this._accountId = options.accountId ?? process.env["CLAWOPS_ACCOUNT_ID"] ?? "";
1727
- this._baseUrl = options.baseUrl ?? chunkD25IRY5V_cjs.DEFAULT_BASE_URL;
1765
+ this._baseUrl = options.baseUrl ?? chunk4QQXMZ76_cjs.DEFAULT_BASE_URL;
1728
1766
  this._fromNumber = options.from;
1729
1767
  this._session = options.session;
1730
1768
  this._recording = options.recording ?? false;
@@ -1732,6 +1770,8 @@ var ClawOpsAgent = class {
1732
1770
  this._mcpServers = options.mcpServers ?? [];
1733
1771
  this._builtinTools = resolveBuiltinTools(options.builtinTools ?? "all" /* ALL */);
1734
1772
  this._passiveDtmfDebounceMs = options.passiveDtmfDebounceMs ?? 500;
1773
+ this._rxGain = _ClawOpsAgent._validateGain("rxGain", options.rxGain ?? 1);
1774
+ this._txGain = _ClawOpsAgent._validateGain("txGain", options.txGain ?? 1);
1735
1775
  if (options.tracing) {
1736
1776
  setTracingConfig(options.tracing);
1737
1777
  }
@@ -1742,6 +1782,12 @@ var ClawOpsAgent = class {
1742
1782
  this._holdAudioChunks = loadHoldAudio(options.toolConfig.holdAudio);
1743
1783
  }
1744
1784
  }
1785
+ static _validateGain(name, gain) {
1786
+ if (typeof gain !== "number" || !Number.isFinite(gain) || gain < 0) {
1787
+ throw new chunk4QQXMZ76_cjs.AgentError(`${name}=${gain} must be a finite number >= 0`);
1788
+ }
1789
+ return gain;
1790
+ }
1745
1791
  /**
1746
1792
  * Register a function tool.
1747
1793
  *
@@ -1752,7 +1798,7 @@ var ClawOpsAgent = class {
1752
1798
  tool(nameOrTool, description, parameters, handler) {
1753
1799
  if (typeof nameOrTool === "string") {
1754
1800
  if (!description || !parameters || !handler) {
1755
- throw new chunkD25IRY5V_cjs.AgentError(
1801
+ throw new chunk4QQXMZ76_cjs.AgentError(
1756
1802
  "tool(name, description, parameters, handler) requires all arguments."
1757
1803
  );
1758
1804
  }
@@ -1788,10 +1834,10 @@ var ClawOpsAgent = class {
1788
1834
  async connect() {
1789
1835
  if (this._controlWs) return;
1790
1836
  if (!this._apiKey) {
1791
- throw new chunkD25IRY5V_cjs.AgentError("API key is required. Set CLAWOPS_API_KEY or pass apiKey option.");
1837
+ throw new chunk4QQXMZ76_cjs.AgentError("API key is required. Set CLAWOPS_API_KEY or pass apiKey option.");
1792
1838
  }
1793
1839
  if (!this._accountId) {
1794
- throw new chunkD25IRY5V_cjs.AgentError(
1840
+ throw new chunk4QQXMZ76_cjs.AgentError(
1795
1841
  "Account ID is required. Set CLAWOPS_ACCOUNT_ID or pass accountId option."
1796
1842
  );
1797
1843
  }
@@ -1815,7 +1861,7 @@ var ClawOpsAgent = class {
1815
1861
  } catch {
1816
1862
  }
1817
1863
  } catch (err) {
1818
- throw new chunkD25IRY5V_cjs.AgentConnectionError(
1864
+ throw new chunk4QQXMZ76_cjs.AgentConnectionError(
1819
1865
  `Failed to connect to ClawOps: ${err instanceof Error ? err.message : String(err)}`
1820
1866
  );
1821
1867
  }
@@ -1866,7 +1912,7 @@ var ClawOpsAgent = class {
1866
1912
  });
1867
1913
  if (resp.status !== 201) {
1868
1914
  const error = await resp.json();
1869
- throw new chunkD25IRY5V_cjs.AgentError(`\uBC1C\uC2E0 \uC2E4\uD328 (${resp.status}): ${error["error"] ?? ""}`);
1915
+ throw new chunk4QQXMZ76_cjs.AgentError(`\uBC1C\uC2E0 \uC2E4\uD328 (${resp.status}): ${error["error"] ?? ""}`);
1870
1916
  }
1871
1917
  const data = await resp.json();
1872
1918
  const callSession = new CallSession({
@@ -2024,9 +2070,14 @@ var ClawOpsAgent = class {
2024
2070
  }
2025
2071
  const mediaWs = new MediaWebSocket();
2026
2072
  mediaWs.setLogger(this._log);
2073
+ let latestMediaTs = 0;
2027
2074
  session._bindTransport(
2028
2075
  (audio) => {
2029
- mediaWs.sendAudio(audio.toString("base64"));
2076
+ const gained = applyUlawGain(audio, this._txGain);
2077
+ if (recorder) {
2078
+ recorder.writeOutbound(ulawToPcm16(gained), latestMediaTs);
2079
+ }
2080
+ mediaWs.sendAudio(gained.toString("base64"));
2030
2081
  session.recordFirstResponse();
2031
2082
  },
2032
2083
  () => {
@@ -2063,12 +2114,14 @@ var ClawOpsAgent = class {
2063
2114
  sessionHandler.setHoldAudio(this._holdAudioChunks);
2064
2115
  }
2065
2116
  this._callSessions.set(session.callId, sessionHandler);
2066
- mediaWs.onAudio((ulawAudio, _timestamp) => {
2067
- if (sessionHandler) {
2068
- sessionHandler.feedAudio(ulawAudio);
2069
- }
2117
+ mediaWs.onAudio((ulawAudio, timestamp) => {
2118
+ latestMediaTs = timestamp;
2119
+ const gained = applyUlawGain(ulawAudio, this._rxGain);
2070
2120
  if (recorder) {
2071
- recorder.writeInbound(ulawToPcm16(ulawAudio));
2121
+ recorder.writeInbound(ulawToPcm16(gained), timestamp);
2122
+ }
2123
+ if (sessionHandler) {
2124
+ sessionHandler.feedAudio(gained, timestamp);
2072
2125
  }
2073
2126
  });
2074
2127
  mediaWs.onDtmf((digit) => {
@@ -2539,10 +2592,6 @@ var PipelineSession = class {
2539
2592
  sampleRate: this._sampleRate
2540
2593
  })) {
2541
2594
  if (!this._running || !this._speaking) break;
2542
- if (this._recorder) {
2543
- const pcm8k2 = this._sampleRate !== 8e3 ? resamplePcm16(audioChunk, this._sampleRate, 8e3) : audioChunk;
2544
- this._recorder.writeOutbound(pcm8k2);
2545
- }
2546
2595
  const pcm8k = resamplePcm16(audioChunk, this._sampleRate, 8e3);
2547
2596
  const ulaw = pcm16ToUlaw(pcm8k);
2548
2597
  for (let off = 0; off < ulaw.length; off += 160) {
@@ -2612,7 +2661,7 @@ var OpenAIRealtime = class {
2612
2661
  constructor(options = {}) {
2613
2662
  this._apiKey = options.apiKey ?? process.env["OPENAI_API_KEY"] ?? "";
2614
2663
  this._systemPrompt = options.systemPrompt ?? "";
2615
- this._model = options.model ?? "gpt-realtime-1.5";
2664
+ this._model = options.model ?? "gpt-realtime-2";
2616
2665
  this._voice = options.voice ?? "marin";
2617
2666
  this._language = options.language ?? "ko";
2618
2667
  this._turnDetection = options.turnDetection !== void 0 ? options.turnDetection : { type: "semantic_vad", eagerness: "medium", interrupt_response: true };
@@ -2687,8 +2736,8 @@ var OpenAIRealtime = class {
2687
2736
  });
2688
2737
  this._send({ type: "response.create" });
2689
2738
  }
2690
- feedAudio(audio) {
2691
- this._latestMediaTs = Date.now();
2739
+ feedAudio(audio, timestamp) {
2740
+ this._latestMediaTs = timestamp ?? this._latestMediaTs;
2692
2741
  if (this._ws && this._ws.readyState === 1 && !this._closed) {
2693
2742
  this._send({
2694
2743
  type: "input_audio_buffer.append",
@@ -2799,7 +2848,7 @@ var OpenAIRealtime = class {
2799
2848
  if (this._playback === null) {
2800
2849
  this._playback = {
2801
2850
  itemId: msg["item_id"] || "",
2802
- startTs: this._latestMediaTs || Date.now(),
2851
+ startTs: this._latestMediaTs,
2803
2852
  sentChunks: 0,
2804
2853
  generating: true,
2805
2854
  audioRemainder: Buffer.alloc(0)
@@ -2809,9 +2858,6 @@ var OpenAIRealtime = class {
2809
2858
  }
2810
2859
  const pb = this._playback;
2811
2860
  const ulaw = Buffer.from(msg["delta"], "base64");
2812
- if (this._recorder) {
2813
- this._recorder.writeOutbound(ulawToPcm16(ulaw));
2814
- }
2815
2861
  const combined = Buffer.concat([pb.audioRemainder, ulaw]);
2816
2862
  const chunkSize = 160;
2817
2863
  const fullEnd = Math.floor(combined.length / chunkSize) * chunkSize;
@@ -2835,7 +2881,7 @@ var OpenAIRealtime = class {
2835
2881
  if (pb === null) {
2836
2882
  return;
2837
2883
  }
2838
- const playedMs = Math.max(0, (this._latestMediaTs || Date.now()) - pb.startTs);
2884
+ const playedMs = Math.max(0, this._latestMediaTs - pb.startTs);
2839
2885
  this._log.info(
2840
2886
  "[Interrupt] item=%s played=%dms total=%dms",
2841
2887
  pb.itemId,
@@ -3127,9 +3173,6 @@ var GeminiRealtime = class _GeminiRealtime {
3127
3173
  feedAudio(audio) {
3128
3174
  if (this._session && !this._closed) {
3129
3175
  const pcm8k = ulawToPcm16(audio);
3130
- if (this._recorder) {
3131
- this._recorder.writeInbound(pcm8k);
3132
- }
3133
3176
  const pcm16k = resamplePcm16(pcm8k, 8e3, 16e3);
3134
3177
  this._session.sendRealtimeInput({
3135
3178
  audio: {
@@ -3227,9 +3270,6 @@ var GeminiRealtime = class _GeminiRealtime {
3227
3270
  _handleAudioData(b64Data) {
3228
3271
  if (!this._call) return;
3229
3272
  const pcm24k = Buffer.from(b64Data, "base64");
3230
- if (this._recorder) {
3231
- this._recorder.writeOutbound(resamplePcm16(pcm24k, 24e3, 8e3));
3232
- }
3233
3273
  const pcm8k = resamplePcm16(pcm24k, 24e3, 8e3);
3234
3274
  const ulaw = pcm16ToUlaw(pcm8k);
3235
3275
  const combined = Buffer.concat([this._audioRemainder, ulaw]);