pi-voice-input 0.1.0 → 0.1.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/AGENTS.md ADDED
@@ -0,0 +1,94 @@
1
+ # AGENTS.md
2
+
3
+ Development workflow for this repo.
4
+
5
+ ## Project
6
+
7
+ - Package: `pi-voice-input`
8
+ - GitHub: `git@github.com:tr-nc/pi-voice-input.git`
9
+ - npm: `pi-voice-input`
10
+ - Main extension: `extensions/voice-input.ts`
11
+ - Current provider: VolcEngine WebSocket ASR only
12
+ - Provider architecture should remain extensible so more ASR providers can be added later.
13
+
14
+ ## Secrets and local data
15
+
16
+ - Never commit API keys, `.env`, recordings, logs, caches, or `node_modules`.
17
+ - User credentials belong in `~/.pi/agent/voice-input.env`, usually written by `/voice key`.
18
+ - Do not print or copy real API keys into commits, docs, tests, or command output.
19
+ - The explicit VolcEngine API key URL that should be shown to users is:
20
+ `https://console.volcengine.com/speech/new/setting/apikeys?projectName=default`
21
+
22
+ ## Before committing
23
+
24
+ Run from the repo root:
25
+
26
+ ```bash
27
+ npm install --package-lock=false
28
+ npx tsc --noEmit --module NodeNext --moduleResolution NodeNext --target ES2022 --skipLibCheck --types node extensions/voice-input.ts
29
+ PI_OFFLINE=1 pi -e ./extensions/voice-input.ts --list-models
30
+ npm pack --dry-run
31
+ ```
32
+
33
+ Check that `npm pack --dry-run` includes only publishable files, normally:
34
+
35
+ ```text
36
+ .env.example
37
+ AGENTS.md
38
+ README.md
39
+ extensions/voice-input.ts
40
+ package.json
41
+ ```
42
+
43
+ Clean local generated files before committing:
44
+
45
+ ```bash
46
+ rm -rf node_modules package-lock.json logs recordings
47
+ ```
48
+
49
+ Then check:
50
+
51
+ ```bash
52
+ git status --short
53
+ rg -n "VOLC_API_KEY=|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}" \
54
+ --glob '!node_modules/**' --glob '!package-lock.json' . || true
55
+ ```
56
+
57
+ Use conventional commit messages, for example:
58
+
59
+ - `feat: add voice API key setup command`
60
+ - `fix: handle missing recorder cleanly`
61
+ - `docs: clarify npm installation`
62
+ - `chore: update package metadata`
63
+
64
+ ## Release workflow
65
+
66
+ 1. Bump `package.json` version. npm versions are immutable.
67
+ 2. Validate with the commands above.
68
+ 3. Commit with a conventional commit message.
69
+ 4. Push to GitHub:
70
+
71
+ ```bash
72
+ git push origin main
73
+ ```
74
+
75
+ 5. Publish to npm:
76
+
77
+ ```bash
78
+ npm publish --access public
79
+ ```
80
+
81
+ 6. Update the local installed pi package and verify startup:
82
+
83
+ ```bash
84
+ pi update npm:pi-voice-input
85
+ PI_OFFLINE=1 pi --list-models
86
+ ```
87
+
88
+ If testing a local checkout instead of the npm package, use:
89
+
90
+ ```bash
91
+ pi -e ./extensions/voice-input.ts
92
+ ```
93
+
94
+ Do not leave local development wrappers in `~/.pi/agent/extensions/voice-input.ts` when validating the npm installation path.
package/README.md CHANGED
@@ -70,18 +70,24 @@ Planned provider direction:
70
70
 
71
71
  ## Configure credentials
72
72
 
73
- Create a config file:
73
+ In pi, run:
74
74
 
75
- ```bash
76
- mkdir -p ~/.pi/agent
77
- cp .env.example ~/.pi/agent/voice-input.env
78
- $EDITOR ~/.pi/agent/voice-input.env
75
+ ```text
76
+ /voice key
79
77
  ```
80
78
 
81
- At minimum, set:
79
+ Paste your VolcEngine Speech API key into the prompt. The extension saves it for future sessions and keeps it out of your project files.
82
80
 
83
- ```bash
84
- VOLC_API_KEY=your_volcengine_speech_api_key
81
+ The key URL is also shown inside pi when the key is missing, when you run `/voice key`, and in `/voice help`:
82
+
83
+ ```text
84
+ https://console.volcengine.com/speech/new/setting/apikeys?projectName=default
85
+ ```
86
+
87
+ Then verify:
88
+
89
+ ```text
90
+ /voice config
85
91
  ```
86
92
 
87
93
  You can get/manage the key here:
@@ -90,18 +96,25 @@ https://console.volcengine.com/speech/new/setting/apikeys?projectName=default
90
96
 
91
97
  If `VOLC_API_KEY` is missing, the extension does not silently fail. It shows an error notification explaining:
92
98
 
93
- - that `VOLC_API_KEY` is missing
94
- - where to put it: `~/.pi/agent/voice-input.env`
95
- - the exact config line to add
96
- - the Volcengine API-key settings URL
99
+ - that the current provider API key is missing
100
+ - to run `/voice key`
101
+ - the VolcEngine API-key settings URL
97
102
  - that `/voice config` can be used to verify detection
98
103
 
104
+ Manual fallback:
105
+
106
+ ```bash
107
+ mkdir -p ~/.pi/agent
108
+ cp .env.example ~/.pi/agent/voice-input.env
109
+ $EDITOR ~/.pi/agent/voice-input.env
110
+ ```
111
+
99
112
  ## Configuration reference
100
113
 
101
114
  Example:
102
115
 
103
116
  ```bash
104
- # Required
117
+ # Required for the current provider. Usually set by /voice key.
105
118
  VOLC_API_KEY=your_volcengine_speech_api_key
106
119
 
107
120
  # Current provider: VolcEngine WebSocket ASR endpoint and resource
@@ -139,7 +152,7 @@ Config loading order, later values override earlier ones:
139
152
  3. current-working-directory `.env`
140
153
  4. shell environment variables
141
154
 
142
- Do not commit real credentials. Keep private local values in `.env` or `~/.pi/agent/voice-input.env`.
155
+ Do not commit real credentials. Prefer `/voice key`, or keep private local values in `.env` or `~/.pi/agent/voice-input.env`.
143
156
 
144
157
  ## Usage
145
158
 
@@ -158,6 +171,8 @@ Slash commands:
158
171
  /voice cancel # stop recording without transcribing
159
172
  /voice status # show recorder state
160
173
  /voice config # show effective non-secret config and whether API key is detected
174
+ /voice key # prompt for and save the current provider API key
175
+ /voice help # show setup help, including the explicit VolcEngine API key URL
161
176
  ```
162
177
 
163
178
  ## Notes
@@ -3,6 +3,7 @@ import { Key } from "@earendil-works/pi-tui";
3
3
  import { spawn, spawnSync } from "node:child_process";
4
4
  import { randomUUID } from "node:crypto";
5
5
  import {
6
+ chmodSync,
6
7
  closeSync,
7
8
  existsSync,
8
9
  mkdirSync,
@@ -20,6 +21,8 @@ import WebSocket from "ws";
20
21
 
21
22
  const EXTENSION_DIR = path.dirname(fileURLToPath(import.meta.url));
22
23
  const PACKAGE_ROOT = path.resolve(EXTENSION_DIR, "..");
24
+ const PRIVATE_CONFIG_PATH = path.join(homedir(), ".pi", "agent", "voice-input.env");
25
+ const VOLC_API_KEY_URL = "https://console.volcengine.com/speech/new/setting/apikeys?projectName=default";
23
26
  const DEFAULT_SHORTCUT = Key.ctrlShift("r");
24
27
 
25
28
  const MSG_TYPE_CLIENT_FULL_REQUEST = 0b0001;
@@ -101,7 +104,7 @@ function parseEnvText(text: string): EnvMap {
101
104
 
102
105
  function loadEnvFiles(): EnvMap {
103
106
  const candidates = [
104
- path.join(homedir(), ".pi", "agent", "voice-input.env"),
107
+ PRIVATE_CONFIG_PATH,
105
108
  path.join(PACKAGE_ROOT, ".env"),
106
109
  path.join(process.cwd(), ".env"),
107
110
  ];
@@ -185,6 +188,38 @@ function ensureDir(dir: string) {
185
188
  mkdirSync(dir, { recursive: true });
186
189
  }
187
190
 
191
+ function envValue(value: string): string {
192
+ if (/^[A-Za-z0-9_./:@+-]*$/.test(value)) return value;
193
+ return JSON.stringify(value);
194
+ }
195
+
196
+ function writePrivateEnvValue(name: string, value: string) {
197
+ if (/\r|\n/.test(value)) throw new Error(`${name} must be a single-line value`);
198
+ ensureDir(path.dirname(PRIVATE_CONFIG_PATH));
199
+
200
+ const original = existsSync(PRIVATE_CONFIG_PATH) ? readFileSync(PRIVATE_CONFIG_PATH, "utf8") : "";
201
+ const lines = original ? original.split(/\r?\n/) : [];
202
+ const replacement = `${name}=${envValue(value)}`;
203
+ let replaced = false;
204
+
205
+ const nextLines = lines.map((line) => {
206
+ if (new RegExp(`^\\s*${name}\\s*=`).test(line)) {
207
+ replaced = true;
208
+ return replacement;
209
+ }
210
+ return line;
211
+ });
212
+
213
+ if (!replaced) {
214
+ if (nextLines.length > 0 && nextLines[nextLines.length - 1] !== "") nextLines.push("");
215
+ nextLines.push("# Managed by pi-voice-input. You can also update this with /voice key.");
216
+ nextLines.push(replacement);
217
+ }
218
+
219
+ writeFileSync(PRIVATE_CONFIG_PATH, nextLines.join("\n").replace(/\n*$/, "\n"), { mode: 0o600 });
220
+ chmodSync(PRIVATE_CONFIG_PATH, 0o600);
221
+ }
222
+
188
223
  function timestampForFilename(): string {
189
224
  return new Date().toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "");
190
225
  }
@@ -422,11 +457,9 @@ function parseRecordedWav(filePath: string): { pcm: Buffer; durationMs: number }
422
457
 
423
458
  function missingCredentialsMessage(): string {
424
459
  return [
425
- "Missing VOLC_API_KEY for Volcengine ASR.",
426
- "Create ~/.pi/agent/voice-input.env with:",
427
- " VOLC_API_KEY=your_volcengine_speech_api_key",
428
- "Optional: copy .env.example from this package as a template.",
429
- "API key settings: https://console.volcengine.com/speech/new/setting/apikeys?projectName=default",
460
+ "Missing VOLC_API_KEY for the current VolcEngine ASR provider.",
461
+ "Run /voice key and paste your VolcEngine Speech API key.",
462
+ `Get/create the key here: ${VOLC_API_KEY_URL}`,
430
463
  "Run /voice config to verify whether the key is detected.",
431
464
  ].join("\n");
432
465
  }
@@ -685,10 +718,44 @@ async function toggleRecording(ctx: ExtensionContext) {
685
718
  else await startRecording(ctx);
686
719
  }
687
720
 
721
+ function setupHelp(config = getConfig()): string {
722
+ return [
723
+ "pi Voice Input setup:",
724
+ "- Current provider: VolcEngine WebSocket ASR",
725
+ `- API key: ${config.apiKey ? "set" : "missing"}`,
726
+ "- To save/update the key, run: /voice key",
727
+ `- Get/create a VolcEngine Speech API key here: ${VOLC_API_KEY_URL}`,
728
+ "- After saving the key, run: /voice config",
729
+ ].join("\n");
730
+ }
731
+
732
+ async function configureApiKey(ctx: ExtensionContext, providedKey = "") {
733
+ let apiKey = providedKey.trim();
734
+
735
+ if (!apiKey) {
736
+ if (!ctx.hasUI) {
737
+ ctx.ui.notify(`Run /voice key in interactive pi, or get a key from ${VOLC_API_KEY_URL} and set VOLC_API_KEY.`, "error");
738
+ return;
739
+ }
740
+ ctx.ui.notify(`Get/create a VolcEngine Speech API key here:\n${VOLC_API_KEY_URL}`, "info");
741
+ const current = getConfig().apiKey;
742
+ const placeholder = current ? "Paste a new VolcEngine API key (current key is already set)" : "Paste VOLC_API_KEY";
743
+ apiKey = (await ctx.ui.input("VolcEngine API key", placeholder))?.trim() ?? "";
744
+ }
745
+
746
+ if (!apiKey) {
747
+ ctx.ui.notify("API key unchanged.", "warning");
748
+ return;
749
+ }
750
+
751
+ writePrivateEnvValue("VOLC_API_KEY", apiKey);
752
+ ctx.ui.notify("VolcEngine API key saved for pi voice input. Run /voice config to verify it is detected.", "info");
753
+ }
754
+
688
755
  function configSummary(config: VoiceConfig): string {
689
756
  return [
690
757
  "Voice input config:",
691
- `- api key: ${config.apiKey ? "set" : "missing"}`,
758
+ `- api key: ${config.apiKey ? "set" : "missing"} (update with /voice key)`,
692
759
  `- ws url: ${config.wsUrl}`,
693
760
  `- resource id: ${config.resourceId}`,
694
761
  `- language: ${config.language || "auto"}`,
@@ -697,6 +764,8 @@ function configSummary(config: VoiceConfig): string {
697
764
  `- recordings: ${config.recordingsDir}`,
698
765
  `- state: ${config.statePath}`,
699
766
  `- shortcut: ${config.shortcut}`,
767
+ "Run /voice key to save/update the current provider API key.",
768
+ `VolcEngine API key URL: ${VOLC_API_KEY_URL}`,
700
769
  "Config files checked: ~/.pi/agent/voice-input.env, package .env, current .env; shell env overrides them.",
701
770
  ].join("\n");
702
771
  }
@@ -717,9 +786,11 @@ export default function (pi: ExtensionAPI) {
717
786
  });
718
787
 
719
788
  pi.registerCommand("voice", {
720
- description: "Voice input: start | stop | status | toggle | cancel | config",
789
+ description: "Voice input: start | stop | status | toggle | cancel | config | key | help",
721
790
  handler: async (args, ctx) => {
722
- const action = (args || "toggle").trim().toLowerCase();
791
+ const input = (args || "toggle").trim();
792
+ const action = (input.split(/\s+/, 1)[0] || "toggle").toLowerCase();
793
+ const rest = input.slice(action.length).trim();
723
794
  try {
724
795
  if (action === "start") {
725
796
  await startRecording(ctx);
@@ -743,11 +814,19 @@ export default function (pi: ExtensionAPI) {
743
814
  ctx.ui.notify(configSummary(getConfig()), "info");
744
815
  return;
745
816
  }
817
+ if (["key", "api-key", "apikey", "setup", "configure"].includes(action)) {
818
+ await configureApiKey(ctx, rest);
819
+ return;
820
+ }
821
+ if (["help", "doctor"].includes(action)) {
822
+ ctx.ui.notify(setupHelp(getConfig()), "info");
823
+ return;
824
+ }
746
825
  if (action === "toggle" || action === "") {
747
826
  await toggleRecording(ctx);
748
827
  return;
749
828
  }
750
- ctx.ui.notify("Usage: /voice start | stop | status | toggle | cancel | config", "error");
829
+ ctx.ui.notify("Usage: /voice start | stop | status | toggle | cancel | config | key | help", "error");
751
830
  } catch (error) {
752
831
  ctx.ui.setStatus("voice-input", undefined);
753
832
  ctx.ui.notify(`Voice command error: ${error instanceof Error ? error.message : String(error)}`, "error");
@@ -756,6 +835,17 @@ export default function (pi: ExtensionAPI) {
756
835
  });
757
836
 
758
837
  pi.on("session_start", (_event, ctx) => {
759
- ctx.ui.notify(`Voice input loaded: ${startupConfig.shortcut} toggles recording.`, "info");
838
+ if (getConfig().apiKey) {
839
+ ctx.ui.notify(`Voice input loaded: ${startupConfig.shortcut} toggles recording.`, "info");
840
+ return;
841
+ }
842
+ ctx.ui.notify(
843
+ [
844
+ `Voice input loaded: ${startupConfig.shortcut} toggles recording.`,
845
+ "API key is missing. Run /voice key to set it up.",
846
+ `Get/create a VolcEngine Speech API key here: ${VOLC_API_KEY_URL}`,
847
+ ].join("\n"),
848
+ "warning",
849
+ );
760
850
  });
761
851
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-voice-input",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "provider-extensible voice input extension for pi",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -23,7 +23,8 @@
23
23
  "files": [
24
24
  "extensions",
25
25
  ".env.example",
26
- "README.md"
26
+ "README.md",
27
+ "AGENTS.md"
27
28
  ],
28
29
  "pi": {
29
30
  "extensions": [