pi-web-voice 0.1.0 → 0.1.1

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/README.md CHANGED
@@ -297,9 +297,14 @@ no restart, and any agent session you have running stays alive.
297
297
 
298
298
  ## Privacy
299
299
 
300
- Audio goes from your browser to your own pi-web origin, and from there to the
301
- speech backend you configured. Credentials stay on the server; the page is only told
302
- which provider is active. Nothing is written to disk and nothing else is contacted.
300
+ Audio goes from your browser to your own pi-web origin, and from there to the speech
301
+ backend you configured. The page is only told which provider is active, never the key.
302
+ Nothing is written to disk and nothing else is contacted.
303
+
304
+ `~/.pi/agent/voice.env` is parsed into a private object rather than merged into
305
+ `process.env`. pi-web runs the agent's shell commands as children of its own process, so
306
+ anything placed in that environment would be handed to every command the agent ever runs.
307
+ Keeping the credentials out of it means only the transcription request sees them.
303
308
 
304
309
  ## Compatibility
305
310
 
package/lib/config.cjs CHANGED
@@ -19,17 +19,39 @@ const path = require("node:path");
19
19
 
20
20
  const ENV_FILE = path.join(os.homedir(), ".pi", "agent", "voice.env");
21
21
 
22
- let loadedEnvFile = null;
22
+ let fileEnv = null;
23
23
 
24
+ /**
25
+ * Reads the key file into a private object.
26
+ *
27
+ * Node's own `process.loadEnvFile` would be shorter, but it merges into
28
+ * `process.env`, and pi-web spawns the agent's shell commands as children of
29
+ * this process. That would hand every command the agent ever runs a copy of
30
+ * the speech credentials. Keeping them in a local object means they are only
31
+ * ever read by the request that needs them.
32
+ */
24
33
  function loadEnvFile() {
25
- if (loadedEnvFile !== null) return loadedEnvFile;
26
- loadedEnvFile = "";
27
- if (!fs.existsSync(ENV_FILE)) return loadedEnvFile;
34
+ if (fileEnv !== null) return fileEnv;
35
+ fileEnv = {};
36
+ if (!fs.existsSync(ENV_FILE)) return fileEnv;
28
37
 
29
38
  try {
30
- // Node applies the same rule as --env-file: the real environment wins.
31
- process.loadEnvFile(ENV_FILE);
32
- loadedEnvFile = ENV_FILE;
39
+ for (const line of fs.readFileSync(ENV_FILE, "utf8").split("\n")) {
40
+ const trimmed = line.trim();
41
+ if (!trimmed || trimmed.startsWith("#")) continue;
42
+ const eq = trimmed.indexOf("=");
43
+ if (eq < 1) continue;
44
+ const key = trimmed.slice(0, eq).replace(/^export\s+/, "").trim();
45
+ let value = trimmed.slice(eq + 1).trim();
46
+ if (
47
+ value.length >= 2 &&
48
+ ((value.startsWith('"') && value.endsWith('"')) ||
49
+ (value.startsWith("'") && value.endsWith("'")))
50
+ ) {
51
+ value = value.slice(1, -1);
52
+ }
53
+ if (key) fileEnv[key] = value;
54
+ }
33
55
 
34
56
  const mode = fs.statSync(ENV_FILE).mode & 0o077;
35
57
  if (mode !== 0 && process.platform !== "win32") {
@@ -38,7 +60,12 @@ function loadEnvFile() {
38
60
  } catch (error) {
39
61
  console.error(`[pi-web-voice] cannot read ${ENV_FILE}: ${error.message}`);
40
62
  }
41
- return loadedEnvFile;
63
+ return fileEnv;
64
+ }
65
+
66
+ /** Real environment first, then the key file. */
67
+ function fromEnv(name) {
68
+ return process.env[name] ?? loadEnvFile()[name];
42
69
  }
43
70
 
44
71
  // Not worth configuring: one correct value each.
@@ -76,8 +103,14 @@ function azureEndpoint(value, suffix) {
76
103
  }
77
104
 
78
105
  function loadConfig() {
79
- const envFile = loadEnvFile();
80
- const env = process.env;
106
+ loadEnvFile();
107
+ const envFile = fs.existsSync(ENV_FILE) ? ENV_FILE : "";
108
+ const env = new Proxy(
109
+ {},
110
+ {
111
+ get: (_target, name) => (typeof name === "string" ? fromEnv(name) : undefined),
112
+ },
113
+ );
81
114
 
82
115
  const provider = text(
83
116
  env.PI_VOICE_PROVIDER,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-web-voice",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Voice input for pi-web. A NODE_OPTIONS hook that injects a microphone button into the chat composer and transcribes speech with Azure AI Speech, Azure OpenAI, or any OpenAI-compatible endpoint. No fork, no patching, no rebuild.",
5
5
  "keywords": [
6
6
  "pi",