@fastagent-sh/voicenote 0.18.3 → 0.18.5
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 +28 -6
- package/README.zh-CN.md +2 -0
- package/package.json +1 -1
- package/src/cli.ts +17 -20
- package/src/envConfig.ts +33 -14
- package/src/piProvider.ts +4 -0
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ CLI command: `vn`
|
|
|
6
6
|
|
|
7
7
|
[中文文档 / Chinese documentation](README.zh-CN.md)
|
|
8
8
|
|
|
9
|
-
Currently tuned for the PHILIPS VTR6500 voice recorder, but the workflow is generic: scan recordings under a mount point → transcribe with speaker diarization →
|
|
9
|
+
Currently tuned for the PHILIPS VTR6500 voice recorder, but the workflow is generic: scan recordings under a mount point → transcribe with speaker diarization → the selected summary model performs cleanup and process reconstruction → produce smart notes.
|
|
10
10
|
|
|
11
11
|
**Two ways to use it:**
|
|
12
12
|
|
|
@@ -130,6 +130,24 @@ provider that actually failed. If that empties the chain, `vn run --mode notes`
|
|
|
130
130
|
skips instead of paying for a transcript whose summary cannot happen.
|
|
131
131
|
`vn doctor` prints the effective chain and the status of anything not ready.
|
|
132
132
|
|
|
133
|
+
### DeepSeek notes
|
|
134
|
+
|
|
135
|
+
In the desktop app, open **Settings → Notes generation**, select **DeepSeek API**, enter your API key, and save. The model defaults to `deepseek-v4-flash`; enter `deepseek-v4-pro` to use Pro. ChatGPT sign-in is only shown for configurations that use ChatGPT. Audio transcription continues to use Volcano.
|
|
136
|
+
|
|
137
|
+
For the CLI, merge these values into `~/.config/voicenote/config.json`:
|
|
138
|
+
|
|
139
|
+
```json
|
|
140
|
+
{
|
|
141
|
+
"VOICENOTE_PI_PROVIDER": "deepseek",
|
|
142
|
+
"VOICENOTE_PI_MODEL_SUMMARY": "deepseek-v4-flash",
|
|
143
|
+
"DEEPSEEK_API_KEY": "..."
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
The notes-specific model setting takes precedence over `VOICENOTE_PI_MODEL`. When switching providers in the GUI, the model resets to that provider's default. API keys can also come from pi's authentication file or the environment; pi's authentication file takes priority over API-key environment variables.
|
|
148
|
+
|
|
149
|
+
Save changes, then check `vn doctor` or the app's Status panel. Credential checks confirm configuration, not API connectivity or account balance. Each provider in a fallback chain receives the same model ID, so configure DeepSeek on its own rather than mixing it with OpenAI.
|
|
150
|
+
|
|
133
151
|
## Usage
|
|
134
152
|
|
|
135
153
|
```bash
|
|
@@ -176,7 +194,9 @@ The install script writes an editable template:
|
|
|
176
194
|
}
|
|
177
195
|
```
|
|
178
196
|
|
|
179
|
-
Changes take effect on the next `vn run`.
|
|
197
|
+
Changes take effect on the next `vn run`. Config values and unquoted/double-quoted `.zshrc` exports support simple `$VAR` / `${VAR}` references to other settings and `$HOME`. Single-quoted shell values stay literal. Shell commands are never executed. Runtime references honor inherited environment values; scheduler comparisons resolve from files alone.
|
|
198
|
+
|
|
199
|
+
A legacy `~/.config/voicenote/speakers.json` is still read as a compatibility fallback.
|
|
180
200
|
|
|
181
201
|
## Workflow
|
|
182
202
|
|
|
@@ -219,7 +239,7 @@ The LaunchAgent invokes `vn run` every 60 seconds. It skips safely when no recor
|
|
|
219
239
|
|
|
220
240
|
> Config changes (`config.json` or `~/.zshrc`) are picked up automatically by the background agent on its next run — no reinstall needed. The plist only snapshots real environment variables and pi's absolute path: **after changing `VOICENOTE_PI_BIN`, re-run `vn install-launch-agent` and reload** (`vn upgrade` regenerates the plist automatically). If pi is not signed in or ASR is not configured, the agent skips processing instead of burning ASR spend.
|
|
221
241
|
>
|
|
222
|
-
>
|
|
242
|
+
> Proxy values that match the file configuration, including expanded variable references, are not embedded and produce no override warning. Values supplied only by the shell, or differing from the files, are embedded as explicit overrides. To clear an unwanted override, update or unset the shell variable, then run `vn install-launch-agent --load`. Prefer `LOCAL_PROXY_HOST`/`LOCAL_PROXY_PORT` in `config.json` for proxy configuration.
|
|
223
243
|
|
|
224
244
|
Logs:
|
|
225
245
|
|
|
@@ -259,7 +279,7 @@ A self-contained macOS `.app` (Tauri v2) for **non-terminal users**: the target
|
|
|
259
279
|
|
|
260
280
|
**Positioning**: the GUI is only a "status dashboard + quick access to output" — it does **not** drive processing. The full pipeline runs autonomously every 60s via the background LaunchAgent using the bundled engine (it keeps running with the GUI closed).
|
|
261
281
|
|
|
262
|
-
- First run:
|
|
282
|
+
- First run: settings (identity / Volcano keys / notes provider / proxy). Choose DeepSeek with an API key, or ChatGPT with browser sign-in (`vn login`'s browser-callback flow).
|
|
263
283
|
- After that: the main view shows agent activity + recent notes (open note / open folder)
|
|
264
284
|
|
|
265
285
|
### What's bundled
|
|
@@ -271,7 +291,7 @@ A self-contained macOS `.app` (Tauri v2) for **non-terminal users**: the target
|
|
|
271
291
|
| `vn` (compiled) | externalBin | pipeline + ChatGPT sign-in |
|
|
272
292
|
| `bun` | externalBin | runs pi |
|
|
273
293
|
| `ffprobe` (native arm64 static) | externalBin | audio duration (pi only needs ffprobe, not all of ffmpeg) |
|
|
274
|
-
| `pi` + node_modules | resource | notes backend (ChatGPT
|
|
294
|
+
| `pi` + node_modules | resource | notes backend (ChatGPT, OpenAI API, or DeepSeek) |
|
|
275
295
|
|
|
276
296
|
At runtime, Rust generates a wrapper (`exec <bundled bun> <bundled pi/cli.js> "$@"`) and injects `VOICENOTE_PI_BIN` / `VOICENOTE_FFPROBE_BIN` into `vn`. Release builds are **universal** (x86_64 + arm64; vn/bun/ffprobe each merged with `lipo`; pi is JS and needs none).
|
|
277
297
|
|
|
@@ -317,12 +337,14 @@ irm https://raw.githubusercontent.com/fastagent-sh/voicenote/main/scripts/instal
|
|
|
317
337
|
|
|
318
338
|
`install-app.sh` downloads the packaged `.app` from GitHub Releases → installs to `/Applications` → **removes the quarantine flag for the user** (Gatekeeper bypass for un-notarized builds) → opens it. The target machine needs no bun/pi/ffprobe/global vn (all bundled).
|
|
319
339
|
|
|
320
|
-
**First launch**: the app lands on Settings
|
|
340
|
+
**First launch**: the app lands on Settings. Fill in identity, your Volcano ASR/TOS keys, notes provider/model, and proxy as needed. For DeepSeek or OpenAI API, enter the corresponding API key; for ChatGPT, save and click "Sign in to ChatGPT" in the Status panel. Saving installs and loads the background LaunchAgent using the bundled engine. Once credentials are configured, plug in the recorder for automatic transcription and notes.
|
|
321
341
|
|
|
322
342
|
> The background agent label is `sh.fastagent.voicenote` (same as the CLI version; only one exists per machine). If the `.app` is moved, open it once to recalibrate the plist.
|
|
323
343
|
|
|
324
344
|
**Upgrades**: since 0.1.9 the app has a built-in updater — open the app → "Settings → Software update" → "Check for updates"; when a new version appears, click "Download & install"; the app restarts automatically with config/notes preserved. For first installs, or upgrades from 0.1.8 and earlier (which had no updater), re-run the one-line install script above.
|
|
325
345
|
|
|
346
|
+
> **Windows, from 0.1.11 or earlier**: those builds point their updater at the pre-rebrand repo, which still exists and stops at 0.1.11 — "Check for updates" therefore always reports "up to date". The bundle identifier changed in the same rebrand, so re-running the installer does *not* replace them; both copies stay installed under the same name. Uninstall the old VoiceNote (Settings → Apps) first, then run the one-line install. Config and notes are untouched by the uninstall.
|
|
347
|
+
|
|
326
348
|
### Maintainers: packaging + release
|
|
327
349
|
|
|
328
350
|
**Automatic (recommended)**: push an `app-v*` tag to trigger `.github/workflows/release-app.yml`:
|
package/README.zh-CN.md
CHANGED
|
@@ -321,6 +321,8 @@ irm https://raw.githubusercontent.com/fastagent-sh/voicenote/main/scripts/instal
|
|
|
321
321
|
|
|
322
322
|
**升级**:0.1.9 起内置自动更新 —— 打开 app →「设置 → 软件更新」→「检查更新」,有新版点「下载并安装」,装好自动重启,配置/纪要均保留。首次安装、或从 0.1.8 及更早版本升级(它们还没有更新器),重跑上面的一键安装脚本即可。
|
|
323
323
|
|
|
324
|
+
> **Windows 从 0.1.11 及更早版本升级**:这些构建的更新地址指向改名前的旧仓库,而旧仓库至今存在、最新版停在 0.1.11,所以「检查更新」永远显示已是最新。同一次改名还换了 bundle identifier,重跑安装脚本**不会**覆盖旧版,两份会同名共存。先在「设置 → 应用」里卸载旧的 VoiceNote,再跑一键安装。卸载不影响配置和纪要。
|
|
325
|
+
|
|
324
326
|
### 维护者:打包 + 发布
|
|
325
327
|
|
|
326
328
|
**自动(推荐)**:打 `app-v*` tag 触发 `.github/workflows/release-app.yml`:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fastagent-sh/voicenote",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.5",
|
|
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
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { cac } from 'cac'
|
|
3
3
|
import { deriveNoProxy, envKeysToEmbed, hydrateFromFileEnv, parseFileEnv } from './envConfig'
|
|
4
4
|
import { parseLockOwner } from './runLock'
|
|
5
|
-
import { parsePiAuthStatus, parseProviderChain, usableChain, type PiAuthStatus } from './piProvider'
|
|
5
|
+
import { defaultPiModel, parsePiAuthStatus, parseProviderChain, usableChain, type PiAuthStatus } from './piProvider'
|
|
6
6
|
import { applyOutcome, buildJobsView, classify, emptyState, localIso, MAX_ATTEMPTS, migrateLegacyState, ownsOutput, parseJobsLimit, parseStateFile, parseStrictJson, patchJob, pruneUnseen, reconcileInterrupted, startAttempt, SUMMARY_FAILED_STATUS, type CurrentJob, type JobRecord, type StateFile } from './jobs'
|
|
7
7
|
import { createHash, createHmac, randomUUID } from 'node:crypto'
|
|
8
8
|
import { appendFile, chmod, mkdir, readFile, writeFile, copyFile, rename, unlink, stat, readdir, rm } from 'node:fs/promises'
|
|
@@ -13,7 +13,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
|
13
13
|
import { spawn, spawnSync } from 'node:child_process'
|
|
14
14
|
import os from 'node:os'
|
|
15
15
|
|
|
16
|
-
const VERSION = '0.18.
|
|
16
|
+
const VERSION = '0.18.5'
|
|
17
17
|
const LAUNCH_AGENT_LABEL = 'sh.fastagent.voicenote'
|
|
18
18
|
const LAUNCH_AGENT_LABEL_LEGACY = 'com.kid7st.voicenote' // pre-fastagent installs; cleaned up on install
|
|
19
19
|
const TASK_NAME = 'VoiceNote' // Windows Task Scheduler name (mac uses LAUNCH_AGENT_LABEL)
|
|
@@ -137,6 +137,7 @@ const ENV_KEYS = [
|
|
|
137
137
|
'HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY', 'NO_PROXY',
|
|
138
138
|
'LOCAL_PROXY_HOST', 'LOCAL_PROXY_PORT', 'LOCAL_NO_PROXY',
|
|
139
139
|
'OPENAI_API_KEY',
|
|
140
|
+
'DEEPSEEK_API_KEY',
|
|
140
141
|
]
|
|
141
142
|
|
|
142
143
|
// Volcano endpoints (TOS object storage + openspeech ASR) should NEVER go through
|
|
@@ -208,20 +209,21 @@ function applyDerivedProxy(): void {
|
|
|
208
209
|
}
|
|
209
210
|
}
|
|
210
211
|
|
|
211
|
-
//
|
|
212
|
-
//
|
|
212
|
+
// File values for each ENV_KEY. Hydration passes the current environment for
|
|
213
|
+
// variable references; scheduler comparison uses files alone. Primary source is
|
|
214
|
+
// ~/.config/voicenote/config.json
|
|
213
215
|
// (ENV-style runtime keys at the top level; identity under `speakers`); the
|
|
214
216
|
// legacy fallback is `export KEY=...` lines in ~/.zshrc, for CLI installs
|
|
215
217
|
// that predate config.json. Precedence/expansion logic lives in envConfig.ts
|
|
216
218
|
// (pure + tested). Two consumers: loadEnvConfig() hydrates these into
|
|
217
219
|
// process.env for keys the real environment doesn't set, and launchAgentEnv()
|
|
218
220
|
// uses them to decide which values are recoverable at run time.
|
|
219
|
-
function fileProvidedEnv(): Record<string, string> {
|
|
221
|
+
function fileProvidedEnv(environment: Record<string, string | undefined> = {}): Record<string, string> {
|
|
220
222
|
const data = loadJsonSync<Record<string, unknown>>(CONFIG_ENV_PATH, {})
|
|
221
223
|
let zshrc: string | null = null
|
|
222
224
|
const zshrcPath = join(os.homedir(), '.zshrc')
|
|
223
225
|
if (existsSync(zshrcPath)) { try { zshrc = readFileSync(zshrcPath, 'utf8') } catch { zshrc = null } }
|
|
224
|
-
return parseFileEnv(ENV_KEYS, data, zshrc, os.homedir())
|
|
226
|
+
return parseFileEnv(ENV_KEYS, data, zshrc, os.homedir(), environment)
|
|
225
227
|
}
|
|
226
228
|
|
|
227
229
|
let envConfigLoaded = false
|
|
@@ -230,7 +232,7 @@ function loadEnvConfig(): void {
|
|
|
230
232
|
envConfigLoaded = true
|
|
231
233
|
// Precedence: process.env > config.json (GUI) > ~/.zshrc (legacy); an
|
|
232
234
|
// explicit empty string in the environment is never overridden.
|
|
233
|
-
const toApply = hydrateFromFileEnv(ENV_KEYS, process.env, fileProvidedEnv())
|
|
235
|
+
const toApply = hydrateFromFileEnv(ENV_KEYS, process.env, fileProvidedEnv(process.env))
|
|
234
236
|
for (const [key, v] of Object.entries(toApply)) { process.env[key] = v; hydratedEnvKeys.add(key) }
|
|
235
237
|
// Derive http_proxy etc. from LOCAL_PROXY_HOST/PORT regardless of source, and
|
|
236
238
|
// always keep Volcano hosts on NO_PROXY. (Runs even with no config files.)
|
|
@@ -1493,7 +1495,8 @@ function piProviderAuthStatus(provider: string): PiAuthStatus {
|
|
|
1493
1495
|
const cached = piAuthStatusCache.get(provider)
|
|
1494
1496
|
if (cached && Date.now() - cached.at < PI_AUTH_TTL_MS) return cached.status
|
|
1495
1497
|
const inv = piInvocation(['auth', 'check', '--provider', provider, '--json'])
|
|
1496
|
-
|
|
1498
|
+
// Bun's spawnSync needs an explicit env to inherit keys loaded from config.json.
|
|
1499
|
+
const out = spawnSync(inv.bin, inv.args, { encoding: 'utf8', timeout: PI_AUTH_PROBE_MS, windowsHide: true, env: process.env })
|
|
1497
1500
|
// No pi binary is deterministic evidence in its own right — next run gets the
|
|
1498
1501
|
// same answer — so it belongs with 'unusable', not with a probe that timed out.
|
|
1499
1502
|
const status = (out.error as NodeJS.ErrnoException | undefined)?.code === 'ENOENT'
|
|
@@ -1524,7 +1527,7 @@ function piProviderFor(): string {
|
|
|
1524
1527
|
}
|
|
1525
1528
|
|
|
1526
1529
|
function piCodexModelFor(): string {
|
|
1527
|
-
return process.env.VOICENOTE_PI_MODEL_SUMMARY || process.env.VOICENOTE_PI_MODEL ||
|
|
1530
|
+
return process.env.VOICENOTE_PI_MODEL_SUMMARY || process.env.VOICENOTE_PI_MODEL || defaultPiModel(piConfiguredProviders()[0]!)
|
|
1528
1531
|
}
|
|
1529
1532
|
|
|
1530
1533
|
function stripJsonFences(text: string): string {
|
|
@@ -1661,7 +1664,7 @@ function piSummaryToolsHint(contextDir: string): string {
|
|
|
1661
1664
|
return `Before writing the notes you have two read-only tools: read and grep. Your current working directory (cwd) is \`${contextDir}\` (the configured notes/reference directory); use relative paths for grep/read.\n\nGoal: use existing context to align names, speakers, client/project names, product names, and domain terms in this note; do not maintain or assume a separate glossary.\n\nSuggested flow:\n- First extract the most likely client/project/product keywords from the title, filename, and transcript.\n- If a clear topic matches, prefer grep/read on related index pages, project docs, status records, or the 3-5 most recent related notes in the same directory; use them to identify Speaker B/C/F etc., common aliases, product names, and term spellings.\n- If no clear topic matches, grep the current directory with keywords and read only the few most relevant files.\n- Before output, do one names/terms lint pass: eliminate leftover Speaker A/B/C, obviously misheard names, product-name variants, and outdated names; when context is insufficient, keep the uncertainty — never guess.\n\nConstraints:\n- At most 10 tool calls total; if the transcript alone is sufficient, make none.\n- Read only within \`${contextDir}\`; skip directories that clearly involve personal privacy/credentials/finance (e.g. identity / credentials / finance).\n- Found information is only for consistency and background calibration; never write content absent from this transcript into the notes as new meeting facts.\n- Do not attempt to write files or call bash (those tools are not enabled).`
|
|
1662
1665
|
}
|
|
1663
1666
|
|
|
1664
|
-
// Summary runs
|
|
1667
|
+
// Summary runs through pi with the configured provider. The agent's working dir IS the knowledge
|
|
1665
1668
|
// base, so read/grep/find operate there directly. If a configured context dir is
|
|
1666
1669
|
// missing, say so loudly and run without tools rather than searching the wrong
|
|
1667
1670
|
// tree (tools, the cwd hint, and the spawn cwd move together).
|
|
@@ -2248,12 +2251,6 @@ async function launchAgentEnv(): Promise<Record<string, string>> {
|
|
|
2248
2251
|
// about: it may equally be a stale shell session, and it will keep
|
|
2249
2252
|
// overriding config edits until the scheduler is reinstalled.
|
|
2250
2253
|
//
|
|
2251
|
-
// Known blind spot: the matrix compares each key against its OWN file value,
|
|
2252
|
-
// so it can't see cross-key derivations. A real-env http_proxy is embedded
|
|
2253
|
-
// as-is and will shadow a GUI edit to LOCAL_PROXY_HOST (different key name)
|
|
2254
|
-
// until reinstall. Only no_proxy is special-cased (originals below) because
|
|
2255
|
-
// WE synthesize it; http_proxy from a user's shell is left as a real value.
|
|
2256
|
-
//
|
|
2257
2254
|
// no_proxy/NO_PROXY carry a volcano-hosts merge we added; substitute the
|
|
2258
2255
|
// pre-merge real-env original (or drop it entirely if we synthesized the
|
|
2259
2256
|
// whole value) so the scheduler never freezes our merge over config edits.
|
|
@@ -2263,7 +2260,7 @@ async function launchAgentEnv(): Promise<Record<string, string>> {
|
|
|
2263
2260
|
const { embed, frozenOverrides } = envKeysToEmbed(ENV_KEYS, embedEnv, hydratedEnvKeys, fileEnv)
|
|
2264
2261
|
Object.assign(env, embed)
|
|
2265
2262
|
for (const k of frozenOverrides) {
|
|
2266
|
-
console.error(`Warning: environment ${k}
|
|
2263
|
+
console.error(`Warning: environment ${k} overrides the config file. Update or unset it, then re-run \`vn install-launch-agent --load\` to apply the intended value.`)
|
|
2267
2264
|
}
|
|
2268
2265
|
// Embed pi's ABSOLUTE path so launchd resolves it regardless of the fixed plist
|
|
2269
2266
|
// PATH (npm global bin can live outside it under nvm / custom prefixes). Resolve
|
|
@@ -2753,8 +2750,8 @@ async function collectDoctor() {
|
|
|
2753
2750
|
const ff = await runCommand(ffprobeBin(), ['-version'], 5000)
|
|
2754
2751
|
const v = config.volcano
|
|
2755
2752
|
const tools = piSummaryTools()
|
|
2756
|
-
//
|
|
2757
|
-
|
|
2753
|
+
// An explicit status refresh must see newly saved keys and OAuth logins.
|
|
2754
|
+
piAuthStatusCache.clear()
|
|
2758
2755
|
const configuredProviders = piConfiguredProviders()
|
|
2759
2756
|
const effectiveProviders = piProviderCandidates()
|
|
2760
2757
|
const providerStatus = Object.fromEntries(configuredProviders.map(p => [p, piProviderAuthStatus(p)]))
|
|
@@ -2989,7 +2986,7 @@ async function serve(): Promise<void> {
|
|
|
2989
2986
|
|
|
2990
2987
|
const cli = cac('vn')
|
|
2991
2988
|
|
|
2992
|
-
cli.command('run', 'Scan recorder and process recordings (Volcano ASR + pi
|
|
2989
|
+
cli.command('run', 'Scan recorder and process recordings (Volcano ASR + pi notes)')
|
|
2993
2990
|
.option('--mode <mode>', 'Output mode: notes (default) | transcript', { default: 'notes' })
|
|
2994
2991
|
.option('--latest', 'Only process newest eligible recording')
|
|
2995
2992
|
.option('--force', 'Reprocess already processed recordings')
|
package/src/envConfig.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// Pure logic behind cli.ts's env-config provenance. Extracted (no fs, no
|
|
2
2
|
// process.env) so its invariants are testable — see envConfig.test.ts:
|
|
3
3
|
//
|
|
4
|
-
// 1. File precedence: config.json (GUI) wins over ~/.zshrc (legacy CLI)
|
|
5
|
-
// $
|
|
4
|
+
// 1. File precedence: config.json (GUI) wins over ~/.zshrc (legacy CLI).
|
|
5
|
+
// Simple $VAR/${VAR} references resolve from these values without running
|
|
6
|
+
// shell code; runtime hydration may also use inherited environment values.
|
|
6
7
|
// 2. Hydration: only keys the real environment does NOT set are filled from
|
|
7
8
|
// files — an explicit empty string in the environment (e.g.
|
|
8
9
|
// VOICENOTE_PI_SUMMARY_TOOLS="") counts as set and is never overridden.
|
|
@@ -17,28 +18,46 @@
|
|
|
17
18
|
// warn: it may equally be a stale shell session shadowing a fresh config
|
|
18
19
|
// edit, and it will keep overriding until the scheduler is reinstalled.
|
|
19
20
|
|
|
20
|
-
/**
|
|
21
|
+
/** Omit `environment` when checking which values files can reproduce on their own. */
|
|
21
22
|
export function parseFileEnv(
|
|
22
23
|
keys: readonly string[],
|
|
23
24
|
configData: Record<string, unknown>,
|
|
24
25
|
zshrcContent: string | null,
|
|
25
26
|
home: string,
|
|
27
|
+
environment: Record<string, string | undefined> = {},
|
|
26
28
|
): Record<string, string> {
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
+
const raw = new Map<string, string>()
|
|
30
|
+
const literal = new Set<string>()
|
|
29
31
|
for (const key of keys) {
|
|
30
32
|
const v = configData[key]
|
|
31
|
-
if (typeof v === 'string')
|
|
33
|
+
if (typeof v === 'string') { raw.set(key, v); continue }
|
|
34
|
+
const pattern = new RegExp(`(?:^|\\n)\\s*export\\s+${key}=(?:"([^"]*)"|'([^']*)'|([^\\s"'#]+))`)
|
|
35
|
+
const match = zshrcContent?.match(pattern)
|
|
36
|
+
const value = match?.slice(1).find(v => v !== undefined)
|
|
37
|
+
if (value !== undefined) raw.set(key, value)
|
|
38
|
+
if (match?.[2] !== undefined) literal.add(key)
|
|
32
39
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
+
const resolved = new Map<string, string>()
|
|
41
|
+
const visiting = new Set<string>()
|
|
42
|
+
const resolve = (key: string): string => {
|
|
43
|
+
const cached = resolved.get(key)
|
|
44
|
+
if (cached !== undefined) return cached
|
|
45
|
+
if (visiting.has(key)) throw new Error(`Circular config variable reference: ${key}`)
|
|
46
|
+
visiting.add(key)
|
|
47
|
+
const value = raw.get(key)!
|
|
48
|
+
const expanded = literal.has(key) ? value : value.replace(/\\(\$)|\$(?:\{([A-Za-z_][A-Za-z0-9_]*)\}|([A-Za-z_][A-Za-z0-9_]*))/g, (token, escaped, braced, bare) => {
|
|
49
|
+
if (escaped) return escaped
|
|
50
|
+
const ref = braced || bare
|
|
51
|
+
if (ref === 'HOME') return home
|
|
52
|
+
// Unknown references stay literal, so they cannot look recoverable from files.
|
|
53
|
+
if (Object.hasOwn(environment, ref) && environment[ref] !== undefined) return environment[ref]!
|
|
54
|
+
return raw.has(ref) ? resolve(ref) : token
|
|
55
|
+
})
|
|
56
|
+
visiting.delete(key)
|
|
57
|
+
resolved.set(key, expanded)
|
|
58
|
+
return expanded
|
|
40
59
|
}
|
|
41
|
-
return
|
|
60
|
+
return Object.fromEntries(Array.from(raw.keys(), key => [key, resolve(key)]))
|
|
42
61
|
}
|
|
43
62
|
|
|
44
63
|
/** Which keys to copy from fileEnv into an environment (invariant 2). */
|
package/src/piProvider.ts
CHANGED
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
|
|
4
4
|
export const DEFAULT_PI_PROVIDERS = ['openai-codex', 'openai']
|
|
5
5
|
|
|
6
|
+
export function defaultPiModel(provider: string): string {
|
|
7
|
+
return provider === 'deepseek' ? 'deepseek-v4-flash' : 'gpt-5.5'
|
|
8
|
+
}
|
|
9
|
+
|
|
6
10
|
export function parseProviderChain(raw: string | undefined): string[] {
|
|
7
11
|
const parsed = Array.from(new Set((raw ?? '').split(',').map(s => s.trim()).filter(Boolean)))
|
|
8
12
|
return parsed.length ? parsed : [...DEFAULT_PI_PROVIDERS]
|