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 +94 -0
- package/README.md +29 -14
- package/extensions/voice-input.ts +101 -11
- package/package.json +3 -2
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
|
-
|
|
73
|
+
In pi, run:
|
|
74
74
|
|
|
75
|
-
```
|
|
76
|
-
|
|
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
|
-
|
|
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
|
-
|
|
84
|
-
|
|
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
|
|
94
|
-
-
|
|
95
|
-
- the
|
|
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.
|
|
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
|
-
|
|
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
|
|
426
|
-
"
|
|
427
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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": [
|