accelr8 3.0.0 → 3.0.2
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/chunks/{chunk-SF6NNSYZ.js → chunk-WRSSNAIF.js} +6 -2
- package/dist/chunks/{client-7BVK2DEW.js → client-W4563KDL.js} +473 -38
- package/dist/chunks/install-DL5WLVXN.js +72 -0
- package/dist/chunks/{server-ZP2DV4IO.js → server-OQNHWL3Z.js} +53 -8
- package/dist/cli.js +3 -3
- package/package.json +4 -3
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/protocol.ts
|
|
4
|
-
var VERSION = "3.0.0";
|
|
4
|
+
var VERSION = true ? "3.0.2" : "3.0.2";
|
|
5
5
|
var PROTO = 3;
|
|
6
6
|
var PORT = 8787;
|
|
7
7
|
var DEFAULT_SERVER = "wss://accelr8-canvas.patonchain.workers.dev";
|
|
@@ -36,7 +36,11 @@ var LIMITS = {
|
|
|
36
36
|
agentQueueCap: 5,
|
|
37
37
|
agentMaxDepth: 3,
|
|
38
38
|
agentBudgetPerTrigger: 5,
|
|
39
|
-
agentRepeatPerChain: 2
|
|
39
|
+
agentRepeatPerChain: 2,
|
|
40
|
+
voiceFramesPerSec: 50,
|
|
41
|
+
// ~25/s real, 2x headroom; exempt from framesPerWindow
|
|
42
|
+
voiceFrameMaxB64: 4096
|
|
43
|
+
// a 40ms s16le mono chunk is ~1.7KB base64; cap slack
|
|
40
44
|
};
|
|
41
45
|
var DEFAULT_ACCESS = { mode: "owner", users: [] };
|
|
42
46
|
function encode(msg) {
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
VERSION,
|
|
8
8
|
decode,
|
|
9
9
|
encode
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-WRSSNAIF.js";
|
|
11
11
|
import {
|
|
12
12
|
DEFAULT_MODEL,
|
|
13
13
|
addAccount,
|
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
// src/client.ts
|
|
26
26
|
import tk from "terminal-kit";
|
|
27
27
|
import { WebSocket } from "ws";
|
|
28
|
-
import { spawn } from "node:child_process";
|
|
28
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
29
29
|
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
30
30
|
import { homedir } from "node:os";
|
|
31
31
|
import { join } from "node:path";
|
|
@@ -1173,6 +1173,295 @@ function microLine(w, engine, frame2, info) {
|
|
|
1173
1173
|
return clampTo(tiny, Math.max(0, w));
|
|
1174
1174
|
}
|
|
1175
1175
|
|
|
1176
|
+
// src/voice.ts
|
|
1177
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
1178
|
+
import fs from "node:fs";
|
|
1179
|
+
import path from "node:path";
|
|
1180
|
+
var SAMPLE_RATE = 16e3;
|
|
1181
|
+
var CHUNK_BYTES = 1280;
|
|
1182
|
+
var KILL_GRACE_MS = 300;
|
|
1183
|
+
var SPEAKER_IDLE_MS = 2e3;
|
|
1184
|
+
var DRAIN_KILL_MS = 2e3;
|
|
1185
|
+
var MAX_PLAYERS = 8;
|
|
1186
|
+
var STDERR_TAIL = 400;
|
|
1187
|
+
var VERIFY_MIN_BYTES = SAMPLE_RATE * 2 / 10;
|
|
1188
|
+
var PLAYBACK_ARGS = [
|
|
1189
|
+
"-hide_banner",
|
|
1190
|
+
"-loglevel",
|
|
1191
|
+
"error",
|
|
1192
|
+
"-nodisp",
|
|
1193
|
+
"-fflags",
|
|
1194
|
+
"nobuffer",
|
|
1195
|
+
"-flags",
|
|
1196
|
+
"low_delay",
|
|
1197
|
+
"-f",
|
|
1198
|
+
"s16le",
|
|
1199
|
+
"-ar",
|
|
1200
|
+
String(SAMPLE_RATE),
|
|
1201
|
+
"-ch_layout",
|
|
1202
|
+
"mono",
|
|
1203
|
+
"-i",
|
|
1204
|
+
"-"
|
|
1205
|
+
];
|
|
1206
|
+
function detectVoiceTools() {
|
|
1207
|
+
const ffmpegPath = which("ffmpeg");
|
|
1208
|
+
const ffplayPath = which("ffplay");
|
|
1209
|
+
const out = { capture: !!ffmpegPath, playback: !!ffplayPath };
|
|
1210
|
+
if (ffmpegPath) out.ffmpegPath = ffmpegPath;
|
|
1211
|
+
if (ffplayPath) out.ffplayPath = ffplayPath;
|
|
1212
|
+
if (!ffmpegPath || !ffplayPath) {
|
|
1213
|
+
const missing = [ffmpegPath ? "" : "ffmpeg", ffplayPath ? "" : "ffplay"].filter(Boolean).join(" + ");
|
|
1214
|
+
out.reason = `${missing} not found on PATH (voice needs ffmpeg; brew/apt install ffmpeg)`;
|
|
1215
|
+
}
|
|
1216
|
+
return out;
|
|
1217
|
+
}
|
|
1218
|
+
function which(bin) {
|
|
1219
|
+
const exts = process.platform === "win32" ? [".exe", ".cmd", ".bat", ""] : [""];
|
|
1220
|
+
for (const dir of (process.env.PATH ?? "").split(path.delimiter).filter(Boolean)) {
|
|
1221
|
+
for (const ext of exts) {
|
|
1222
|
+
const candidate = path.join(dir, bin + ext);
|
|
1223
|
+
try {
|
|
1224
|
+
if (!fs.existsSync(candidate)) continue;
|
|
1225
|
+
fs.accessSync(candidate, fs.constants.X_OK);
|
|
1226
|
+
if (fs.statSync(candidate).isFile()) return candidate;
|
|
1227
|
+
} catch {
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
return void 0;
|
|
1232
|
+
}
|
|
1233
|
+
function captureArgs(platform, device) {
|
|
1234
|
+
const base = ["-hide_banner", "-loglevel", "error"];
|
|
1235
|
+
const output = ["-ac", "1", "-ar", String(SAMPLE_RATE), "-f", "s16le", "-"];
|
|
1236
|
+
if (platform === "darwin") return [...base, "-f", "avfoundation", "-i", ":" + (device ?? "default"), ...output];
|
|
1237
|
+
if (platform === "win32") return [...base, "-f", "dshow", "-i", "audio=" + (device ?? "default"), ...output];
|
|
1238
|
+
return [...base, "-f", "alsa", "-i", device ?? "default", ...output];
|
|
1239
|
+
}
|
|
1240
|
+
function chunkPcm(buf, chunkBytes) {
|
|
1241
|
+
if (!Number.isInteger(chunkBytes) || chunkBytes <= 0) throw new RangeError("chunkBytes must be a positive integer");
|
|
1242
|
+
const b = Buffer.isBuffer(buf) ? buf : Buffer.from(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
1243
|
+
const out = [];
|
|
1244
|
+
for (let i = 0; i < b.length; i += chunkBytes) out.push(b.subarray(i, i + chunkBytes).toString("base64"));
|
|
1245
|
+
return out;
|
|
1246
|
+
}
|
|
1247
|
+
var VoiceCapture = class {
|
|
1248
|
+
constructor(tools) {
|
|
1249
|
+
this.tools = tools;
|
|
1250
|
+
}
|
|
1251
|
+
tools;
|
|
1252
|
+
session = null;
|
|
1253
|
+
get active() {
|
|
1254
|
+
return this.session !== null;
|
|
1255
|
+
}
|
|
1256
|
+
/** Spawn ffmpeg mic->PCM and stream ~40ms base64 chunks to onChunk.
|
|
1257
|
+
* No-op while a capture is already active. */
|
|
1258
|
+
start(opts) {
|
|
1259
|
+
if (this.session) return;
|
|
1260
|
+
if (!this.tools.capture || !this.tools.ffmpegPath) {
|
|
1261
|
+
const why = this.tools.reason ?? "ffmpeg not found on PATH";
|
|
1262
|
+
queueMicrotask(() => opts.onError(why));
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1265
|
+
this.session = this.spawnSession(opts, captureArgs(process.platform, opts.device), "primary");
|
|
1266
|
+
}
|
|
1267
|
+
/** Flush the sub-chunk remainder, then SIGINT -> SIGKILL after 300ms. */
|
|
1268
|
+
stop() {
|
|
1269
|
+
const s = this.session;
|
|
1270
|
+
if (!s) return;
|
|
1271
|
+
this.session = null;
|
|
1272
|
+
s.stopped = true;
|
|
1273
|
+
if (s.buf.length) {
|
|
1274
|
+
s.onChunk(s.buf.toString("base64"));
|
|
1275
|
+
s.buf = Buffer.alloc(0);
|
|
1276
|
+
}
|
|
1277
|
+
try {
|
|
1278
|
+
s.child.kill("SIGINT");
|
|
1279
|
+
} catch {
|
|
1280
|
+
}
|
|
1281
|
+
const t = setTimeout(() => {
|
|
1282
|
+
try {
|
|
1283
|
+
s.child.kill("SIGKILL");
|
|
1284
|
+
} catch {
|
|
1285
|
+
}
|
|
1286
|
+
}, KILL_GRACE_MS);
|
|
1287
|
+
t.unref();
|
|
1288
|
+
}
|
|
1289
|
+
spawnSession(opts, args, backend) {
|
|
1290
|
+
let child;
|
|
1291
|
+
try {
|
|
1292
|
+
child = spawn(this.tools.ffmpegPath, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
1293
|
+
} catch (e) {
|
|
1294
|
+
queueMicrotask(() => opts.onError("mic capture failed to start: " + errMsg(e)));
|
|
1295
|
+
return null;
|
|
1296
|
+
}
|
|
1297
|
+
const s = { child, onChunk: opts.onChunk, buf: Buffer.alloc(0), stderrTail: "", produced: false, stopped: false };
|
|
1298
|
+
child.stdout?.on("data", (chunk) => {
|
|
1299
|
+
if (s.stopped) return;
|
|
1300
|
+
s.produced = true;
|
|
1301
|
+
s.buf = s.buf.length ? Buffer.concat([s.buf, chunk]) : chunk;
|
|
1302
|
+
const whole = Math.floor(s.buf.length / CHUNK_BYTES) * CHUNK_BYTES;
|
|
1303
|
+
if (whole === 0) return;
|
|
1304
|
+
for (const b64 of chunkPcm(s.buf.subarray(0, whole), CHUNK_BYTES)) opts.onChunk(b64);
|
|
1305
|
+
s.buf = s.buf.subarray(whole);
|
|
1306
|
+
});
|
|
1307
|
+
child.stderr?.setEncoding("utf8");
|
|
1308
|
+
child.stderr?.on("data", (c) => {
|
|
1309
|
+
s.stderrTail = (s.stderrTail + c).slice(-STDERR_TAIL);
|
|
1310
|
+
});
|
|
1311
|
+
child.on("error", (e) => {
|
|
1312
|
+
if (s.stopped || this.session !== s) return;
|
|
1313
|
+
this.session = null;
|
|
1314
|
+
opts.onError(e.code === "ENOENT" ? "ffmpeg not found" : "mic capture error: " + errMsg(e));
|
|
1315
|
+
});
|
|
1316
|
+
child.on("close", (code) => {
|
|
1317
|
+
if (s.stopped || this.session !== s) return;
|
|
1318
|
+
if (backend === "primary" && process.platform === "linux" && !s.produced && code !== 0) {
|
|
1319
|
+
const pulse = captureArgs("linux", opts.device).map((a) => a === "alsa" ? "pulse" : a);
|
|
1320
|
+
this.session = this.spawnSession(opts, pulse, "pulse");
|
|
1321
|
+
return;
|
|
1322
|
+
}
|
|
1323
|
+
this.session = null;
|
|
1324
|
+
const tail = s.stderrTail.trim();
|
|
1325
|
+
opts.onError(code === 0 ? "mic capture ended unexpectedly" : "mic capture failed: " + (tail || `ffmpeg exited with code ${code}`));
|
|
1326
|
+
});
|
|
1327
|
+
return s;
|
|
1328
|
+
}
|
|
1329
|
+
};
|
|
1330
|
+
var VoicePlayback = class {
|
|
1331
|
+
constructor(tools) {
|
|
1332
|
+
this.tools = tools;
|
|
1333
|
+
}
|
|
1334
|
+
tools;
|
|
1335
|
+
players = /* @__PURE__ */ new Map();
|
|
1336
|
+
/** ids with an active player */
|
|
1337
|
+
get speakers() {
|
|
1338
|
+
return [...this.players.keys()];
|
|
1339
|
+
}
|
|
1340
|
+
/** Feed a received chunk; lazily spawns one ffplay per speaker. Chunks
|
|
1341
|
+
* are dropped silently when playback is unavailable, the speaker cap
|
|
1342
|
+
* is hit, or a player cannot be (re)spawned. */
|
|
1343
|
+
feed(speakerId, b64) {
|
|
1344
|
+
if (!this.tools.playback || !this.tools.ffplayPath) return;
|
|
1345
|
+
const bytes = Buffer.from(b64, "base64");
|
|
1346
|
+
if (bytes.length === 0) return;
|
|
1347
|
+
let p = this.players.get(speakerId);
|
|
1348
|
+
if (p && !p.alive) {
|
|
1349
|
+
this.reap(speakerId, p);
|
|
1350
|
+
p = void 0;
|
|
1351
|
+
}
|
|
1352
|
+
if (!p) {
|
|
1353
|
+
if (this.players.size >= MAX_PLAYERS) return;
|
|
1354
|
+
const spawned = this.spawnPlayer(speakerId);
|
|
1355
|
+
if (!spawned) return;
|
|
1356
|
+
p = spawned;
|
|
1357
|
+
}
|
|
1358
|
+
if (!this.write(p, bytes)) {
|
|
1359
|
+
this.reap(speakerId, p);
|
|
1360
|
+
const fresh = this.players.size < MAX_PLAYERS ? this.spawnPlayer(speakerId) : null;
|
|
1361
|
+
if (!fresh || !this.write(fresh, bytes)) return;
|
|
1362
|
+
p = fresh;
|
|
1363
|
+
}
|
|
1364
|
+
this.touch(speakerId, p);
|
|
1365
|
+
}
|
|
1366
|
+
/** Graceful close for one speaker (voiceStop): end stdin so ffplay drains
|
|
1367
|
+
* its buffered audio, then a SIGKILL backstop after 2s (we dropped -autoexit,
|
|
1368
|
+
* so the backstop is what actually reaps the idle player). */
|
|
1369
|
+
end(speakerId) {
|
|
1370
|
+
const p = this.players.get(speakerId);
|
|
1371
|
+
if (!p) return;
|
|
1372
|
+
this.players.delete(speakerId);
|
|
1373
|
+
if (p.idleTimer) {
|
|
1374
|
+
clearTimeout(p.idleTimer);
|
|
1375
|
+
p.idleTimer = null;
|
|
1376
|
+
}
|
|
1377
|
+
try {
|
|
1378
|
+
p.child.stdin?.end();
|
|
1379
|
+
} catch {
|
|
1380
|
+
}
|
|
1381
|
+
const t = setTimeout(() => {
|
|
1382
|
+
try {
|
|
1383
|
+
p.child.kill("SIGKILL");
|
|
1384
|
+
} catch {
|
|
1385
|
+
}
|
|
1386
|
+
}, DRAIN_KILL_MS);
|
|
1387
|
+
t.unref();
|
|
1388
|
+
}
|
|
1389
|
+
/** Hard cleanup on quit: kill every player immediately. */
|
|
1390
|
+
stopAll() {
|
|
1391
|
+
for (const [, p] of this.players) {
|
|
1392
|
+
if (p.idleTimer) {
|
|
1393
|
+
clearTimeout(p.idleTimer);
|
|
1394
|
+
p.idleTimer = null;
|
|
1395
|
+
}
|
|
1396
|
+
p.alive = false;
|
|
1397
|
+
try {
|
|
1398
|
+
p.child.kill("SIGKILL");
|
|
1399
|
+
} catch {
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
this.players.clear();
|
|
1403
|
+
}
|
|
1404
|
+
spawnPlayer(speakerId) {
|
|
1405
|
+
let child;
|
|
1406
|
+
try {
|
|
1407
|
+
child = spawn(this.tools.ffplayPath, PLAYBACK_ARGS, { stdio: ["pipe", "ignore", "ignore"] });
|
|
1408
|
+
} catch {
|
|
1409
|
+
return null;
|
|
1410
|
+
}
|
|
1411
|
+
const p = { child, alive: true, idleTimer: null };
|
|
1412
|
+
child.stdin?.on("error", () => {
|
|
1413
|
+
p.alive = false;
|
|
1414
|
+
});
|
|
1415
|
+
child.on("error", () => {
|
|
1416
|
+
p.alive = false;
|
|
1417
|
+
if (this.players.get(speakerId) === p) this.reap(speakerId, p);
|
|
1418
|
+
});
|
|
1419
|
+
child.on("close", () => {
|
|
1420
|
+
p.alive = false;
|
|
1421
|
+
if (this.players.get(speakerId) === p) {
|
|
1422
|
+
if (p.idleTimer) {
|
|
1423
|
+
clearTimeout(p.idleTimer);
|
|
1424
|
+
p.idleTimer = null;
|
|
1425
|
+
}
|
|
1426
|
+
this.players.delete(speakerId);
|
|
1427
|
+
}
|
|
1428
|
+
});
|
|
1429
|
+
this.players.set(speakerId, p);
|
|
1430
|
+
return p;
|
|
1431
|
+
}
|
|
1432
|
+
write(p, bytes) {
|
|
1433
|
+
const sin = p.child.stdin;
|
|
1434
|
+
if (!p.alive || !sin || sin.destroyed || !sin.writable) return false;
|
|
1435
|
+
try {
|
|
1436
|
+
sin.write(bytes);
|
|
1437
|
+
return true;
|
|
1438
|
+
} catch {
|
|
1439
|
+
p.alive = false;
|
|
1440
|
+
return false;
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
touch(speakerId, p) {
|
|
1444
|
+
if (p.idleTimer) clearTimeout(p.idleTimer);
|
|
1445
|
+
p.idleTimer = setTimeout(() => this.end(speakerId), SPEAKER_IDLE_MS);
|
|
1446
|
+
p.idleTimer.unref();
|
|
1447
|
+
}
|
|
1448
|
+
reap(speakerId, p) {
|
|
1449
|
+
if (p.idleTimer) {
|
|
1450
|
+
clearTimeout(p.idleTimer);
|
|
1451
|
+
p.idleTimer = null;
|
|
1452
|
+
}
|
|
1453
|
+
p.alive = false;
|
|
1454
|
+
try {
|
|
1455
|
+
p.child.kill("SIGKILL");
|
|
1456
|
+
} catch {
|
|
1457
|
+
}
|
|
1458
|
+
if (this.players.get(speakerId) === p) this.players.delete(speakerId);
|
|
1459
|
+
}
|
|
1460
|
+
};
|
|
1461
|
+
function errMsg(e) {
|
|
1462
|
+
return e instanceof Error ? e.message : String(e);
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1176
1465
|
// src/client.ts
|
|
1177
1466
|
var term = tk.terminal;
|
|
1178
1467
|
var ScreenBuffer = tk.ScreenBuffer;
|
|
@@ -1223,26 +1512,135 @@ var overlay = null;
|
|
|
1223
1512
|
var accessAgentId = null;
|
|
1224
1513
|
var overlayFocus = 0;
|
|
1225
1514
|
var updateInfo = null;
|
|
1226
|
-
var
|
|
1227
|
-
var
|
|
1515
|
+
var task = null;
|
|
1516
|
+
var cancelTask = null;
|
|
1228
1517
|
function startUpdate() {
|
|
1229
|
-
if (
|
|
1230
|
-
|
|
1518
|
+
if (task && task.phase === "running") return;
|
|
1519
|
+
task = { title: "updating accelr8", phase: "running", line: "starting npm\u2026", okLabel: "quit now \xB7 enter", onOk: () => shutdown() };
|
|
1231
1520
|
import("./update-UEPNGB6Y.js").then(({ runUpdate }) => {
|
|
1232
|
-
|
|
1521
|
+
cancelTask = runUpdate({
|
|
1233
1522
|
onLine: (l) => {
|
|
1234
|
-
if (
|
|
1523
|
+
if (task) task.line = l.slice(0, 120);
|
|
1235
1524
|
},
|
|
1236
1525
|
onDone: (ok, message) => {
|
|
1237
|
-
|
|
1238
|
-
|
|
1526
|
+
if (task) {
|
|
1527
|
+
task.phase = "done";
|
|
1528
|
+
task.ok = ok;
|
|
1529
|
+
task.line = message;
|
|
1530
|
+
}
|
|
1531
|
+
cancelTask = null;
|
|
1239
1532
|
if (ok) updateInfo = null;
|
|
1240
1533
|
}
|
|
1241
1534
|
});
|
|
1242
1535
|
}).catch(() => {
|
|
1243
|
-
|
|
1536
|
+
if (task) {
|
|
1537
|
+
task.phase = "done";
|
|
1538
|
+
task.ok = false;
|
|
1539
|
+
task.line = "could not load the updater \xB7 npm i -g accelr8@latest";
|
|
1540
|
+
}
|
|
1541
|
+
});
|
|
1542
|
+
}
|
|
1543
|
+
function startVoiceInstall() {
|
|
1544
|
+
import("./install-DL5WLVXN.js").then(({ ffmpegInstallPlan, runCommand }) => {
|
|
1545
|
+
const plan = ffmpegInstallPlan();
|
|
1546
|
+
if (!plan.auto) {
|
|
1547
|
+
state.status = `voice needs ffmpeg \xB7 ${plan.manual}`;
|
|
1548
|
+
return;
|
|
1549
|
+
}
|
|
1550
|
+
const auto = plan.auto;
|
|
1551
|
+
task = {
|
|
1552
|
+
title: "enable voice",
|
|
1553
|
+
phase: "confirm",
|
|
1554
|
+
line: `voice needs ffmpeg \xB7 ${auto.label} \xB7 press enter`,
|
|
1555
|
+
run: () => {
|
|
1556
|
+
if (!task) return;
|
|
1557
|
+
task.phase = "running";
|
|
1558
|
+
task.line = "installing ffmpeg\u2026";
|
|
1559
|
+
cancelTask = runCommand(auto.cmd, auto.args, {
|
|
1560
|
+
onLine: (l) => {
|
|
1561
|
+
if (task) task.line = l.slice(0, 120);
|
|
1562
|
+
},
|
|
1563
|
+
onDone: (ok, message) => {
|
|
1564
|
+
if (task) {
|
|
1565
|
+
task.phase = "done";
|
|
1566
|
+
task.ok = ok;
|
|
1567
|
+
task.line = ok ? "ffmpeg ready \xB7 voice is on" : message + " \xB7 " + auto.label;
|
|
1568
|
+
task.okLabel = ok ? "start voice \xB7 enter" : void 0;
|
|
1569
|
+
}
|
|
1570
|
+
cancelTask = null;
|
|
1571
|
+
}
|
|
1572
|
+
});
|
|
1573
|
+
},
|
|
1574
|
+
onOk: () => {
|
|
1575
|
+
voiceTools = null;
|
|
1576
|
+
task = null;
|
|
1577
|
+
toggleVoice();
|
|
1578
|
+
}
|
|
1579
|
+
// re-detect + join
|
|
1580
|
+
};
|
|
1581
|
+
}).catch(() => {
|
|
1582
|
+
state.status = "could not load the installer";
|
|
1244
1583
|
});
|
|
1245
1584
|
}
|
|
1585
|
+
var voiceTools = null;
|
|
1586
|
+
var voiceOn = false;
|
|
1587
|
+
var voiceTransmit = false;
|
|
1588
|
+
var voiceCapture = null;
|
|
1589
|
+
var voicePlayback = null;
|
|
1590
|
+
function toggleVoice() {
|
|
1591
|
+
if (voiceOn) return leaveVoice();
|
|
1592
|
+
if (!state.project) {
|
|
1593
|
+
state.status = "join a project first";
|
|
1594
|
+
return;
|
|
1595
|
+
}
|
|
1596
|
+
voiceTools = voiceTools ?? detectVoiceTools();
|
|
1597
|
+
if (!voiceTools.capture && !voiceTools.playback) {
|
|
1598
|
+
startVoiceInstall();
|
|
1599
|
+
return;
|
|
1600
|
+
}
|
|
1601
|
+
voicePlayback = new VoicePlayback(voiceTools);
|
|
1602
|
+
voiceCapture = new VoiceCapture(voiceTools);
|
|
1603
|
+
voiceOn = true;
|
|
1604
|
+
voiceTransmit = false;
|
|
1605
|
+
sendMsg({ t: "voiceJoin" });
|
|
1606
|
+
state.status = voiceTools.capture ? "voice on \xB7 /talk (or ctrl-g) to speak" : "voice on \xB7 listen only (no mic found)";
|
|
1607
|
+
}
|
|
1608
|
+
function leaveVoice() {
|
|
1609
|
+
if (!voiceOn) return;
|
|
1610
|
+
voiceCapture?.stop();
|
|
1611
|
+
voicePlayback?.stopAll();
|
|
1612
|
+
voiceCapture = null;
|
|
1613
|
+
voicePlayback = null;
|
|
1614
|
+
voiceOn = false;
|
|
1615
|
+
voiceTransmit = false;
|
|
1616
|
+
state.voicePeers = [];
|
|
1617
|
+
sendMsg({ t: "voiceLeave" });
|
|
1618
|
+
state.status = "voice off";
|
|
1619
|
+
}
|
|
1620
|
+
function toggleTalk() {
|
|
1621
|
+
if (!voiceOn || !voiceCapture) return;
|
|
1622
|
+
if (!voiceTools?.capture) {
|
|
1623
|
+
state.status = "no mic \xB7 install ffmpeg to talk";
|
|
1624
|
+
return;
|
|
1625
|
+
}
|
|
1626
|
+
if (voiceTransmit) {
|
|
1627
|
+
voiceCapture.stop();
|
|
1628
|
+
voiceTransmit = false;
|
|
1629
|
+
state.status = "muted \xB7 /talk (or ctrl-g) to speak";
|
|
1630
|
+
} else {
|
|
1631
|
+
voiceTransmit = true;
|
|
1632
|
+
state.status = "\u25CF live \xB7 /mute (or ctrl-g) to stop";
|
|
1633
|
+
voiceCapture.start({
|
|
1634
|
+
onChunk: (b64) => {
|
|
1635
|
+
if (voiceTransmit) sendMsg({ t: "voiceFrame", data: b64 });
|
|
1636
|
+
},
|
|
1637
|
+
onError: (m) => {
|
|
1638
|
+
voiceTransmit = false;
|
|
1639
|
+
state.status = "mic error \xB7 " + m.slice(0, 44);
|
|
1640
|
+
}
|
|
1641
|
+
});
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1246
1644
|
function openHelp() {
|
|
1247
1645
|
overlay = "help";
|
|
1248
1646
|
overlayFocus = 0;
|
|
@@ -1253,6 +1651,7 @@ function openSettings() {
|
|
|
1253
1651
|
}
|
|
1254
1652
|
function goHome() {
|
|
1255
1653
|
if (state.project) {
|
|
1654
|
+
if (voiceOn) leaveVoice();
|
|
1256
1655
|
overlay = null;
|
|
1257
1656
|
state.screen = "home";
|
|
1258
1657
|
sendMsg({ t: "tab", tab: "home" });
|
|
@@ -1270,6 +1669,7 @@ var state = {
|
|
|
1270
1669
|
sessions: [],
|
|
1271
1670
|
agents: [],
|
|
1272
1671
|
secrets: [],
|
|
1672
|
+
voicePeers: [],
|
|
1273
1673
|
status: "",
|
|
1274
1674
|
modal: null
|
|
1275
1675
|
};
|
|
@@ -1664,6 +2064,12 @@ function handleServer(msg) {
|
|
|
1664
2064
|
case "secrets":
|
|
1665
2065
|
state.secrets = Array.isArray(msg.list) ? msg.list : [];
|
|
1666
2066
|
break;
|
|
2067
|
+
case "voice":
|
|
2068
|
+
state.voicePeers = Array.isArray(msg.peers) ? msg.peers : [];
|
|
2069
|
+
break;
|
|
2070
|
+
case "voiceFrame":
|
|
2071
|
+
if (voiceOn) voicePlayback?.feed(msg.sid, msg.data);
|
|
2072
|
+
break;
|
|
1667
2073
|
case "error": {
|
|
1668
2074
|
if (msg.code === "chatRate" || msg.code === "msgTooLarge") {
|
|
1669
2075
|
const failed = msg.ref ? chatLog.failOptimistic(msg.ref) : null;
|
|
@@ -1851,6 +2257,9 @@ function __curTheme() {
|
|
|
1851
2257
|
function __setUpdate(latest, current = "3.0.0") {
|
|
1852
2258
|
updateInfo = latest ? { latest, current } : null;
|
|
1853
2259
|
}
|
|
2260
|
+
function __taskDemo(phase, ok = true) {
|
|
2261
|
+
task = { title: "enable voice", phase, ok, line: phase === "confirm" ? "voice needs ffmpeg \xB7 brew install ffmpeg \xB7 press enter" : phase === "running" ? "installing ffmpeg\u2026" : ok ? "ffmpeg ready \xB7 voice is on" : "brew exited 1 \xB7 brew install ffmpeg", okLabel: "start voice \xB7 enter" };
|
|
2262
|
+
}
|
|
1854
2263
|
function __closeOverlay() {
|
|
1855
2264
|
overlay = null;
|
|
1856
2265
|
}
|
|
@@ -1936,28 +2345,36 @@ function render() {
|
|
|
1936
2345
|
overlayFocus = clickables.length ? Math.max(0, Math.min(overlayFocus, clickables.length - 1)) : 0;
|
|
1937
2346
|
}
|
|
1938
2347
|
if (state.modal && bigEnough) renderModal();
|
|
1939
|
-
if (
|
|
2348
|
+
if (task && bigEnough) renderTask();
|
|
1940
2349
|
if (screen.dst) screen.draw({ delta: true });
|
|
1941
2350
|
}
|
|
1942
|
-
function
|
|
2351
|
+
function renderTask() {
|
|
1943
2352
|
clickables = [];
|
|
1944
2353
|
const w = W(), h = H();
|
|
1945
2354
|
const bw = Math.min(66, w - 6), bh = 7;
|
|
1946
2355
|
const x0 = Math.floor((w - bw) / 2), y0 = Math.floor((h - bh) / 2);
|
|
1947
2356
|
for (let yy = y0; yy < y0 + bh; yy++) puts(x0, yy, " ".repeat(bw), { bgColor: C.panel });
|
|
1948
|
-
box(x0, y0, x0 + bw - 1, y0 + bh - 1, { color: C.accent, bgColor: C.panel },
|
|
1949
|
-
const
|
|
1950
|
-
if (
|
|
1951
|
-
puts(x0 + 2, y0 + 2,
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
2357
|
+
box(x0, y0, x0 + bw - 1, y0 + bh - 1, { color: C.accent, bgColor: C.panel }, task.title);
|
|
2358
|
+
const t = task;
|
|
2359
|
+
if (t.phase === "confirm") {
|
|
2360
|
+
puts(x0 + 2, y0 + 2, t.line.slice(0, bw - 4), { color: C.bright, bgColor: C.panel, bold: true });
|
|
2361
|
+
button(x0 + 2, y0 + 4, " install \xB7 enter ", { bgColor: C.accent, color: onFill(C.accent), bold: true }, () => t.run?.());
|
|
2362
|
+
button(x0 + 20, y0 + 4, " not now \xB7 esc ", { bgColor: C.tabOff, color: C.text }, () => {
|
|
2363
|
+
task = null;
|
|
2364
|
+
});
|
|
2365
|
+
} else if (t.phase === "running") {
|
|
2366
|
+
puts(x0 + 2, y0 + 2, `${SPINNER[frame % SPINNER.length]} ${t.title}\u2026`, { color: C.bright, bgColor: C.panel, bold: true });
|
|
2367
|
+
puts(x0 + 2, y0 + 4, ("\xB7 " + t.line).slice(0, bw - 4), { color: C.dim, bgColor: C.panel });
|
|
2368
|
+
} else if (t.ok) {
|
|
2369
|
+
puts(x0 + 2, y0 + 2, "\u2713 " + t.line.slice(0, bw - 6), { color: C.good, bgColor: C.panel, bold: true });
|
|
2370
|
+
if (t.okLabel) button(x0 + 2, y0 + 4, ` ${t.okLabel} `, { bgColor: C.good, color: onFill(C.good), bold: true }, () => t.onOk?.());
|
|
2371
|
+
else button(x0 + 2, y0 + 4, " close \xB7 enter ", { bgColor: C.good, color: onFill(C.good), bold: true }, () => {
|
|
2372
|
+
task = null;
|
|
2373
|
+
});
|
|
1957
2374
|
} else {
|
|
1958
|
-
puts(x0 + 2, y0 + 2, "\u2715 " +
|
|
2375
|
+
puts(x0 + 2, y0 + 2, "\u2715 " + t.line.slice(0, bw - 6), { color: C.danger, bgColor: C.panel, bold: true });
|
|
1959
2376
|
button(x0 + 2, y0 + 4, " close \xB7 esc ", { bgColor: C.tabOff, color: C.text }, () => {
|
|
1960
|
-
|
|
2377
|
+
task = null;
|
|
1961
2378
|
});
|
|
1962
2379
|
}
|
|
1963
2380
|
}
|
|
@@ -2239,7 +2656,7 @@ function renderHome(rung) {
|
|
|
2239
2656
|
}
|
|
2240
2657
|
y += 1;
|
|
2241
2658
|
}
|
|
2242
|
-
if (updateInfo && !
|
|
2659
|
+
if (updateInfo && !task) {
|
|
2243
2660
|
puts(2, y, "\u21BB", { color: C.warn, bold: true });
|
|
2244
2661
|
const w2 = W();
|
|
2245
2662
|
const label = ` update to v${updateInfo.latest} \xB7 press enter `;
|
|
@@ -2297,7 +2714,12 @@ function renderStatusBar(rung) {
|
|
|
2297
2714
|
puts(0, sy, " ".repeat(w), { bgColor: C.panel });
|
|
2298
2715
|
const agentBit = state.agents.length ? ` \xB7 \u2726${state.agents.length} agents` : "";
|
|
2299
2716
|
const left = state.project ? ` ${state.project.name} \xB7 ${state.project.code} \xB7 ${onlineUserIds().size} online${agentBit} ` : " no project open ";
|
|
2300
|
-
|
|
2717
|
+
const drawn = left.slice(0, w - 24);
|
|
2718
|
+
puts(0, sy, drawn, { bgColor: C.panel, color: C.dim });
|
|
2719
|
+
if (voiceOn) {
|
|
2720
|
+
const label = voiceTransmit ? " \u25CF live \xB7 /mute" : ` \u25CB voice ${state.voicePeers.length} \xB7 /talk`;
|
|
2721
|
+
if (drawn.length + label.length < w - 24) puts(drawn.length, sy, label, { bgColor: C.panel, color: voiceTransmit ? C.good : C.accent, bold: voiceTransmit });
|
|
2722
|
+
}
|
|
2301
2723
|
const hint = state.modal ? "[enter] ok \xB7 [esc] cancel " : "[tab] tabs \xB7 [^C] quit ";
|
|
2302
2724
|
puts(w - hint.length, sy, hint, { bgColor: C.panel, color: C.dim });
|
|
2303
2725
|
if (state.status) {
|
|
@@ -2954,7 +3376,7 @@ function copyToClipboard(text, tools) {
|
|
|
2954
3376
|
return;
|
|
2955
3377
|
}
|
|
2956
3378
|
try {
|
|
2957
|
-
const p =
|
|
3379
|
+
const p = spawn2(tool, args, { stdio: ["pipe", "ignore", "ignore"] });
|
|
2958
3380
|
p.on("error", () => copyToClipboard(text, candidatesList.slice(1)));
|
|
2959
3381
|
p.on("close", (code) => {
|
|
2960
3382
|
state.status = code === 0 ? "copied join command" : "copy failed";
|
|
@@ -2976,7 +3398,7 @@ function onMouse(name, data) {
|
|
|
2976
3398
|
return;
|
|
2977
3399
|
}
|
|
2978
3400
|
if (state.modal) return;
|
|
2979
|
-
if (
|
|
3401
|
+
if (task) {
|
|
2980
3402
|
if (name === "MOUSE_LEFT_BUTTON_PRESSED") {
|
|
2981
3403
|
for (const r of clickables) if (mx >= r.x0 && mx <= r.x1 && my >= r.y0 && my <= r.y1) {
|
|
2982
3404
|
r.action();
|
|
@@ -3092,6 +3514,10 @@ function slashCommand(text) {
|
|
|
3092
3514
|
ghostsOff = cmd.endsWith("off");
|
|
3093
3515
|
state.status = ghostsOff ? "draft broadcasting off" : "draft broadcasting on";
|
|
3094
3516
|
draftDirty = true;
|
|
3517
|
+
} else if (cmd === "/voice") {
|
|
3518
|
+
toggleVoice();
|
|
3519
|
+
} else if (cmd === "/talk" || cmd === "/mute") {
|
|
3520
|
+
toggleTalk();
|
|
3095
3521
|
} else {
|
|
3096
3522
|
state.status = `unknown command: ${cmd.split(" ")[0]}`;
|
|
3097
3523
|
}
|
|
@@ -3118,24 +3544,27 @@ function sendChat() {
|
|
|
3118
3544
|
}
|
|
3119
3545
|
function onKey(name) {
|
|
3120
3546
|
if (name === "CTRL_C") return shutdown();
|
|
3547
|
+
if ((name === "CTRL_G" || name === "CTRL_ALT_G") && voiceOn) return toggleTalk();
|
|
3121
3548
|
if (boot) return skipBoot();
|
|
3122
3549
|
const rung = rungFor(W(), H());
|
|
3123
3550
|
if (rung === "micro" || rung === "orb") return;
|
|
3124
3551
|
const paused = rung === "ticker";
|
|
3125
|
-
if (
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
}
|
|
3552
|
+
if (task && !paused) {
|
|
3553
|
+
const enter = name === "ENTER" || name === "KP_ENTER";
|
|
3554
|
+
if (task.phase === "confirm") {
|
|
3555
|
+
if (enter) task.run?.();
|
|
3556
|
+
else if (name === "ESCAPE") task = null;
|
|
3131
3557
|
return;
|
|
3132
3558
|
}
|
|
3133
|
-
if (
|
|
3134
|
-
if (name === "
|
|
3135
|
-
|
|
3559
|
+
if (task.phase === "running") {
|
|
3560
|
+
if (name === "ESCAPE") {
|
|
3561
|
+
cancelTask?.();
|
|
3562
|
+
task = null;
|
|
3563
|
+
}
|
|
3136
3564
|
return;
|
|
3137
3565
|
}
|
|
3138
|
-
if (
|
|
3566
|
+
if (enter && task.ok && task.onOk) return task.onOk();
|
|
3567
|
+
if (enter || name === "ESCAPE") task = null;
|
|
3139
3568
|
return;
|
|
3140
3569
|
}
|
|
3141
3570
|
if (state.modal && !paused) {
|
|
@@ -3358,6 +3787,11 @@ function restoreTerminal() {
|
|
|
3358
3787
|
}
|
|
3359
3788
|
function shutdown() {
|
|
3360
3789
|
restoreTerminal();
|
|
3790
|
+
try {
|
|
3791
|
+
voiceCapture?.stop();
|
|
3792
|
+
voicePlayback?.stopAll();
|
|
3793
|
+
} catch {
|
|
3794
|
+
}
|
|
3361
3795
|
for (const h of runningExec.values()) {
|
|
3362
3796
|
try {
|
|
3363
3797
|
h.abort();
|
|
@@ -3440,6 +3874,7 @@ export {
|
|
|
3440
3874
|
__setMySid,
|
|
3441
3875
|
__setNav,
|
|
3442
3876
|
__setUpdate,
|
|
3877
|
+
__taskDemo,
|
|
3443
3878
|
chatLog,
|
|
3444
3879
|
cursors,
|
|
3445
3880
|
editor,
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/install.ts
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
function which(bin) {
|
|
8
|
+
const exts = process.platform === "win32" ? [".exe", ".cmd", ".bat", ""] : [""];
|
|
9
|
+
for (const dir of (process.env.PATH ?? "").split(path.delimiter).filter(Boolean)) {
|
|
10
|
+
for (const ext of exts) {
|
|
11
|
+
const p = path.join(dir, bin + ext);
|
|
12
|
+
try {
|
|
13
|
+
fs.accessSync(p, fs.constants.X_OK);
|
|
14
|
+
if (fs.statSync(p).isFile()) return true;
|
|
15
|
+
} catch {
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
function ffmpegInstallPlan(platform = process.platform) {
|
|
22
|
+
if (platform === "darwin") {
|
|
23
|
+
if (which("brew")) return { auto: { cmd: "brew", args: ["install", "ffmpeg"], label: "brew install ffmpeg" }, manual: "brew install ffmpeg" };
|
|
24
|
+
return { manual: "install Homebrew from brew.sh, then: brew install ffmpeg", note: "Homebrew not found" };
|
|
25
|
+
}
|
|
26
|
+
if (platform === "win32") {
|
|
27
|
+
if (which("winget")) return { auto: { cmd: "winget", args: ["install", "--silent", "--accept-package-agreements", "--accept-source-agreements", "Gyan.FFmpeg"], label: "winget install ffmpeg" }, manual: "winget install Gyan.FFmpeg" };
|
|
28
|
+
if (which("choco")) return { auto: { cmd: "choco", args: ["install", "-y", "ffmpeg-full"], label: "choco install ffmpeg-full" }, manual: "choco install ffmpeg-full" };
|
|
29
|
+
return { manual: "install ffmpeg from ffmpeg.org and add it to PATH", note: "no winget/choco found" };
|
|
30
|
+
}
|
|
31
|
+
if (which("apt-get") || which("apt")) return { manual: "sudo apt install ffmpeg", note: "needs sudo \u2014 run it in your shell" };
|
|
32
|
+
if (which("dnf")) return { manual: "sudo dnf install ffmpeg", note: "needs sudo \u2014 run it in your shell" };
|
|
33
|
+
if (which("pacman")) return { manual: "sudo pacman -S ffmpeg", note: "needs sudo \u2014 run it in your shell" };
|
|
34
|
+
return { manual: "install ffmpeg with your package manager", note: "unknown package manager" };
|
|
35
|
+
}
|
|
36
|
+
function runCommand(cmd, args, cb) {
|
|
37
|
+
let child;
|
|
38
|
+
try {
|
|
39
|
+
child = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
40
|
+
} catch (e) {
|
|
41
|
+
queueMicrotask(() => cb.onDone(false, `couldn't start ${cmd}`));
|
|
42
|
+
return () => {
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
let tail = "";
|
|
46
|
+
const feed = (chunk) => {
|
|
47
|
+
tail += chunk.toString();
|
|
48
|
+
let nl;
|
|
49
|
+
while ((nl = tail.indexOf("\n")) >= 0) {
|
|
50
|
+
const line = tail.slice(0, nl).trim();
|
|
51
|
+
tail = tail.slice(nl + 1);
|
|
52
|
+
if (line) cb.onLine(line);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
child.stdout?.on("data", feed);
|
|
56
|
+
child.stderr?.on("data", feed);
|
|
57
|
+
child.on("error", (e) => cb.onDone(false, e.code === "ENOENT" ? `${cmd} not found` : e.message));
|
|
58
|
+
child.on("close", (code) => {
|
|
59
|
+
if (tail.trim()) cb.onLine(tail.trim());
|
|
60
|
+
cb.onDone(code === 0, code === 0 ? "installed" : `${cmd} exited ${code}`);
|
|
61
|
+
});
|
|
62
|
+
return () => {
|
|
63
|
+
try {
|
|
64
|
+
child.kill();
|
|
65
|
+
} catch {
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
export {
|
|
70
|
+
ffmpegInstallPlan,
|
|
71
|
+
runCommand
|
|
72
|
+
};
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
LIMITS,
|
|
7
7
|
PORT,
|
|
8
8
|
PROTO
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-WRSSNAIF.js";
|
|
10
10
|
|
|
11
11
|
// src/server.ts
|
|
12
12
|
import { WebSocketServer, WebSocket } from "ws";
|
|
@@ -109,7 +109,6 @@ function sanitizeText(v, maxCp) {
|
|
|
109
109
|
return out;
|
|
110
110
|
}
|
|
111
111
|
var RoomCore = class {
|
|
112
|
-
// userId -> trigger times
|
|
113
112
|
constructor(host) {
|
|
114
113
|
this.host = host;
|
|
115
114
|
}
|
|
@@ -135,6 +134,11 @@ var RoomCore = class {
|
|
|
135
134
|
invokeWindow = /* @__PURE__ */ new Map();
|
|
136
135
|
// projectId -> invocation times
|
|
137
136
|
triggerWindow = /* @__PURE__ */ new Map();
|
|
137
|
+
// userId -> trigger times
|
|
138
|
+
// voice channel (realtime, loss-tolerant): sids currently in voice, and a
|
|
139
|
+
// per-sid rate window (voiceFrame is exempt from the general frame flood cap)
|
|
140
|
+
voiceSids = /* @__PURE__ */ new Set();
|
|
141
|
+
voiceWindow = /* @__PURE__ */ new Map();
|
|
138
142
|
// ---------- lifecycle ----------
|
|
139
143
|
async onMessage(conn, raw) {
|
|
140
144
|
let msg;
|
|
@@ -145,14 +149,16 @@ var RoomCore = class {
|
|
|
145
149
|
}
|
|
146
150
|
if (!msg || typeof msg !== "object" || typeof msg.t !== "string") return;
|
|
147
151
|
const now = this.host.now();
|
|
148
|
-
|
|
149
|
-
|
|
152
|
+
if (msg.t !== "voiceFrame") {
|
|
153
|
+
const win = (this.frameWindow.get(conn) ?? []).filter((t) => now - t < 1e4);
|
|
154
|
+
if (win.length >= LIMITS.framesPerWindow) {
|
|
155
|
+
this.frameWindow.set(conn, win);
|
|
156
|
+
this.host.close(conn, 1008, "frame flood");
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
win.push(now);
|
|
150
160
|
this.frameWindow.set(conn, win);
|
|
151
|
-
this.host.close(conn, 1008, "frame flood");
|
|
152
|
-
return;
|
|
153
161
|
}
|
|
154
|
-
win.push(now);
|
|
155
|
-
this.frameWindow.set(conn, win);
|
|
156
162
|
try {
|
|
157
163
|
await this.dispatch(conn, msg);
|
|
158
164
|
} catch (e) {
|
|
@@ -322,6 +328,34 @@ var RoomCore = class {
|
|
|
322
328
|
for (const c of this.roomConns(s.projectId)) this.host.send(c, { t: "cleared" });
|
|
323
329
|
break;
|
|
324
330
|
}
|
|
331
|
+
case "voiceJoin": {
|
|
332
|
+
if (!s.projectId || this.voiceSids.has(s.sid)) break;
|
|
333
|
+
this.voiceSids.add(s.sid);
|
|
334
|
+
this.broadcastVoice(s.projectId);
|
|
335
|
+
await this.systemLine(s.projectId, `${s.name} joined voice`);
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
case "voiceLeave": {
|
|
339
|
+
if (!s.projectId || !this.voiceSids.has(s.sid)) break;
|
|
340
|
+
this.voiceSids.delete(s.sid);
|
|
341
|
+
this.voiceWindow.delete(s.sid);
|
|
342
|
+
this.broadcastVoice(s.projectId);
|
|
343
|
+
await this.systemLine(s.projectId, `${s.name} left voice`);
|
|
344
|
+
break;
|
|
345
|
+
}
|
|
346
|
+
case "voiceFrame": {
|
|
347
|
+
if (!s.projectId || !this.voiceSids.has(s.sid)) break;
|
|
348
|
+
const data = typeof msg.data === "string" ? msg.data : "";
|
|
349
|
+
if (!data || data.length > LIMITS.voiceFrameMaxB64) break;
|
|
350
|
+
if (!this.rateOk(this.voiceWindow, s.sid, LIMITS.voiceFramesPerSec, 1e3)) break;
|
|
351
|
+
for (const c of this.roomConns(s.projectId)) {
|
|
352
|
+
const peer = this.host.getSession(c);
|
|
353
|
+
if (peer && peer.sid !== s.sid && this.voiceSids.has(peer.sid)) {
|
|
354
|
+
this.host.send(c, { t: "voiceFrame", sid: s.sid, data });
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
325
359
|
case "chatSend": {
|
|
326
360
|
if (!s.projectId) break;
|
|
327
361
|
const cmid = String(msg.cmid || "").slice(0, 40);
|
|
@@ -519,12 +553,18 @@ var RoomCore = class {
|
|
|
519
553
|
if (inv && inv.projectId === pid) void this.finalizeInvocation(inv, "partial");
|
|
520
554
|
this.queues.delete(agentId);
|
|
521
555
|
}
|
|
556
|
+
const wasInVoice = this.voiceSids.delete(s.sid);
|
|
557
|
+
this.voiceWindow.delete(s.sid);
|
|
522
558
|
s.projectId = null;
|
|
523
559
|
this.host.setSession(conn, s);
|
|
524
560
|
for (const c of this.roomPeers(pid, conn)) {
|
|
525
561
|
this.host.send(c, { t: "cursorGone", sid: s.sid });
|
|
526
562
|
}
|
|
527
563
|
this.broadcastSessions(pid);
|
|
564
|
+
if (wasInVoice) {
|
|
565
|
+
this.broadcastVoice(pid);
|
|
566
|
+
void this.systemLine(pid, `${s.name} left voice`);
|
|
567
|
+
}
|
|
528
568
|
if (agentsChanged) {
|
|
529
569
|
const p = this.projectsSync?.get(pid);
|
|
530
570
|
if (p) this.broadcastAgents(pid);
|
|
@@ -757,6 +797,11 @@ var RoomCore = class {
|
|
|
757
797
|
const sessions = this.sessionsFor(projectId);
|
|
758
798
|
for (const c of this.roomConns(projectId)) this.host.send(c, { t: "sessions", sessions });
|
|
759
799
|
}
|
|
800
|
+
/** who is in voice for this project (join/leave presence, not per-frame). */
|
|
801
|
+
broadcastVoice(projectId) {
|
|
802
|
+
const peers = this.roomConns(projectId).map((c) => this.host.getSession(c)).filter((s) => !!s && this.voiceSids.has(s.sid)).map((s) => ({ sid: s.sid, userId: s.userId, name: s.name, color: s.color }));
|
|
803
|
+
for (const c of this.roomConns(projectId)) this.host.send(c, { t: "voice", peers });
|
|
804
|
+
}
|
|
760
805
|
onlineIds(projectId) {
|
|
761
806
|
return [...new Set(this.sessionsFor(projectId).map((s) => s.userId))];
|
|
762
807
|
}
|
package/dist/cli.js
CHANGED
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
DEFAULT_SERVER,
|
|
4
4
|
PORT,
|
|
5
5
|
VERSION
|
|
6
|
-
} from "./chunks/chunk-
|
|
6
|
+
} from "./chunks/chunk-WRSSNAIF.js";
|
|
7
7
|
|
|
8
8
|
// src/cli.ts
|
|
9
9
|
var HELP = `
|
|
@@ -52,7 +52,7 @@ async function main() {
|
|
|
52
52
|
console.error(`invalid port: ${argVal(args, "--port")}`);
|
|
53
53
|
process.exit(1);
|
|
54
54
|
}
|
|
55
|
-
const { runServer } = await import("./chunks/server-
|
|
55
|
+
const { runServer } = await import("./chunks/server-OQNHWL3Z.js");
|
|
56
56
|
runServer(port);
|
|
57
57
|
return;
|
|
58
58
|
}
|
|
@@ -76,7 +76,7 @@ async function main() {
|
|
|
76
76
|
const { cachedUpdate, refreshUpdateCache } = await import("./chunks/update-UEPNGB6Y.js");
|
|
77
77
|
refreshUpdateCache();
|
|
78
78
|
const update = cachedUpdate(VERSION);
|
|
79
|
-
const { runClient } = await import("./chunks/client-
|
|
79
|
+
const { runClient } = await import("./chunks/client-W4563KDL.js");
|
|
80
80
|
runClient({
|
|
81
81
|
name: argVal(args, "--name"),
|
|
82
82
|
server: argVal(args, "--server"),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "accelr8",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.2",
|
|
4
4
|
"description": "Collaborative ascii canvas in your terminal. Real multiplayer, live cursors, mouse-driven TUI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "ISC",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"chattest": "tsx src/chattest.ts",
|
|
43
43
|
"inputtest": "tsx src/inputtest.ts",
|
|
44
44
|
"torturetest": "tsx src/torturetest.ts",
|
|
45
|
-
"test": "for t in core chat input ladder boot orb crypto accounts executor agentcontext map voice glyph materialize update contrast; do tsx src/${t}test.ts || exit 1; done && tsx src/rendertest.ts > /dev/null && echo RENDER OK",
|
|
45
|
+
"test": "for t in core chat input ladder boot orb crypto accounts executor agentcontext map voice glyph materialize update install contrast; do tsx src/${t}test.ts || exit 1; done && tsx src/rendertest.ts > /dev/null && echo RENDER OK",
|
|
46
46
|
"laddertest": "tsx src/laddertest.ts",
|
|
47
47
|
"boottest": "tsx src/boottest.ts",
|
|
48
48
|
"reconnecttest": "tsx src/reconnecttest.ts",
|
|
@@ -53,7 +53,8 @@
|
|
|
53
53
|
"accountstest": "tsx src/accountstest.ts",
|
|
54
54
|
"executortest": "tsx src/executortest.ts",
|
|
55
55
|
"agentcontexttest": "tsx src/agentcontexttest.ts",
|
|
56
|
-
"chaintest": "tsx src/chaintest.ts"
|
|
56
|
+
"chaintest": "tsx src/chaintest.ts",
|
|
57
|
+
"voicetransporttest": "tsx src/voicetransporttest.ts"
|
|
57
58
|
},
|
|
58
59
|
"dependencies": {
|
|
59
60
|
"terminal-kit": "3.1.2",
|