open-agents-ai 0.54.0 → 0.56.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +269 -14
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -16726,6 +16726,62 @@ transcribe-cli error: ${transcribeCliError}` : "";
|
|
|
16726
16726
|
this.pendingText = "";
|
|
16727
16727
|
return text;
|
|
16728
16728
|
}
|
|
16729
|
+
/**
|
|
16730
|
+
* Create a standalone transcriber for call sessions.
|
|
16731
|
+
* No microphone capture — just an ASR engine you feed PCM chunks to.
|
|
16732
|
+
* Returns an EventEmitter with .write(pcm: Buffer) and 'transcript' events.
|
|
16733
|
+
* Caller is responsible for calling .stop() when done.
|
|
16734
|
+
*/
|
|
16735
|
+
async createCallTranscriber() {
|
|
16736
|
+
let tc = await this.loadTranscribeCli();
|
|
16737
|
+
if (!tc && _bgInstallPromise) {
|
|
16738
|
+
await _bgInstallPromise;
|
|
16739
|
+
this.transcribeCliAvailable = null;
|
|
16740
|
+
tc = await this.loadTranscribeCli();
|
|
16741
|
+
}
|
|
16742
|
+
if (!tc) {
|
|
16743
|
+
try {
|
|
16744
|
+
execSync18("npm i -g transcribe-cli", { stdio: "pipe", timeout: 18e4 });
|
|
16745
|
+
this.transcribeCliAvailable = null;
|
|
16746
|
+
tc = await this.loadTranscribeCli();
|
|
16747
|
+
} catch {
|
|
16748
|
+
}
|
|
16749
|
+
}
|
|
16750
|
+
if (tc?.TranscribeLive) {
|
|
16751
|
+
try {
|
|
16752
|
+
const transcriber = new tc.TranscribeLive({
|
|
16753
|
+
model: this.config.model,
|
|
16754
|
+
sampleRate: 16e3,
|
|
16755
|
+
channels: 1,
|
|
16756
|
+
sampleWidth: 2,
|
|
16757
|
+
chunkDuration: 3
|
|
16758
|
+
});
|
|
16759
|
+
await new Promise((resolve28, reject) => {
|
|
16760
|
+
const timeout = setTimeout(() => reject(new Error("Model load timeout (60s)")), 6e4);
|
|
16761
|
+
transcriber.on("ready", () => {
|
|
16762
|
+
clearTimeout(timeout);
|
|
16763
|
+
resolve28();
|
|
16764
|
+
});
|
|
16765
|
+
transcriber.on("error", (err) => {
|
|
16766
|
+
clearTimeout(timeout);
|
|
16767
|
+
reject(err);
|
|
16768
|
+
});
|
|
16769
|
+
});
|
|
16770
|
+
return transcriber;
|
|
16771
|
+
} catch {
|
|
16772
|
+
}
|
|
16773
|
+
}
|
|
16774
|
+
const scriptPath2 = findLiveWhisperScript();
|
|
16775
|
+
if (!scriptPath2)
|
|
16776
|
+
return null;
|
|
16777
|
+
try {
|
|
16778
|
+
const fallback = new WhisperFallbackTranscriber(this.config.model, scriptPath2);
|
|
16779
|
+
await fallback.start();
|
|
16780
|
+
return fallback;
|
|
16781
|
+
} catch {
|
|
16782
|
+
return null;
|
|
16783
|
+
}
|
|
16784
|
+
}
|
|
16729
16785
|
/**
|
|
16730
16786
|
* Transcribe a file (audio or video) using transcribe-cli.
|
|
16731
16787
|
* Returns the transcription result.
|
|
@@ -17284,8 +17340,8 @@ function renderSlashHelp() {
|
|
|
17284
17340
|
["/listen manual", "Manual mode \u2014 press Enter to submit (turns off auto)"],
|
|
17285
17341
|
["/listen auto", "Auto-submit after 3s silence (blinking \u25CF indicator)"],
|
|
17286
17342
|
["/listen stop", "Stop listening"],
|
|
17287
|
-
["/call", "Start voice call session (cloudflared tunnel +
|
|
17288
|
-
["/
|
|
17343
|
+
["/call", "Start voice call session (cloudflared tunnel + ASR/TTS)"],
|
|
17344
|
+
["/hangup", "End active call session"],
|
|
17289
17345
|
["/cost", "Show session token cost breakdown"],
|
|
17290
17346
|
["/evaluate", "Evaluate last completed task (LLM quality scoring)"],
|
|
17291
17347
|
["/task-type", "Set task type (code, document, analysis, plan, general, auto)"],
|
|
@@ -17675,6 +17731,7 @@ var init_render = __esm({
|
|
|
17675
17731
|
"/voice",
|
|
17676
17732
|
"/listen",
|
|
17677
17733
|
"/call",
|
|
17734
|
+
"/hangup",
|
|
17678
17735
|
"/stream",
|
|
17679
17736
|
"/verbose",
|
|
17680
17737
|
"/dream",
|
|
@@ -17692,6 +17749,7 @@ var init_render = __esm({
|
|
|
17692
17749
|
// packages/cli/dist/tui/voice-session.js
|
|
17693
17750
|
import { createServer } from "node:http";
|
|
17694
17751
|
import { spawn as spawn10, execSync as execSync19 } from "node:child_process";
|
|
17752
|
+
import { createHash } from "node:crypto";
|
|
17695
17753
|
import { EventEmitter as EventEmitter2 } from "node:events";
|
|
17696
17754
|
function parseWebSocketFrame(buf) {
|
|
17697
17755
|
if (buf.length < 2)
|
|
@@ -17846,26 +17904,42 @@ let scriptProcessor = null;
|
|
|
17846
17904
|
let micActive = false;
|
|
17847
17905
|
let playbackQueue = [];
|
|
17848
17906
|
let isPlaying = false;
|
|
17907
|
+
let reconnectDelay = 1000;
|
|
17908
|
+
let reconnectTimer = null;
|
|
17849
17909
|
|
|
17850
17910
|
// Connect WebSocket
|
|
17851
17911
|
function connect() {
|
|
17912
|
+
if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
|
|
17852
17913
|
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
17853
17914
|
ws = new WebSocket(proto + '//' + location.host + '/ws');
|
|
17854
17915
|
ws.binaryType = 'arraybuffer';
|
|
17855
17916
|
|
|
17856
17917
|
ws.onopen = () => {
|
|
17857
17918
|
statusEl.innerHTML = '<span class="connected">Connected</span>';
|
|
17919
|
+
reconnectDelay = 1000; // Reset backoff on successful connection
|
|
17920
|
+
};
|
|
17921
|
+
|
|
17922
|
+
ws.onerror = () => {
|
|
17923
|
+
// Error fires before close \u2014 just update status, close handler does reconnect
|
|
17924
|
+
statusEl.innerHTML = '<span class="disconnected">Connection error</span>';
|
|
17858
17925
|
};
|
|
17859
17926
|
|
|
17860
17927
|
ws.onclose = () => {
|
|
17861
17928
|
statusEl.innerHTML = '<span class="disconnected">Disconnected \u2014 reconnecting...</span>';
|
|
17862
|
-
|
|
17929
|
+
// Exponential backoff: 1s, 2s, 4s, max 10s
|
|
17930
|
+
reconnectTimer = setTimeout(connect, reconnectDelay);
|
|
17931
|
+
reconnectDelay = Math.min(reconnectDelay * 2, 10000);
|
|
17863
17932
|
};
|
|
17864
17933
|
|
|
17865
17934
|
ws.onmessage = (evt) => {
|
|
17866
17935
|
if (typeof evt.data === 'string') {
|
|
17867
17936
|
try {
|
|
17868
17937
|
const msg = JSON.parse(evt.data);
|
|
17938
|
+
if (msg.type === 'keepalive') {
|
|
17939
|
+
// Respond to keep connection alive through proxies
|
|
17940
|
+
if (ws.readyState === 1) ws.send(JSON.stringify({ type: 'pong' }));
|
|
17941
|
+
return;
|
|
17942
|
+
}
|
|
17869
17943
|
if (msg.type === 'transcript') {
|
|
17870
17944
|
addTranscript(msg.speaker, msg.text);
|
|
17871
17945
|
} else if (msg.type === 'speaking_start') {
|
|
@@ -17990,11 +18064,11 @@ function renderVoiceSessionStart(tunnelUrl) {
|
|
|
17990
18064
|
process.stdout.write(`
|
|
17991
18065
|
${c2.cyan("\u2601")} ${c2.bold("Live Voice Session")}
|
|
17992
18066
|
`);
|
|
17993
|
-
process.stdout.write(` ${c2.dim("\u23BF")}
|
|
18067
|
+
process.stdout.write(` ${c2.dim("\u23BF")} ${c2.cyan(tunnelUrl)}
|
|
17994
18068
|
`);
|
|
17995
18069
|
process.stdout.write(` ${c2.dim("\u23BF")} Bidirectional PCM audio + live transcription
|
|
17996
18070
|
`);
|
|
17997
|
-
process.stdout.write(` ${c2.dim("\u23BF")}
|
|
18071
|
+
process.stdout.write(` ${c2.dim("\u23BF")} /hangup to end session (auto-closes after 1 min idle)
|
|
17998
18072
|
|
|
17999
18073
|
`);
|
|
18000
18074
|
}
|
|
@@ -18011,6 +18085,12 @@ function renderVoiceSessionUser(action, username) {
|
|
|
18011
18085
|
process.stdout.write(` ${c2.dim("\u23BF")} ${c2.cyan("\u2601")} ${icon} ${username} ${action}
|
|
18012
18086
|
`);
|
|
18013
18087
|
}
|
|
18088
|
+
function renderVoiceSessionTranscript(speaker, text) {
|
|
18089
|
+
const label = speaker === "user" ? c2.yellow("user") : c2.cyan("agent");
|
|
18090
|
+
const preview = text.length > 80 ? text.slice(0, 77) + "..." : text;
|
|
18091
|
+
process.stdout.write(` ${c2.dim("\u23BF")} ${c2.cyan("\u2601")} [${label}] ${preview}
|
|
18092
|
+
`);
|
|
18093
|
+
}
|
|
18014
18094
|
var VoiceSession;
|
|
18015
18095
|
var init_voice_session = __esm({
|
|
18016
18096
|
"packages/cli/dist/tui/voice-session.js"() {
|
|
@@ -18022,7 +18102,10 @@ var init_voice_session = __esm({
|
|
|
18022
18102
|
cloudflaredProcess = null;
|
|
18023
18103
|
wsClients = /* @__PURE__ */ new Map();
|
|
18024
18104
|
runtimeTimer = null;
|
|
18105
|
+
idleTimer = null;
|
|
18025
18106
|
ttsSpeaking = false;
|
|
18107
|
+
/** Idle timeout before auto-closing (ms). Default 60s — no users connected → session ends. */
|
|
18108
|
+
idleTimeoutMs = 6e4;
|
|
18026
18109
|
/** Callback invoked when user audio chunk arrives (PCM 16kHz 16-bit mono) */
|
|
18027
18110
|
onUserAudio = null;
|
|
18028
18111
|
/** Callback invoked when user speech is transcribed */
|
|
@@ -18056,6 +18139,8 @@ var init_voice_session = __esm({
|
|
|
18056
18139
|
const port = await this.findFreePort();
|
|
18057
18140
|
this.state.localPort = port;
|
|
18058
18141
|
this.server = createServer((req, res) => this.handleHTTP(req, res));
|
|
18142
|
+
this.server.timeout = 0;
|
|
18143
|
+
this.server.keepAliveTimeout = 0;
|
|
18059
18144
|
this.server.on("upgrade", (req, socket, head) => {
|
|
18060
18145
|
if (req.url === "/ws") {
|
|
18061
18146
|
this.handleWebSocketUpgrade(req, socket, head);
|
|
@@ -18081,7 +18166,8 @@ var init_voice_session = __esm({
|
|
|
18081
18166
|
this.emit("tick", this.runtime);
|
|
18082
18167
|
}, 1e3);
|
|
18083
18168
|
this.emit("started", this.state.tunnelUrl);
|
|
18084
|
-
|
|
18169
|
+
this.resetIdleTimer();
|
|
18170
|
+
return this.state.tunnelUrl;
|
|
18085
18171
|
}
|
|
18086
18172
|
/**
|
|
18087
18173
|
* Stop the voice session.
|
|
@@ -18094,6 +18180,10 @@ var init_voice_session = __esm({
|
|
|
18094
18180
|
clearInterval(this.runtimeTimer);
|
|
18095
18181
|
this.runtimeTimer = null;
|
|
18096
18182
|
}
|
|
18183
|
+
if (this.idleTimer) {
|
|
18184
|
+
clearTimeout(this.idleTimer);
|
|
18185
|
+
this.idleTimer = null;
|
|
18186
|
+
}
|
|
18097
18187
|
for (const [id, socket] of this.wsClients) {
|
|
18098
18188
|
try {
|
|
18099
18189
|
const closeFrame = createWebSocketFrame(8, Buffer.alloc(0));
|
|
@@ -18184,9 +18274,11 @@ var init_voice_session = __esm({
|
|
|
18184
18274
|
socket.destroy();
|
|
18185
18275
|
return;
|
|
18186
18276
|
}
|
|
18187
|
-
const crypto2 = __require("node:crypto");
|
|
18188
18277
|
const magic = "258EAFA5-E914-47DA-95CA-5AB9DC085B62";
|
|
18189
|
-
const accept =
|
|
18278
|
+
const accept = createHash("sha1").update(key + magic).digest("base64");
|
|
18279
|
+
socket.setNoDelay(true);
|
|
18280
|
+
socket.setKeepAlive(true, 1e4);
|
|
18281
|
+
socket.setTimeout(0);
|
|
18190
18282
|
socket.write(`HTTP/1.1 101 Switching Protocols\r
|
|
18191
18283
|
Upgrade: websocket\r
|
|
18192
18284
|
Connection: Upgrade\r
|
|
@@ -18197,6 +18289,23 @@ Sec-WebSocket-Accept: ${accept}\r
|
|
|
18197
18289
|
this.wsClients.set(clientId, socket);
|
|
18198
18290
|
this.state.connectedUsers.set(clientId, { username: "web-user", connectedAt: Date.now() });
|
|
18199
18291
|
this.emit("userConnected", clientId, "web-user");
|
|
18292
|
+
this.resetIdleTimer();
|
|
18293
|
+
try {
|
|
18294
|
+
socket.write(createWebSocketFrame(9, Buffer.from("keepalive")));
|
|
18295
|
+
} catch {
|
|
18296
|
+
}
|
|
18297
|
+
const pingInterval = setInterval(() => {
|
|
18298
|
+
if (socket.destroyed) {
|
|
18299
|
+
clearInterval(pingInterval);
|
|
18300
|
+
return;
|
|
18301
|
+
}
|
|
18302
|
+
try {
|
|
18303
|
+
socket.write(createWebSocketFrame(9, Buffer.from("keepalive")));
|
|
18304
|
+
socket.write(createWebSocketFrame(1, Buffer.from(JSON.stringify({ type: "keepalive" }))));
|
|
18305
|
+
} catch {
|
|
18306
|
+
clearInterval(pingInterval);
|
|
18307
|
+
}
|
|
18308
|
+
}, 5e3);
|
|
18200
18309
|
let frameBuffer = Buffer.alloc(0);
|
|
18201
18310
|
socket.on("data", (data) => {
|
|
18202
18311
|
frameBuffer = Buffer.concat([frameBuffer, data]);
|
|
@@ -18218,10 +18327,15 @@ Sec-WebSocket-Accept: ${accept}\r
|
|
|
18218
18327
|
const consumed = headerLen + (masked ? 4 : 0) + payloadLen;
|
|
18219
18328
|
frameBuffer = frameBuffer.subarray(consumed);
|
|
18220
18329
|
if (frame.opcode === 8) {
|
|
18330
|
+
try {
|
|
18331
|
+
socket.write(createWebSocketFrame(8, Buffer.alloc(0)));
|
|
18332
|
+
} catch {
|
|
18333
|
+
}
|
|
18221
18334
|
socket.end();
|
|
18222
18335
|
return;
|
|
18223
18336
|
} else if (frame.opcode === 9) {
|
|
18224
18337
|
socket.write(createWebSocketFrame(10, frame.payload));
|
|
18338
|
+
} else if (frame.opcode === 10) {
|
|
18225
18339
|
} else if (frame.opcode === 1) {
|
|
18226
18340
|
try {
|
|
18227
18341
|
const msg = JSON.parse(frame.payload.toString());
|
|
@@ -18229,7 +18343,6 @@ Sec-WebSocket-Accept: ${accept}\r
|
|
|
18229
18343
|
const entry = this.state.connectedUsers.get(clientId);
|
|
18230
18344
|
if (entry)
|
|
18231
18345
|
entry.username = msg.username;
|
|
18232
|
-
this.emit("userConnected", clientId, msg.username);
|
|
18233
18346
|
}
|
|
18234
18347
|
} catch {
|
|
18235
18348
|
}
|
|
@@ -18241,14 +18354,17 @@ Sec-WebSocket-Accept: ${accept}\r
|
|
|
18241
18354
|
}
|
|
18242
18355
|
});
|
|
18243
18356
|
socket.on("close", () => {
|
|
18357
|
+
clearInterval(pingInterval);
|
|
18244
18358
|
this.wsClients.delete(clientId);
|
|
18245
|
-
const user = this.state.connectedUsers.get(clientId);
|
|
18246
18359
|
this.state.connectedUsers.delete(clientId);
|
|
18247
18360
|
this.emit("userDisconnected", clientId);
|
|
18361
|
+
this.resetIdleTimer();
|
|
18248
18362
|
});
|
|
18249
18363
|
socket.on("error", () => {
|
|
18364
|
+
clearInterval(pingInterval);
|
|
18250
18365
|
this.wsClients.delete(clientId);
|
|
18251
18366
|
this.state.connectedUsers.delete(clientId);
|
|
18367
|
+
this.resetIdleTimer();
|
|
18252
18368
|
});
|
|
18253
18369
|
if (head.length > 0) {
|
|
18254
18370
|
socket.emit("data", head);
|
|
@@ -18291,6 +18407,27 @@ Sec-WebSocket-Accept: ${accept}\r
|
|
|
18291
18407
|
});
|
|
18292
18408
|
});
|
|
18293
18409
|
}
|
|
18410
|
+
// ── Idle timer ───────────────────────────────────────────────────────
|
|
18411
|
+
/**
|
|
18412
|
+
* Reset idle timer. Called when users connect/disconnect.
|
|
18413
|
+
* When no users are connected, starts a countdown to auto-close.
|
|
18414
|
+
* When a user connects, cancels any pending auto-close.
|
|
18415
|
+
*/
|
|
18416
|
+
resetIdleTimer() {
|
|
18417
|
+
if (this.idleTimer) {
|
|
18418
|
+
clearTimeout(this.idleTimer);
|
|
18419
|
+
this.idleTimer = null;
|
|
18420
|
+
}
|
|
18421
|
+
if (this.state.connectedUsers.size === 0) {
|
|
18422
|
+
this.idleTimer = setTimeout(() => {
|
|
18423
|
+
if (this.state.active && this.state.connectedUsers.size === 0) {
|
|
18424
|
+
this.emit("idle_timeout");
|
|
18425
|
+
this.stop().catch(() => {
|
|
18426
|
+
});
|
|
18427
|
+
}
|
|
18428
|
+
}, this.idleTimeoutMs);
|
|
18429
|
+
}
|
|
18430
|
+
}
|
|
18294
18431
|
// ── Helpers ───────────────────────────────────────────────────────────
|
|
18295
18432
|
findFreePort() {
|
|
18296
18433
|
return new Promise((resolve28, reject) => {
|
|
@@ -20749,6 +20886,19 @@ async function handleSlashCommand(input, ctx) {
|
|
|
20749
20886
|
}
|
|
20750
20887
|
return "handled";
|
|
20751
20888
|
}
|
|
20889
|
+
case "hangup": {
|
|
20890
|
+
if (!ctx.callStop) {
|
|
20891
|
+
renderWarning("Call mode not available in this context.");
|
|
20892
|
+
return "handled";
|
|
20893
|
+
}
|
|
20894
|
+
if (ctx.isCallActive?.()) {
|
|
20895
|
+
await ctx.callStop();
|
|
20896
|
+
renderInfo("Call session ended.");
|
|
20897
|
+
} else {
|
|
20898
|
+
renderWarning("No active call session.");
|
|
20899
|
+
}
|
|
20900
|
+
return "handled";
|
|
20901
|
+
}
|
|
20752
20902
|
case "bruteforce":
|
|
20753
20903
|
case "brute": {
|
|
20754
20904
|
const isOn = ctx.bruteForceToggle();
|
|
@@ -23135,6 +23285,12 @@ var init_voice = __esm({
|
|
|
23135
23285
|
enabled = false;
|
|
23136
23286
|
modelId = "glados";
|
|
23137
23287
|
ready = false;
|
|
23288
|
+
/**
|
|
23289
|
+
* Callback fired with PCM Int16 data whenever TTS synthesizes audio.
|
|
23290
|
+
* Used by VoiceSession to stream TTS output to WebSocket clients.
|
|
23291
|
+
* Set this to wire TTS → call session broadcast.
|
|
23292
|
+
*/
|
|
23293
|
+
onPCMOutput = null;
|
|
23138
23294
|
session = null;
|
|
23139
23295
|
// ort.InferenceSession
|
|
23140
23296
|
ort = null;
|
|
@@ -23366,6 +23522,14 @@ var init_voice = __esm({
|
|
|
23366
23522
|
const audioData = result["output"].data;
|
|
23367
23523
|
if (audioData.length === 0)
|
|
23368
23524
|
return;
|
|
23525
|
+
if (this.onPCMOutput) {
|
|
23526
|
+
const int16 = new Int16Array(audioData.length);
|
|
23527
|
+
for (let i = 0; i < audioData.length; i++) {
|
|
23528
|
+
const s = Math.max(-1, Math.min(1, audioData[i]));
|
|
23529
|
+
int16[i] = s < 0 ? s * 32768 : s * 32767;
|
|
23530
|
+
}
|
|
23531
|
+
this.onPCMOutput(Buffer.from(int16.buffer), this.config.audio.sample_rate);
|
|
23532
|
+
}
|
|
23369
23533
|
const wavPath = join34(tmpdir6(), `oa-voice-${Date.now()}.wav`);
|
|
23370
23534
|
this.writeWav(audioData, this.config.audio.sample_rate, wavPath);
|
|
23371
23535
|
await this.playWav(wavPath);
|
|
@@ -27257,6 +27421,10 @@ function convertMarkdownToTelegramHTML(md) {
|
|
|
27257
27421
|
return html;
|
|
27258
27422
|
}
|
|
27259
27423
|
function formatIntermediateState(event) {
|
|
27424
|
+
if (event.type === "tool_call" && event.toolName === "task_complete")
|
|
27425
|
+
return null;
|
|
27426
|
+
if (event.type === "tool_result" && event.toolName === "task_complete")
|
|
27427
|
+
return null;
|
|
27260
27428
|
if (event.type === "tool_call") {
|
|
27261
27429
|
const argsPreview = event.toolArgs ? JSON.stringify(event.toolArgs).slice(0, 60) : "";
|
|
27262
27430
|
return `\u{1F527} <code>${event.toolName || "tool"}</code>(${argsPreview.length > 57 ? argsPreview.slice(0, 57) + "..." : argsPreview})`;
|
|
@@ -27456,6 +27624,10 @@ with summary "no_reply" to silently skip without responding.
|
|
|
27456
27624
|
commandHandler = null;
|
|
27457
27625
|
/** Callback to get active call session URL (wired from interactive.ts) */
|
|
27458
27626
|
callUrlGetter = null;
|
|
27627
|
+
/** Callback to start a call session and return the URL (wired from interactive.ts) */
|
|
27628
|
+
callStarter = null;
|
|
27629
|
+
/** Callback to stop a call session (wired from interactive.ts) */
|
|
27630
|
+
callStopper = null;
|
|
27459
27631
|
/** Callback to write content into the scrollable TUI waterfall area (wired from interactive.ts) */
|
|
27460
27632
|
writeContent = null;
|
|
27461
27633
|
/** Media cache — fileUniqueId → cache entry */
|
|
@@ -27495,6 +27667,14 @@ with summary "no_reply" to silently skip without responding.
|
|
|
27495
27667
|
setCallUrlGetter(getter) {
|
|
27496
27668
|
this.callUrlGetter = getter;
|
|
27497
27669
|
}
|
|
27670
|
+
/** Register callback to start a call session (returns URL or null) */
|
|
27671
|
+
setCallStarter(starter) {
|
|
27672
|
+
this.callStarter = starter;
|
|
27673
|
+
}
|
|
27674
|
+
/** Register callback to stop a call session */
|
|
27675
|
+
setCallStopper(stopper) {
|
|
27676
|
+
this.callStopper = stopper;
|
|
27677
|
+
}
|
|
27498
27678
|
/** Register callback to write content into the scrollable TUI area (status bar guard) */
|
|
27499
27679
|
setWriteContent(fn) {
|
|
27500
27680
|
this.writeContent = fn;
|
|
@@ -27655,6 +27835,16 @@ with summary "no_reply" to silently skip without responding.
|
|
|
27655
27835
|
const isAdmin = this.isAdminUser(msg);
|
|
27656
27836
|
const toolContext = this.resolveToolContext(msg, isAdmin);
|
|
27657
27837
|
const isAdminDM = toolContext === "telegram-admin-dm";
|
|
27838
|
+
if (msg.text.trim().toLowerCase() === "/hangup" && isAdmin && this.callStopper) {
|
|
27839
|
+
const callUrl = this.callUrlGetter?.();
|
|
27840
|
+
if (callUrl) {
|
|
27841
|
+
await this.callStopper();
|
|
27842
|
+
await this.sendMessage(msg.chatId, "Call session ended.");
|
|
27843
|
+
} else {
|
|
27844
|
+
await this.sendMessage(msg.chatId, "No active call session.");
|
|
27845
|
+
}
|
|
27846
|
+
return;
|
|
27847
|
+
}
|
|
27658
27848
|
if (msg.text.trim().toLowerCase() === "/call" && this.callUrlGetter) {
|
|
27659
27849
|
const callUrl = this.callUrlGetter();
|
|
27660
27850
|
if (callUrl) {
|
|
@@ -27665,6 +27855,20 @@ with summary "no_reply" to silently skip without responding.
|
|
|
27665
27855
|
await this.sendMessage(msg.chatId, "No active call session. Ask the admin to start one with /call.");
|
|
27666
27856
|
return;
|
|
27667
27857
|
}
|
|
27858
|
+
if (this.callStarter) {
|
|
27859
|
+
try {
|
|
27860
|
+
const newUrl = await this.callStarter();
|
|
27861
|
+
if (newUrl) {
|
|
27862
|
+
await this.sendCallButton(msg.chatId, newUrl);
|
|
27863
|
+
} else {
|
|
27864
|
+
await this.sendMessage(msg.chatId, "Failed to start call session.");
|
|
27865
|
+
}
|
|
27866
|
+
} catch (err) {
|
|
27867
|
+
await this.sendMessage(msg.chatId, `Call error: ${err instanceof Error ? err.message : String(err)}`).catch(() => {
|
|
27868
|
+
});
|
|
27869
|
+
}
|
|
27870
|
+
return;
|
|
27871
|
+
}
|
|
27668
27872
|
}
|
|
27669
27873
|
if (isAdminDM && msg.text.startsWith("/") && this.commandHandler) {
|
|
27670
27874
|
const cmdName = msg.text.split(/\s+/)[0].slice(1).toLowerCase();
|
|
@@ -30796,6 +31000,8 @@ Respond concisely and safely. Remember: you are talking to the general public.`;
|
|
|
30796
31000
|
telegramBridge.voiceEnabled = true;
|
|
30797
31001
|
}
|
|
30798
31002
|
telegramBridge.setCallUrlGetter(() => voiceSession?.tunnelUrl ?? null);
|
|
31003
|
+
telegramBridge.setCallStarter(async () => commandCtx.callStart());
|
|
31004
|
+
telegramBridge.setCallStopper(async () => commandCtx.callStop());
|
|
30799
31005
|
telegramBridge.setCommandHandler(async (input) => {
|
|
30800
31006
|
const captured = [];
|
|
30801
31007
|
const origWrite = process.stdout.write;
|
|
@@ -30963,20 +31169,66 @@ Respond concisely and safely. Remember: you are talking to the general public.`;
|
|
|
30963
31169
|
return text ? `Stopped. Last transcript: "${text}"` : "Stopped listening.";
|
|
30964
31170
|
},
|
|
30965
31171
|
// Voice call session — standalone cloudflared tunnel for /call
|
|
31172
|
+
// Wires: WebSocket audio → ASR transcription → agent input → TTS → WebSocket broadcast
|
|
30966
31173
|
async callStart() {
|
|
30967
31174
|
if (voiceSession?.isActive) {
|
|
30968
31175
|
return voiceSession.tunnelUrl;
|
|
30969
31176
|
}
|
|
30970
31177
|
voiceSession = new VoiceSession();
|
|
31178
|
+
const callState = { transcriber: null };
|
|
31179
|
+
const engine = getListenEngine();
|
|
31180
|
+
voiceSession.onUserAudio = (pcmChunk, userId) => {
|
|
31181
|
+
if (callState.transcriber)
|
|
31182
|
+
callState.transcriber.write(pcmChunk);
|
|
31183
|
+
};
|
|
30971
31184
|
voiceSession.on("userConnected", (id, username) => {
|
|
30972
31185
|
writeContent(() => renderVoiceSessionUser("connected", username));
|
|
31186
|
+
if (!callState.transcriber) {
|
|
31187
|
+
engine.createCallTranscriber().then((t) => {
|
|
31188
|
+
if (!t || !voiceSession?.isActive)
|
|
31189
|
+
return;
|
|
31190
|
+
callState.transcriber = t;
|
|
31191
|
+
callState.transcriber.on("transcript", (evt) => {
|
|
31192
|
+
if (!evt.text?.trim() || !voiceSession?.isActive)
|
|
31193
|
+
return;
|
|
31194
|
+
const text = evt.text.trim();
|
|
31195
|
+
writeContent(() => renderVoiceSessionTranscript("user", text));
|
|
31196
|
+
voiceSession?.sendTranscript("user", text);
|
|
31197
|
+
if (evt.isFinal) {
|
|
31198
|
+
rl.write(text + "\n");
|
|
31199
|
+
}
|
|
31200
|
+
});
|
|
31201
|
+
writeContent(() => renderInfo("Call ASR ready \u2014 listening for speech"));
|
|
31202
|
+
}).catch((err) => {
|
|
31203
|
+
writeContent(() => renderWarning(`Call ASR unavailable: ${err instanceof Error ? err.message : String(err)}`));
|
|
31204
|
+
});
|
|
31205
|
+
}
|
|
30973
31206
|
});
|
|
30974
31207
|
voiceSession.on("userDisconnected", (id) => {
|
|
30975
31208
|
writeContent(() => renderVoiceSessionUser("disconnected", id));
|
|
30976
31209
|
});
|
|
31210
|
+
voiceSession.on("idle_timeout", () => {
|
|
31211
|
+
writeContent(() => renderWarning("Call session auto-closed (1 min idle \u2014 no users connected)"));
|
|
31212
|
+
if (callState.transcriber) {
|
|
31213
|
+
callState.transcriber.stop();
|
|
31214
|
+
callState.transcriber = null;
|
|
31215
|
+
}
|
|
31216
|
+
voiceSession = null;
|
|
31217
|
+
});
|
|
30977
31218
|
try {
|
|
30978
31219
|
const tunnelUrl = await voiceSession.start();
|
|
30979
31220
|
writeContent(() => renderVoiceSessionStart(tunnelUrl));
|
|
31221
|
+
if (voiceEngine.enabled && voiceEngine.ready) {
|
|
31222
|
+
const session = voiceSession;
|
|
31223
|
+
voiceEngine.onPCMOutput = (pcm, sampleRate) => {
|
|
31224
|
+
if (session?.isActive) {
|
|
31225
|
+
session.sendSpeakingState(true);
|
|
31226
|
+
session.sendAudioToClients(pcm);
|
|
31227
|
+
const durationMs = pcm.length / 2 / sampleRate * 1e3;
|
|
31228
|
+
setTimeout(() => session.sendSpeakingState(false), durationMs);
|
|
31229
|
+
}
|
|
31230
|
+
};
|
|
31231
|
+
}
|
|
30980
31232
|
if (telegramBridge?.isActive && savedSettings.telegramAdmin) {
|
|
30981
31233
|
const adminChatId = parseInt(savedSettings.telegramAdmin, 10);
|
|
30982
31234
|
if (!isNaN(adminChatId)) {
|
|
@@ -30987,6 +31239,8 @@ Respond concisely and safely. Remember: you are talking to the general public.`;
|
|
|
30987
31239
|
return tunnelUrl;
|
|
30988
31240
|
} catch (err) {
|
|
30989
31241
|
writeContent(() => renderWarning(`Voice session failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
31242
|
+
callState.transcriber?.stop();
|
|
31243
|
+
callState.transcriber = null;
|
|
30990
31244
|
voiceSession = null;
|
|
30991
31245
|
return null;
|
|
30992
31246
|
}
|
|
@@ -30997,6 +31251,7 @@ Respond concisely and safely. Remember: you are talking to the general public.`;
|
|
|
30997
31251
|
await voiceSession.stop();
|
|
30998
31252
|
writeContent(() => renderVoiceSessionStop(runtime));
|
|
30999
31253
|
voiceSession = null;
|
|
31254
|
+
voiceEngine.onPCMOutput = null;
|
|
31000
31255
|
}
|
|
31001
31256
|
},
|
|
31002
31257
|
isCallActive() {
|
|
@@ -31472,13 +31727,13 @@ NEW TASK: ${fullInput}`;
|
|
|
31472
31727
|
writeContent(() => renderError(errMsg));
|
|
31473
31728
|
if (failureStore) {
|
|
31474
31729
|
try {
|
|
31475
|
-
const { createHash:
|
|
31730
|
+
const { createHash: createHash3 } = await import("node:crypto");
|
|
31476
31731
|
failureStore.insert({
|
|
31477
31732
|
taskId: "",
|
|
31478
31733
|
sessionId: `${Date.now()}`,
|
|
31479
31734
|
repoRoot,
|
|
31480
31735
|
failureType: "runtime-error",
|
|
31481
|
-
fingerprint:
|
|
31736
|
+
fingerprint: createHash3("sha256").update(errMsg.slice(0, 200)).digest("hex").slice(0, 16),
|
|
31482
31737
|
filePath: null,
|
|
31483
31738
|
errorMessage: errMsg.slice(0, 500),
|
|
31484
31739
|
context: null,
|
|
@@ -31739,7 +31994,7 @@ var init_run = __esm({
|
|
|
31739
31994
|
import { glob } from "glob";
|
|
31740
31995
|
import ignore from "ignore";
|
|
31741
31996
|
import { readFile as readFile14, stat as stat4 } from "node:fs/promises";
|
|
31742
|
-
import { createHash } from "node:crypto";
|
|
31997
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
31743
31998
|
import { join as join41, relative as relative3, extname as extname10, basename as basename15 } from "node:path";
|
|
31744
31999
|
var DEFAULT_EXCLUDE, LANGUAGE_MAP, CodebaseIndexer;
|
|
31745
32000
|
var init_codebase_indexer = __esm({
|
|
@@ -31805,7 +32060,7 @@ var init_codebase_indexer = __esm({
|
|
|
31805
32060
|
if (fileStat.size > this.config.maxFileSize)
|
|
31806
32061
|
continue;
|
|
31807
32062
|
const content = await readFile14(fullPath);
|
|
31808
|
-
const hash =
|
|
32063
|
+
const hash = createHash2("sha256").update(content).digest("hex");
|
|
31809
32064
|
const ext = extname10(relativePath);
|
|
31810
32065
|
indexed.push({
|
|
31811
32066
|
path: fullPath,
|
package/package.json
CHANGED