@hxnnxs/opencode-voice 0.1.6 → 0.1.7

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/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  All notable changes to this project are documented here.
4
4
 
5
+ ## 0.1.7 - 2026-06-17
6
+
7
+ ### Fixed
8
+
9
+ - Hardened Windows ffmpeg resolution to handle executable-resolution edge cases (extensionless bundled binary paths and local module fallback), so recorder startup can use the actual resolved binary path.
10
+ - Improved diagnostics to print exact recorder command paths and quick per-command probe results in `opencode-voice doctor` output.
11
+
5
12
  ## 0.1.6 - 2026-06-17
6
13
 
7
14
  ### Fixed
@@ -32,6 +32,34 @@ function packageName() {
32
32
  return process.env.OPENCODE_VOICE_PACKAGE || manifest.name || "opencode-voice";
33
33
  }
34
34
 
35
+ function probeCommand(command, args = ["-version"]) {
36
+ if (!command) return { ok: false, message: "missing" };
37
+ try {
38
+ const result = spawnSync(command, args, {
39
+ encoding: "utf8",
40
+ stdio: ["ignore", "pipe", "pipe"],
41
+ timeout: 2000,
42
+ });
43
+
44
+ if (result.error) {
45
+ return { ok: false, message: result.error.message };
46
+ }
47
+
48
+ if (typeof result.status === "number" && result.status !== 0) {
49
+ return {
50
+ ok: false,
51
+ message: `exit ${result.status}`,
52
+ stderr: (result.stderr || "").trim(),
53
+ };
54
+ }
55
+
56
+ const output = `${result.stdout || ""}${result.stderr || ""}`;
57
+ return { ok: true, versionLine: output.split("\n").filter(Boolean)[0] || "" };
58
+ } catch (error) {
59
+ return { ok: false, message: error instanceof Error ? error.message : String(error) };
60
+ }
61
+ }
62
+
35
63
  async function runtime() {
36
64
  const [engine, models, engines] = await Promise.all([import("../lib/engine.js"), import("../lib/models.js"), import("../lib/engines.js")]);
37
65
  return { ...engine, ...models, ...engines };
@@ -40,6 +68,7 @@ async function runtime() {
40
68
  async function doctor() {
41
69
  const {
42
70
  commandExists,
71
+ resolveCommand,
43
72
  getAudioDir,
44
73
  getCacheDir,
45
74
  getEngineStatus,
@@ -50,6 +79,9 @@ async function doctor() {
50
79
 
51
80
  const engine = getEngineStatus("whisper.cpp");
52
81
  const probe = engine.resolvedBinary ? await probeEngine("whisper.cpp", engine.resolvedBinary) : { ok: false, message: "missing binary" };
82
+ const ffmpeg = resolveCommand("ffmpeg");
83
+ const arecord = resolveCommand("arecord");
84
+ const sox = resolveCommand("sox");
53
85
  const payload = {
54
86
  platform: `${process.platform}-${process.arch}`,
55
87
  cacheDir: getCacheDir(),
@@ -58,9 +90,15 @@ async function doctor() {
58
90
  engine,
59
91
  probe,
60
92
  recorders: {
61
- ffmpeg: commandExists("ffmpeg"),
62
- arecord: commandExists("arecord"),
63
- sox: commandExists("sox"),
93
+ ffmpeg,
94
+ arecord,
95
+ sox,
96
+ ffmpegPresent: commandExists("ffmpeg"),
97
+ arecordPresent: commandExists("arecord"),
98
+ soxPresent: commandExists("sox"),
99
+ ffmpegProbe: probeCommand(ffmpeg),
100
+ arecordProbe: probeCommand(arecord, ["--help"]),
101
+ soxProbe: probeCommand(sox, ["--help"]),
64
102
  },
65
103
  microphones: listMicrophones(),
66
104
  };
@@ -80,7 +118,7 @@ async function doctor() {
80
118
  `Managed engine dir: ${engine.managedDir}`,
81
119
  `whisper-cli: ${engine.resolvedBinary || "missing"}`,
82
120
  `Probe: ${probe.ok ? "ok" : probe.message}`,
83
- `Recorders: ffmpeg=${payload.recorders.ffmpeg ? "yes" : "no"}, arecord=${payload.recorders.arecord ? "yes" : "no"}, sox=${payload.recorders.sox ? "yes" : "no"}`,
121
+ `Recorders: ffmpeg=${payload.recorders.ffmpegPresent ? "yes" : "no"}${payload.recorders.ffmpeg ? ` (${payload.recorders.ffmpeg})` : ""}, arecord=${payload.recorders.arecordPresent ? "yes" : "no"}${payload.recorders.arecord ? ` (${payload.recorders.arecord})` : ""}, sox=${payload.recorders.soxPresent ? "yes" : "no"}${payload.recorders.sox ? ` (${payload.recorders.sox})` : ""}`,
84
122
  `Microphones: ${payload.microphones.join(", ")}`,
85
123
  ].join("\n"),
86
124
  );
package/lib/engine.js CHANGED
@@ -24,6 +24,31 @@ function isExecutable(file) {
24
24
  }
25
25
  }
26
26
 
27
+ function executableExtensions() {
28
+ if (process.platform !== "win32") return [""];
29
+ return (process.env.PATHEXT || ".EXE;.CMD;.BAT")
30
+ .split(";")
31
+ .map((entry) => entry.trim().toLowerCase())
32
+ .filter(Boolean)
33
+ .filter((entry) => entry.startsWith("."));
34
+ }
35
+
36
+ function resolveExecutable(file) {
37
+ const normalized = path.resolve(file);
38
+ if (isExecutable(normalized)) return normalized;
39
+
40
+ if (path.extname(normalized) || process.platform !== "win32") {
41
+ return "";
42
+ }
43
+
44
+ for (const ext of executableExtensions()) {
45
+ const candidate = `${normalized}${ext}`;
46
+ if (isExecutable(candidate)) return candidate;
47
+ }
48
+
49
+ return "";
50
+ }
51
+
27
52
  function executableNames(command) {
28
53
  const names = [command];
29
54
  if (path.extname(command)) return names;
@@ -46,12 +71,18 @@ function bundledFfmpegPath() {
46
71
  const resolveFromDependency = () => {
47
72
  try {
48
73
  const candidate = require("ffmpeg-static");
49
- return typeof candidate === "string" ? path.resolve(candidate) : "";
74
+ if (typeof candidate !== "string") return "";
75
+ return resolveExecutable(candidate);
50
76
  } catch {
51
77
  return "";
52
78
  }
53
79
  };
54
80
 
81
+ const resolveFromLocalModule = () => {
82
+ const fallback = path.join(PLUGIN_ROOT, "node_modules", "ffmpeg-static", "ffmpeg");
83
+ return resolveExecutable(fallback);
84
+ };
85
+
55
86
  const installDependency = () => {
56
87
  const npm = process.platform === "win32" ? "npm.cmd" : "npm";
57
88
  try {
@@ -66,6 +97,7 @@ function bundledFfmpegPath() {
66
97
  };
67
98
 
68
99
  let candidate = resolveFromDependency();
100
+ if (!candidate) candidate = resolveFromLocalModule();
69
101
  if (!candidate && !ffmpegStaticInstallAttempted) {
70
102
  ffmpegStaticInstallAttempted = true;
71
103
  if (installDependency()) {
@@ -73,6 +105,8 @@ function bundledFfmpegPath() {
73
105
  }
74
106
  }
75
107
 
108
+ if (!candidate) candidate = resolveFromLocalModule();
109
+
76
110
  ffmpegStaticPathCache = candidate || "";
77
111
  return ffmpegStaticPathCache;
78
112
  }
@@ -134,7 +168,8 @@ function candidateCommands(command, options = {}) {
134
168
  export function resolveCommand(command, options = {}) {
135
169
  for (const candidate of candidateCommands(command, options)) {
136
170
  if (looksLikePath(candidate)) {
137
- if (isExecutable(candidate)) return candidate;
171
+ const resolved = resolveExecutable(candidate);
172
+ if (resolved) return resolved;
138
173
  continue;
139
174
  }
140
175
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hxnnxs/opencode-voice",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "Local voice input plugin for OpenCode",
5
5
  "type": "module",
6
6
  "license": "MIT",