@fastagent-sh/voicenote 0.20.0 → 0.21.0
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 +16 -0
- package/README.zh-CN.md +14 -0
- package/package.json +1 -1
- package/src/cli.ts +31 -11
package/README.md
CHANGED
|
@@ -114,6 +114,7 @@ Optional settings:
|
|
|
114
114
|
"VOICENOTE_MAX_AGE_HOURS": "48",
|
|
115
115
|
"VOICENOTE_PI_BIN": "pi",
|
|
116
116
|
"VOICENOTE_PI_MODEL": "openai-codex/gpt-5.6-sol",
|
|
117
|
+
"PI_CODING_AGENT_DIR": "$HOME/.config/voicenote/pi-agent",
|
|
117
118
|
"VOICENOTE_PI_THINKING": "high",
|
|
118
119
|
"VOICENOTE_PI_SUMMARY_TOOLS": "read,grep",
|
|
119
120
|
"VOICENOTE_CONTEXT_DIR": "/Users/you/vault"
|
|
@@ -131,6 +132,21 @@ in the environment); voicenote never picks a provider and never falls back to a
|
|
|
131
132
|
second one. If pi fails, the transcript is kept and the summary can be retried
|
|
132
133
|
with `vn run --latest`.
|
|
133
134
|
|
|
135
|
+
### Credentials of their own
|
|
136
|
+
|
|
137
|
+
`PI_CODING_AGENT_DIR` relocates pi's config directory, which is where it keeps
|
|
138
|
+
`auth.json`. Point it at a voicenote-owned directory and `vn login` writes there,
|
|
139
|
+
pi refreshes the tokens there, and an interactive pi session cannot clobber
|
|
140
|
+
them — it rewrites its own `auth.json` wholesale on exit, which has silently
|
|
141
|
+
dropped providers before:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
echo '{"env":{"PI_CODING_AGENT_DIR":"$HOME/.config/voicenote/pi-agent"}}' | vn config set
|
|
145
|
+
vn login # signs in and stores credentials in that directory
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
`vn doctor` prints the path it will read (`pi.auth=...`).
|
|
149
|
+
|
|
134
150
|
`DEEPSEEK_API_KEY` and `OPENAI_API_KEY` in the config are only forwarded to pi's
|
|
135
151
|
environment for providers that read them.
|
|
136
152
|
|
package/README.zh-CN.md
CHANGED
|
@@ -114,6 +114,7 @@ brew install ffmpeg
|
|
|
114
114
|
"VOICENOTE_MAX_AGE_HOURS": "48",
|
|
115
115
|
"VOICENOTE_PI_BIN": "pi",
|
|
116
116
|
"VOICENOTE_PI_MODEL": "openai-codex/gpt-5.6-sol",
|
|
117
|
+
"PI_CODING_AGENT_DIR": "$HOME/.config/voicenote/pi-agent",
|
|
117
118
|
"VOICENOTE_PI_THINKING": "high",
|
|
118
119
|
"VOICENOTE_PI_SUMMARY_TOOLS": "read,grep",
|
|
119
120
|
"VOICENOTE_CONTEXT_DIR": "/Users/you/vault"
|
|
@@ -130,6 +131,19 @@ brew install ffmpeg
|
|
|
130
131
|
不选 provider, 也不会回退到第二个。pi 失败时 transcript 会保留, 用 `vn run --latest`
|
|
131
132
|
重试纪要即可。
|
|
132
133
|
|
|
134
|
+
### 给 voicenote 一份独立凭证
|
|
135
|
+
|
|
136
|
+
`PI_CODING_AGENT_DIR` 可以改掉 pi 的配置目录, `auth.json` 就存在那里。指向一个
|
|
137
|
+
voicenote 自己的目录后, `vn login` 写入这里, pi 也在这里刷新 token, 交互式 pi 会话
|
|
138
|
+
碰不到它 —— 那边退出时会整份覆写自己的 `auth.json`, 之前就把 provider 条目悄悄弄丢过:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
echo '{"env":{"PI_CODING_AGENT_DIR":"$HOME/.config/voicenote/pi-agent"}}' | vn config set
|
|
142
|
+
vn login # 登录后凭证就存在该目录
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
`vn doctor` 的 `pi.auth=` 一行会打印实际读取的路径。
|
|
146
|
+
|
|
133
147
|
配置里的 `DEEPSEEK_API_KEY` / `OPENAI_API_KEY` 只是透传给 pi 的环境变量。
|
|
134
148
|
|
|
135
149
|
瞬时性失败(断连、5xx、429)会在同一个 provider 上重试, 次数由 `VOICENOTE_PI_RETRIES`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fastagent-sh/voicenote",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"description": "Voice recordings → diarized transcripts → integrated semantic Markdown notes. Currently optimized for the PHILIPS VTR6500 recorder, but the workflow is generic.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/cli.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
|
12
12
|
import { spawn, spawnSync } from 'node:child_process'
|
|
13
13
|
import os from 'node:os'
|
|
14
14
|
|
|
15
|
-
const VERSION = '0.
|
|
15
|
+
const VERSION = '0.21.0'
|
|
16
16
|
const LAUNCH_AGENT_LABEL = 'sh.fastagent.voicenote'
|
|
17
17
|
const LAUNCH_AGENT_LABEL_LEGACY = 'com.kid7st.voicenote' // pre-fastagent installs; cleaned up on install
|
|
18
18
|
const TASK_NAME = 'VoiceNote' // Windows Task Scheduler name (mac uses LAUNCH_AGENT_LABEL)
|
|
@@ -31,7 +31,15 @@ const LOG_DIR = join(STATE_DIR, 'logs')
|
|
|
31
31
|
const LOCK_PATH = join(STATE_DIR, 'run.lock')
|
|
32
32
|
const SPEAKERS_PATH = join(CONFIG_DIR, 'speakers.json')
|
|
33
33
|
const CONFIG_ENV_PATH = join(CONFIG_DIR, 'config.json')
|
|
34
|
-
|
|
34
|
+
// pi keeps credentials in its config dir, which PI_CODING_AGENT_DIR relocates.
|
|
35
|
+
// Point it at a voicenote-owned directory to get an auth.json that only the
|
|
36
|
+
// pipeline reads and refreshes: an interactive pi session rewrites its own
|
|
37
|
+
// auth.json wholesale on exit and has already dropped entries that way.
|
|
38
|
+
// Resolved per call — the env is hydrated from config.json after module load.
|
|
39
|
+
function piAuthPath(): string {
|
|
40
|
+
const dir = process.env.PI_CODING_AGENT_DIR || join(os.homedir(), '.pi', 'agent')
|
|
41
|
+
return join(expandHome(dir), 'auth.json')
|
|
42
|
+
}
|
|
35
43
|
|
|
36
44
|
const AUDIO_EXTENSIONS = new Set(['.mp3', '.wav', '.m4a', '.wma', '.aac', '.flac'])
|
|
37
45
|
|
|
@@ -125,6 +133,7 @@ const ENV_KEYS = [
|
|
|
125
133
|
'VOLCANO_TOS_KEEP',
|
|
126
134
|
'VOICENOTE_PI_BIN',
|
|
127
135
|
'VOICENOTE_PI_CLI',
|
|
136
|
+
'PI_CODING_AGENT_DIR',
|
|
128
137
|
'VOICENOTE_FFPROBE_BIN',
|
|
129
138
|
'VOICENOTE_PI_MODEL',
|
|
130
139
|
'VOICENOTE_PI_THINKING',
|
|
@@ -175,6 +184,14 @@ function systemProxyUrl(): string | null {
|
|
|
175
184
|
} catch { return null }
|
|
176
185
|
}
|
|
177
186
|
|
|
187
|
+
// Bun's child_process does NOT hand a child the proxy variables this process set
|
|
188
|
+
// on process.env (http_proxy/https_proxy/no_proxy and their uppercase forms are
|
|
189
|
+
// special-cased internally; only all_proxy survives). Every spawn that must reach
|
|
190
|
+
// the network through the proxy has to pass them explicitly, so record them here.
|
|
191
|
+
// Without this, a scheduler run whose proxy comes from config.json rather than a
|
|
192
|
+
// real shell env leaves pi with no proxy at all — it fails with `fetch failed`.
|
|
193
|
+
const childProxyEnv: Record<string, string> = {}
|
|
194
|
+
|
|
178
195
|
function applyDerivedProxy(): void {
|
|
179
196
|
const host = process.env.LOCAL_PROXY_HOST
|
|
180
197
|
const port = process.env.LOCAL_PROXY_PORT
|
|
@@ -191,6 +208,7 @@ function applyDerivedProxy(): void {
|
|
|
191
208
|
const needs = (k: string) => !process.env[k] || process.env[k]!.includes('${')
|
|
192
209
|
for (const k of ['http_proxy', 'https_proxy', 'all_proxy', 'HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY']) {
|
|
193
210
|
if (needs(k)) { process.env[k] = url; hydratedEnvKeys.add(k) } // derived, not real env
|
|
211
|
+
childProxyEnv[k] = process.env[k]!
|
|
194
212
|
}
|
|
195
213
|
}
|
|
196
214
|
// no_proxy/NO_PROXY: seed a base when a proxy is active, then always merge
|
|
@@ -201,6 +219,7 @@ function applyDerivedProxy(): void {
|
|
|
201
219
|
for (const k of ['no_proxy', 'NO_PROXY'] as const) {
|
|
202
220
|
const r = deriveNoProxy(process.env[k], premergeRealNoProxy[k], hydratedEnvKeys.has(k), !!url, baseNoProxy, VOLCANO_NO_PROXY_HOSTS)
|
|
203
221
|
process.env[k] = r.runtime
|
|
222
|
+
childProxyEnv[k] = r.runtime
|
|
204
223
|
if (r.hydrate) hydratedEnvKeys.add(k)
|
|
205
224
|
if (r.capture !== undefined) premergeRealNoProxy[k] = r.capture
|
|
206
225
|
}
|
|
@@ -1309,15 +1328,16 @@ function piInvocation(args: string[]): { bin: string; args: string[] } {
|
|
|
1309
1328
|
// ───────────────────────────────────────────────────────────────────────
|
|
1310
1329
|
|
|
1311
1330
|
async function persistPiOAuth(providerId: string, creds: Record<string, unknown>): Promise<void> {
|
|
1312
|
-
|
|
1331
|
+
const authPath = piAuthPath()
|
|
1332
|
+
await mkdir(dirname(authPath), { recursive: true })
|
|
1313
1333
|
let existing: Json = {}
|
|
1314
|
-
if (existsSync(
|
|
1315
|
-
try { existing = JSON.parse(await readFile(
|
|
1334
|
+
if (existsSync(authPath)) {
|
|
1335
|
+
try { existing = JSON.parse(await readFile(authPath, 'utf8')) as Json } catch (e) { warnSideEffect(`parse ${authPath}`, e) }
|
|
1316
1336
|
}
|
|
1317
1337
|
existing[providerId] = { type: 'oauth', ...creds }
|
|
1318
|
-
const tmp = `${
|
|
1338
|
+
const tmp = `${authPath}.tmp-${process.pid}`
|
|
1319
1339
|
await writeFile(tmp, JSON.stringify(existing, null, 2) + '\n', { mode: 0o600 })
|
|
1320
|
-
await rename(tmp,
|
|
1340
|
+
await rename(tmp, authPath)
|
|
1321
1341
|
}
|
|
1322
1342
|
|
|
1323
1343
|
async function loginChatGPT(opts: { json?: boolean; deviceCode?: boolean; emit?: (o: Record<string, unknown>) => void }): Promise<void> {
|
|
@@ -1367,7 +1387,7 @@ async function loginChatGPT(opts: { json?: boolean; deviceCode?: boolean; emit?:
|
|
|
1367
1387
|
}
|
|
1368
1388
|
await persistPiOAuth(oauth.openaiCodexOAuthProvider.id, creds)
|
|
1369
1389
|
if (json) emit({ event: 'success', provider: oauth.openaiCodexOAuthProvider.id })
|
|
1370
|
-
else console.log(`\n✓ Signed in. Credentials saved to ${
|
|
1390
|
+
else console.log(`\n✓ Signed in. Credentials saved to ${piAuthPath()}. Verify with: vn doctor`)
|
|
1371
1391
|
} catch (e: any) {
|
|
1372
1392
|
let message = String(e?.message || e)
|
|
1373
1393
|
if (/unsupported_country_region_territory|\b403\b/.test(message)) {
|
|
@@ -1520,7 +1540,7 @@ async function runPi(opts: {
|
|
|
1520
1540
|
if (opts.appendSystemPrompt) args.push('--append-system-prompt', opts.appendSystemPrompt)
|
|
1521
1541
|
return new Promise<string>((resolve, reject) => {
|
|
1522
1542
|
const inv = piInvocation(args)
|
|
1523
|
-
const child = spawn(inv.bin, inv.args, { stdio: ['pipe', 'pipe', 'pipe'], cwd: opts.cwd, windowsHide: true })
|
|
1543
|
+
const child = spawn(inv.bin, inv.args, { stdio: ['pipe', 'pipe', 'pipe'], cwd: opts.cwd, windowsHide: true, env: { ...process.env, ...childProxyEnv } })
|
|
1524
1544
|
let stdout = '', stderr = ''
|
|
1525
1545
|
const timer = opts.timeoutMs ? setTimeout(() => child.kill('SIGKILL'), opts.timeoutMs) : null
|
|
1526
1546
|
child.stdout.on('data', d => stdout += String(d))
|
|
@@ -2699,7 +2719,7 @@ async function collectDoctor() {
|
|
|
2699
2719
|
// Provider/model/credentials are pi's own configuration; `pi.available` is
|
|
2700
2720
|
// all we can honestly report about whether a summary can run.
|
|
2701
2721
|
summary: { backend: 'pi', model: piSummaryModel() || null, thinking: piThinkingLevel(), tools: tools || null, contextDir: tools ? summaryContextDir(config) : null },
|
|
2702
|
-
pi: { bin: piCodexBin(), version: piCheck.code === 0 ? (piCheck.stdout.trim() || piCheck.stderr.trim() || null) : null, available: piCheck.code === 0, auth: existsSync(
|
|
2722
|
+
pi: { bin: piCodexBin(), version: piCheck.code === 0 ? (piCheck.stdout.trim() || piCheck.stderr.trim() || null) : null, available: piCheck.code === 0, auth: existsSync(piAuthPath()), authPath: piAuthPath() },
|
|
2703
2723
|
// Outbound proxy for HTTPS endpoints (updater/GitHub): honor the standard
|
|
2704
2724
|
// env chain, not just lowercase http_proxy — an https_proxy-only setup must
|
|
2705
2725
|
// still route the updater.
|
|
@@ -2773,7 +2793,7 @@ async function doctor(opts: { json?: boolean } = {}): Promise<void> {
|
|
|
2773
2793
|
console.log(`pi.version=${s.pi.version || 'missing'}`)
|
|
2774
2794
|
// Neutral fact, not an instruction: an API-key user has no auth.json and needs
|
|
2775
2795
|
// nothing fixed.
|
|
2776
|
-
console.log(`pi.auth=${s.pi.auth ? '
|
|
2796
|
+
console.log(`pi.auth=${s.pi.authPath} ${s.pi.auth ? '(present)' : '(missing — fine if a provider API key is set)'}`)
|
|
2777
2797
|
console.log(`defaultMode=notes`)
|
|
2778
2798
|
console.log(`proxy=${s.proxy.url || '<unset>'}`)
|
|
2779
2799
|
console.log(`speakers.self=${s.identity.self || '<unset>'}`)
|