accelr8 3.0.2 → 3.0.4

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,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/protocol.ts
4
- var VERSION = true ? "3.0.2" : "3.0.2";
4
+ var VERSION = true ? "3.0.4" : "3.0.2";
5
5
  var PROTO = 3;
6
6
  var PORT = 8787;
7
7
  var DEFAULT_SERVER = "wss://accelr8-canvas.patonchain.workers.dev";
@@ -7,7 +7,7 @@ import {
7
7
  VERSION,
8
8
  decode,
9
9
  encode
10
- } from "./chunk-WRSSNAIF.js";
10
+ } from "./chunk-I5NIANY6.js";
11
11
  import {
12
12
  DEFAULT_MODEL,
13
13
  addAccount,
@@ -1181,6 +1181,7 @@ var SAMPLE_RATE = 16e3;
1181
1181
  var CHUNK_BYTES = 1280;
1182
1182
  var KILL_GRACE_MS = 300;
1183
1183
  var SPEAKER_IDLE_MS = 2e3;
1184
+ var PREBUFFER_BYTES = 4800;
1184
1185
  var DRAIN_KILL_MS = 2e3;
1185
1186
  var MAX_PLAYERS = 8;
1186
1187
  var STDERR_TAIL = 400;
@@ -1190,10 +1191,9 @@ var PLAYBACK_ARGS = [
1190
1191
  "-loglevel",
1191
1192
  "error",
1192
1193
  "-nodisp",
1193
- "-fflags",
1194
- "nobuffer",
1195
- "-flags",
1196
- "low_delay",
1194
+ "-sync",
1195
+ "audio",
1196
+ "-infbuf",
1197
1197
  "-f",
1198
1198
  "s16le",
1199
1199
  "-ar",
@@ -1355,13 +1355,31 @@ var VoicePlayback = class {
1355
1355
  if (!spawned) return;
1356
1356
  p = spawned;
1357
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;
1358
+ if (!p.flowing) {
1359
+ p.pre.push(bytes);
1360
+ if (p.pre.reduce((n, b) => n + b.length, 0) >= PREBUFFER_BYTES) {
1361
+ const cushion = Buffer.concat(p.pre);
1362
+ p.pre = [];
1363
+ p.flowing = true;
1364
+ const np2 = this.writeOrRespawn(speakerId, p, cushion);
1365
+ if (np2) this.touch(speakerId, np2);
1366
+ } else {
1367
+ this.touch(speakerId, p);
1368
+ }
1369
+ return;
1363
1370
  }
1364
- this.touch(speakerId, p);
1371
+ const np = this.writeOrRespawn(speakerId, p, bytes);
1372
+ if (np) this.touch(speakerId, np);
1373
+ }
1374
+ /** Write to a player; on EPIPE (it died) respawn once and write there. Returns
1375
+ * the live player, or null if it couldn't be (re)spawned. */
1376
+ writeOrRespawn(speakerId, p, buf) {
1377
+ if (this.write(p, buf)) return p;
1378
+ this.reap(speakerId, p);
1379
+ const fresh = this.players.size < MAX_PLAYERS ? this.spawnPlayer(speakerId) : null;
1380
+ if (!fresh || !this.write(fresh, buf)) return null;
1381
+ fresh.flowing = true;
1382
+ return fresh;
1365
1383
  }
1366
1384
  /** Graceful close for one speaker (voiceStop): end stdin so ffplay drains
1367
1385
  * its buffered audio, then a SIGKILL backstop after 2s (we dropped -autoexit,
@@ -1408,7 +1426,7 @@ var VoicePlayback = class {
1408
1426
  } catch {
1409
1427
  return null;
1410
1428
  }
1411
- const p = { child, alive: true, idleTimer: null };
1429
+ const p = { child, alive: true, idleTimer: null, pre: [], flowing: false };
1412
1430
  child.stdin?.on("error", () => {
1413
1431
  p.alive = false;
1414
1432
  });
@@ -1458,6 +1476,55 @@ var VoicePlayback = class {
1458
1476
  if (this.players.get(speakerId) === p) this.players.delete(speakerId);
1459
1477
  }
1460
1478
  };
1479
+ function listMicDevices() {
1480
+ try {
1481
+ if (process.platform === "darwin") {
1482
+ const text = runForOutput(["-hide_banner", "-f", "avfoundation", "-list_devices", "true", "-i", ""]);
1483
+ const out = [];
1484
+ let inAudio = false;
1485
+ for (const line of text.split("\n")) {
1486
+ if (/audio devices/i.test(line)) {
1487
+ inAudio = true;
1488
+ continue;
1489
+ }
1490
+ if (/video devices/i.test(line)) {
1491
+ inAudio = false;
1492
+ continue;
1493
+ }
1494
+ if (!inAudio) continue;
1495
+ const m = line.match(/\[(\d+)\]\s+(.+?)\s*$/);
1496
+ if (m) out.push({ index: m[1], name: m[2] });
1497
+ }
1498
+ return out;
1499
+ }
1500
+ if (process.platform === "linux") {
1501
+ const text = runForOutput(["-hide_banner", "-sources", "pulse"]);
1502
+ const out = [];
1503
+ for (const line of text.split("\n")) {
1504
+ if (/auto-detected/i.test(line)) continue;
1505
+ const m = line.match(/^\s*\*?\s*(\S+)\s+\[(.+)\]\s*$/);
1506
+ if (m) out.push({ index: m[1], name: m[2] });
1507
+ }
1508
+ return out;
1509
+ }
1510
+ if (process.platform === "win32") {
1511
+ const text = runForOutput(["-hide_banner", "-list_devices", "true", "-f", "dshow", "-i", "dummy"]);
1512
+ const out = [];
1513
+ for (const line of text.split("\n")) {
1514
+ const m = line.match(/"(.+)"\s+\(audio\)/);
1515
+ if (m) out.push({ index: m[1], name: m[1] });
1516
+ }
1517
+ return out;
1518
+ }
1519
+ return [];
1520
+ } catch {
1521
+ return [];
1522
+ }
1523
+ }
1524
+ function runForOutput(args) {
1525
+ const res = spawnSync("ffmpeg", args, { encoding: "utf8", timeout: 5e3 });
1526
+ return (res.stderr ?? "") + (res.stdout ?? "");
1527
+ }
1461
1528
  function errMsg(e) {
1462
1529
  return e instanceof Error ? e.message : String(e);
1463
1530
  }
@@ -1587,6 +1654,41 @@ var voiceOn = false;
1587
1654
  var voiceTransmit = false;
1588
1655
  var voiceCapture = null;
1589
1656
  var voicePlayback = null;
1657
+ var voiceDevice = process.env.ACCELR8_MIC || void 0;
1658
+ var voiceRetries = 0;
1659
+ function autoPickMic() {
1660
+ if (process.platform !== "darwin") return void 0;
1661
+ const mics = listMicDevices();
1662
+ if (!mics.length) return void 0;
1663
+ const builtin = mics.find((m) => /macbook|built.?in|internal/i.test(m.name));
1664
+ if (builtin) return builtin.index;
1665
+ const notPhone = mics.find((m) => !/iphone|ipad|continuity|desk view/i.test(m.name));
1666
+ return notPhone?.index;
1667
+ }
1668
+ function startCapture() {
1669
+ if (!voiceCapture) return;
1670
+ voiceCapture.start({
1671
+ device: voiceDevice,
1672
+ onChunk: (b64) => {
1673
+ voiceRetries = 0;
1674
+ if (voiceTransmit) sendMsg({ t: "voiceFrame", data: b64 });
1675
+ },
1676
+ onError: (m) => {
1677
+ if (!voiceTransmit) return;
1678
+ voiceRetries++;
1679
+ if (voiceRetries <= 6) {
1680
+ state.status = `mic dropped \xB7 reconnecting ${voiceRetries}\u2026`;
1681
+ setTimeout(() => {
1682
+ if (voiceTransmit && voiceCapture && !voiceCapture.active) startCapture();
1683
+ }, 200);
1684
+ } else {
1685
+ voiceTransmit = false;
1686
+ voiceRetries = 0;
1687
+ state.status = "mic keeps dropping \xB7 /voice mics to pick another \xB7 " + m.slice(0, 30);
1688
+ }
1689
+ }
1690
+ });
1691
+ }
1590
1692
  function toggleVoice() {
1591
1693
  if (voiceOn) return leaveVoice();
1592
1694
  if (!state.project) {
@@ -1602,6 +1704,7 @@ function toggleVoice() {
1602
1704
  voiceCapture = new VoiceCapture(voiceTools);
1603
1705
  voiceOn = true;
1604
1706
  voiceTransmit = false;
1707
+ if (voiceDevice === void 0 && voiceTools.capture) voiceDevice = autoPickMic();
1605
1708
  sendMsg({ t: "voiceJoin" });
1606
1709
  state.status = voiceTools.capture ? "voice on \xB7 /talk (or ctrl-g) to speak" : "voice on \xB7 listen only (no mic found)";
1607
1710
  }
@@ -1624,21 +1727,15 @@ function toggleTalk() {
1624
1727
  return;
1625
1728
  }
1626
1729
  if (voiceTransmit) {
1627
- voiceCapture.stop();
1628
1730
  voiceTransmit = false;
1731
+ voiceRetries = 0;
1732
+ voiceCapture.stop();
1629
1733
  state.status = "muted \xB7 /talk (or ctrl-g) to speak";
1630
1734
  } else {
1631
1735
  voiceTransmit = true;
1736
+ voiceRetries = 0;
1632
1737
  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
- });
1738
+ startCapture();
1642
1739
  }
1643
1740
  }
1644
1741
  function openHelp() {
@@ -3019,7 +3116,7 @@ function renderAccounts() {
3019
3116
  puts(ix + 46, y, a.auth.kind === "claude_code_login" ? "claude login" : "api key", { color: C.dim });
3020
3117
  puts(ix + 60, y, status, { color: dotColor });
3021
3118
  const modeColor = a.mode === "auto" ? C.warn : a.mode === "plan" ? C.accent : C.good;
3022
- button(w - 22, y, ` ${a.mode} `, { bgColor: C.tabOff, color: modeColor, bold: true }, () => cycleMode(a));
3119
+ button(w - 22, y, ` ${a.mode} `, { bgColor: C.tabOff, color: readableFg(modeColor, C.tabOff), bold: true }, () => cycleMode(a));
3023
3120
  button(w - 11, y, " remove ", { bgColor: C.tabOff, color: C.text }, () => removeAgentAccount(a));
3024
3121
  y += 1;
3025
3122
  }
@@ -3518,6 +3615,19 @@ function slashCommand(text) {
3518
3615
  toggleVoice();
3519
3616
  } else if (cmd === "/talk" || cmd === "/mute") {
3520
3617
  toggleTalk();
3618
+ } else if (cmd === "/voice mics" || cmd === "/mics") {
3619
+ const mics = listMicDevices();
3620
+ sidecarBlock = { title: "microphones", at: Date.now(), lines: mics.length ? mics.map((m) => `${m.index} ${m.name}${voiceDevice === m.index ? " \xB7 selected" : ""}`).concat("use: /voice mic <index>") : ["none found (need ffmpeg)"] };
3621
+ state.status = `${mics.length} mics \xB7 /voice mic <index> to pick`;
3622
+ } else if (cmd.startsWith("/voice mic ")) {
3623
+ const idx = cmd.slice("/voice mic ".length).trim();
3624
+ voiceDevice = idx || void 0;
3625
+ state.status = `mic set to [${idx}]` + (voiceOn ? " \xB7 reconnecting" : "");
3626
+ if (voiceTransmit && voiceCapture) {
3627
+ voiceCapture.stop();
3628
+ voiceRetries = 0;
3629
+ startCapture();
3630
+ }
3521
3631
  } else {
3522
3632
  state.status = `unknown command: ${cmd.split(" ")[0]}`;
3523
3633
  }
@@ -6,7 +6,7 @@ import {
6
6
  LIMITS,
7
7
  PORT,
8
8
  PROTO
9
- } from "./chunk-WRSSNAIF.js";
9
+ } from "./chunk-I5NIANY6.js";
10
10
 
11
11
  // src/server.ts
12
12
  import { WebSocketServer, WebSocket } from "ws";
package/dist/cli.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  DEFAULT_SERVER,
4
4
  PORT,
5
5
  VERSION
6
- } from "./chunks/chunk-WRSSNAIF.js";
6
+ } from "./chunks/chunk-I5NIANY6.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-OQNHWL3Z.js");
55
+ const { runServer } = await import("./chunks/server-QZIOVRDR.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-W4563KDL.js");
79
+ const { runClient } = await import("./chunks/client-BVLUBSS7.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.2",
3
+ "version": "3.0.4",
4
4
  "description": "Collaborative ascii canvas in your terminal. Real multiplayer, live cursors, mouse-driven TUI.",
5
5
  "type": "module",
6
6
  "license": "ISC",