opencode-interrupt-plugin 0.4.4 → 0.4.5

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,5 +1,6 @@
1
1
  import { spawn } from 'child_process';
2
2
  import { EventEmitter } from 'events';
3
+ import { debug } from '../log.js';
3
4
  export class AudioMonitor extends EventEmitter {
4
5
  pollTimer = null;
5
6
  isRunning = false;
@@ -13,7 +14,7 @@ export class AudioMonitor extends EventEmitter {
13
14
  start() {
14
15
  if (this.isRunning)
15
16
  return;
16
- console.log('[interrupt-monitor] Starting audio monitor');
17
+ debug('[interrupt-monitor] Starting audio monitor');
17
18
  this.isRunning = true;
18
19
  this.emit('started');
19
20
  this.poll();
@@ -33,22 +34,22 @@ export class AudioMonitor extends EventEmitter {
33
34
  if (!this.isRunning)
34
35
  return;
35
36
  if (code !== 0) {
36
- console.log(`[interrupt-monitor] sox exited with code ${code}`);
37
+ debug(`[interrupt-monitor] sox exited with code ${code}`);
37
38
  }
38
39
  const rms = parseRMSFromStat(out);
39
40
  if (rms !== null) {
40
- console.log(`[interrupt-monitor] RMS: ${rms.toFixed(6)} (threshold: ${this.threshold})`);
41
+ debug(`[interrupt-monitor] RMS: ${rms.toFixed(6)} (threshold: ${this.threshold})`);
41
42
  this.handleRMS(rms);
42
43
  }
43
44
  else {
44
- console.log(`[interrupt-monitor] Failed to parse RMS from stderr (${out.length} chars)`);
45
+ debug(`[interrupt-monitor] Failed to parse RMS from stderr (${out.length} chars)`);
45
46
  }
46
47
  this.pollTimer = setTimeout(() => this.poll(), this.pollMs);
47
48
  });
48
49
  proc.on('error', (err) => {
49
50
  if (!this.isRunning)
50
51
  return;
51
- console.log(`[interrupt-monitor] sox spawn error: ${err.message}`);
52
+ debug(`[interrupt-monitor] sox spawn error: ${err.message}`);
52
53
  this.emit('error', new Error('sox not found. Install it: brew install sox (macOS) or apt install sox (Linux)'));
53
54
  this.isRunning = false;
54
55
  });
@@ -1,5 +1,6 @@
1
1
  import { AudioMonitor } from './monitor.js';
2
2
  import { EventEmitter } from 'events';
3
+ import { debug } from '../log.js';
3
4
  export class VoiceOverlapDetector extends EventEmitter {
4
5
  monitor;
5
6
  active = false;
@@ -37,17 +38,17 @@ export class VoiceOverlapDetector extends EventEmitter {
37
38
  handleVoiceDetected(rms) {
38
39
  const ttsPlaying = this.getIsTTSPlaying();
39
40
  if (!ttsPlaying) {
40
- console.log(`[interrupt-overlap] Voice detected (RMS=${rms.toFixed(6)}) but TTS not playing, ignoring`);
41
+ debug(`[interrupt-overlap] Voice detected (RMS=${rms.toFixed(6)}) but TTS not playing, ignoring`);
41
42
  return;
42
43
  }
43
44
  if (Date.now() < this.cooldownUntil) {
44
- console.log(`[interrupt-overlap] In cooldown (${this.cooldownUntil - Date.now()}ms remaining), ignoring`);
45
+ debug(`[interrupt-overlap] In cooldown (${this.cooldownUntil - Date.now()}ms remaining), ignoring`);
45
46
  return;
46
47
  }
47
48
  const now = Date.now();
48
49
  this.cooldownUntil = now + this.COOLDOWN_MS;
49
50
  const partialContent = this.getPartialContent();
50
- console.log(`[interrupt-overlap] OVERLAP! RMS=${rms.toFixed(6)}, stopping TTS, partial=${partialContent.slice(0, 80)}`);
51
+ debug(`[interrupt-overlap] OVERLAP! RMS=${rms.toFixed(6)}, stopping TTS, partial=${partialContent.slice(0, 80)}`);
51
52
  this.stopTTS();
52
53
  const event = {
53
54
  partialTTSContent: partialContent,
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { resolveConfig } from './config.js';
2
2
  import { checkLicense } from './license/guard.js';
3
+ import { debug } from './log.js';
3
4
  import { getSessionState, updateSessionState, clearSessionState, } from './store.js';
4
5
  import { prepareInjection } from './injector.js';
5
6
  import { onTTSStart, onTTSEnd, isTTSTool } from './audio/tts-tracker.js';
@@ -50,10 +51,10 @@ export const InterruptPlugin = (userConfig = {}) => {
50
51
  }, () => ttsStreamer.getPartialContent());
51
52
  overlapDetector.on('voice-overlap', (event) => {
52
53
  if (!activeSessionId) {
53
- console.log(`[interrupt] Voice overlap event but no active session, ignoring`);
54
+ debug(`[interrupt] Voice overlap event but no active session, ignoring`);
54
55
  return;
55
56
  }
56
- console.log(`[interrupt] Voice overlap detected (RMS: ${event.rmsLevel.toFixed(4)})`);
57
+ debug(`[interrupt] Voice overlap detected (RMS: ${event.rmsLevel.toFixed(4)})`);
57
58
  updateSessionState(activeSessionId, {
58
59
  wasInterrupted: true,
59
60
  partialContentAtInterrupt: event.partialTTSContent,
package/dist/log.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ export declare function enableDebugLogging(): void;
2
+ export declare function disableDebugLogging(): void;
3
+ export declare function debug(...args: unknown[]): void;
package/dist/log.js ADDED
@@ -0,0 +1,20 @@
1
+ import { appendFileSync } from 'fs';
2
+ const LOG_FILE = '/tmp/interrupt-debug.log';
3
+ let enabled = true;
4
+ export function enableDebugLogging() {
5
+ enabled = true;
6
+ }
7
+ export function disableDebugLogging() {
8
+ enabled = false;
9
+ }
10
+ export function debug(...args) {
11
+ if (!enabled)
12
+ return;
13
+ try {
14
+ const msg = args.map(a => String(a)).join(' ');
15
+ appendFileSync(LOG_FILE, `[${new Date().toISOString().slice(11, 23)}] ${msg}\n`);
16
+ }
17
+ catch {
18
+ // ignore
19
+ }
20
+ }
@@ -1,6 +1,7 @@
1
1
  import { spawn } from "child_process";
2
2
  import { EventEmitter } from "events";
3
3
  import { join } from "path";
4
+ import { debug } from "../log.js";
4
5
  const VENV_PYTHON = join(process.env.HOME || "/home/pystar", ".config", "opencode", "tts-venv", "bin", "python");
5
6
  const DEFAULT_VOICE = "en-US-AvaNeural";
6
7
  const DEFAULT_RATE = "+25%";
@@ -40,14 +41,14 @@ export class TTSEngine extends EventEmitter {
40
41
  let playDone = false;
41
42
  const checkDone = () => {
42
43
  if (genDone && playDone) {
43
- console.log(`[interrupt-tts] stream done text="${text.slice(0, 60)}" total=${Date.now() - t0}ms`);
44
+ debug(`[interrupt-tts] stream done text="${text.slice(0, 60)}" total=${Date.now() - t0}ms`);
44
45
  resolve();
45
46
  }
46
47
  };
47
48
  this.generator.on("close", (code) => {
48
49
  genDone = true;
49
50
  if (code !== 0) {
50
- console.log(`[interrupt-tts] edge-tts exited ${code}`);
51
+ debug(`[interrupt-tts] edge-tts exited ${code}`);
51
52
  }
52
53
  checkDone();
53
54
  });
@@ -78,7 +79,7 @@ export class TTSEngine extends EventEmitter {
78
79
  });
79
80
  }
80
81
  stop() {
81
- console.log(`[interrupt-engine] stop() called, playing=${this.playing}`);
82
+ debug(`[interrupt-engine] stop() called, playing=${this.playing}`);
82
83
  if (this.generator) {
83
84
  this.generator.kill("SIGTERM");
84
85
  this.generator = null;
package/dist/tts/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { EventEmitter } from "events";
2
2
  import { SentenceBuffer } from "./buffer.js";
3
3
  import { TTSEngine } from "./engine.js";
4
+ import { debug } from "../log.js";
4
5
  export class TTSStreamer extends EventEmitter {
5
6
  buffer = new SentenceBuffer();
6
7
  engine;
@@ -36,7 +37,7 @@ export class TTSStreamer extends EventEmitter {
36
37
  return;
37
38
  this.lastActivity = Date.now();
38
39
  const sentences = this.buffer.feed(chunk);
39
- console.log(`[interrupt-tts] chunk len=${chunk.text.length} sentences=${sentences.length} queue=${this.queue.length} processing=${this.processing} buflen=${this.buffer.length}`);
40
+ debug(`[interrupt-tts] chunk len=${chunk.text.length} sentences=${sentences.length} queue=${this.queue.length} processing=${this.processing} buflen=${this.buffer.length}`);
40
41
  if (sentences.length > 0) {
41
42
  this.cancelForceFlush();
42
43
  for (const s of sentences) {
@@ -57,7 +58,7 @@ export class TTSStreamer extends EventEmitter {
57
58
  return;
58
59
  const text = this.buffer.flush();
59
60
  if (text) {
60
- console.log(`[interrupt-tts] force-flush "${text.slice(0, 60)}"`);
61
+ debug(`[interrupt-tts] force-flush "${text.slice(0, 60)}"`);
61
62
  this.queue.push(text);
62
63
  if (!this.processing) {
63
64
  this.processQueue();
@@ -76,7 +77,7 @@ export class TTSStreamer extends EventEmitter {
76
77
  return;
77
78
  const remaining = this.buffer.flush();
78
79
  if (remaining) {
79
- console.log(`[interrupt-tts] flush remaining="${remaining.slice(0, 80)}"`);
80
+ debug(`[interrupt-tts] flush remaining="${remaining.slice(0, 80)}"`);
80
81
  this.queue.push(remaining);
81
82
  }
82
83
  if (!this.processing)
@@ -104,7 +105,7 @@ export class TTSStreamer extends EventEmitter {
104
105
  }
105
106
  }
106
107
  stop() {
107
- console.log(`[interrupt-tts] stop() called, playing=${this.engine.isPlaying}, queue=${this.queue.length}, processing=${this.processing}`);
108
+ debug(`[interrupt-tts] stop() called, playing=${this.engine.isPlaying}, queue=${this.queue.length}, processing=${this.processing}`);
108
109
  this.stopped = true;
109
110
  this.queue = [];
110
111
  this.engine.stop();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-interrupt-plugin",
3
- "version": "0.4.4",
3
+ "version": "0.4.5",
4
4
  "description": "Streaming TTS + voice interruption for OpenCode. Speaks responses as they arrive and detects when you talk over it.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",