@fastagent-sh/voicenote 0.18.4 → 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 CHANGED
@@ -194,7 +194,9 @@ The install script writes an editable template:
194
194
  }
195
195
  ```
196
196
 
197
- Changes take effect on the next `vn run`. A legacy `~/.config/voicenote/speakers.json` is still read as a compatibility fallback.
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.
198
200
 
199
201
  ## Workflow
200
202
 
@@ -237,7 +239,7 @@ The LaunchAgent invokes `vn run` every 60 seconds. It skips safely when no recor
237
239
 
238
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.
239
241
  >
240
- > Exception: if you `export http_proxy=...` directly in your shell (instead of using `LOCAL_PROXY_HOST`) and then run `vn install-launch-agent`, that real env value is snapshotted into the plist and keeps overriding later `LOCAL_PROXY_HOST` changes; re-run `vn install-launch-agent` to clear it. Prefer `LOCAL_PROXY_HOST`/`LOCAL_PROXY_PORT` for proxy configuration.
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.
241
243
 
242
244
  Logs:
243
245
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fastagent-sh/voicenote",
3
- "version": "0.18.4",
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
@@ -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.4'
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)
@@ -209,20 +209,21 @@ function applyDerivedProxy(): void {
209
209
  }
210
210
  }
211
211
 
212
- // What the config files would provide for each ENV_KEY, independent of this
213
- // process's environment. Primary source is ~/.config/voicenote/config.json
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
214
215
  // (ENV-style runtime keys at the top level; identity under `speakers`); the
215
216
  // legacy fallback is `export KEY=...` lines in ~/.zshrc, for CLI installs
216
217
  // that predate config.json. Precedence/expansion logic lives in envConfig.ts
217
218
  // (pure + tested). Two consumers: loadEnvConfig() hydrates these into
218
219
  // process.env for keys the real environment doesn't set, and launchAgentEnv()
219
220
  // uses them to decide which values are recoverable at run time.
220
- function fileProvidedEnv(): Record<string, string> {
221
+ function fileProvidedEnv(environment: Record<string, string | undefined> = {}): Record<string, string> {
221
222
  const data = loadJsonSync<Record<string, unknown>>(CONFIG_ENV_PATH, {})
222
223
  let zshrc: string | null = null
223
224
  const zshrcPath = join(os.homedir(), '.zshrc')
224
225
  if (existsSync(zshrcPath)) { try { zshrc = readFileSync(zshrcPath, 'utf8') } catch { zshrc = null } }
225
- return parseFileEnv(ENV_KEYS, data, zshrc, os.homedir())
226
+ return parseFileEnv(ENV_KEYS, data, zshrc, os.homedir(), environment)
226
227
  }
227
228
 
228
229
  let envConfigLoaded = false
@@ -231,7 +232,7 @@ function loadEnvConfig(): void {
231
232
  envConfigLoaded = true
232
233
  // Precedence: process.env > config.json (GUI) > ~/.zshrc (legacy); an
233
234
  // explicit empty string in the environment is never overridden.
234
- const toApply = hydrateFromFileEnv(ENV_KEYS, process.env, fileProvidedEnv())
235
+ const toApply = hydrateFromFileEnv(ENV_KEYS, process.env, fileProvidedEnv(process.env))
235
236
  for (const [key, v] of Object.entries(toApply)) { process.env[key] = v; hydratedEnvKeys.add(key) }
236
237
  // Derive http_proxy etc. from LOCAL_PROXY_HOST/PORT regardless of source, and
237
238
  // always keep Volcano hosts on NO_PROXY. (Runs even with no config files.)
@@ -2250,12 +2251,6 @@ async function launchAgentEnv(): Promise<Record<string, string>> {
2250
2251
  // about: it may equally be a stale shell session, and it will keep
2251
2252
  // overriding config edits until the scheduler is reinstalled.
2252
2253
  //
2253
- // Known blind spot: the matrix compares each key against its OWN file value,
2254
- // so it can't see cross-key derivations. A real-env http_proxy is embedded
2255
- // as-is and will shadow a GUI edit to LOCAL_PROXY_HOST (different key name)
2256
- // until reinstall. Only no_proxy is special-cased (originals below) because
2257
- // WE synthesize it; http_proxy from a user's shell is left as a real value.
2258
- //
2259
2254
  // no_proxy/NO_PROXY carry a volcano-hosts merge we added; substitute the
2260
2255
  // pre-merge real-env original (or drop it entirely if we synthesized the
2261
2256
  // whole value) so the scheduler never freezes our merge over config edits.
@@ -2265,7 +2260,7 @@ async function launchAgentEnv(): Promise<Record<string, string>> {
2265
2260
  const { embed, frozenOverrides } = envKeysToEmbed(ENV_KEYS, embedEnv, hydratedEnvKeys, fileEnv)
2266
2261
  Object.assign(env, embed)
2267
2262
  for (const k of frozenOverrides) {
2268
- console.error(`Warning: environment ${k} differs from the config file value; the environment value is snapshotted into the scheduler and will override config edits until you re-run \`vn install-launch-agent\`.`)
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.`)
2269
2264
  }
2270
2265
  // Embed pi's ABSOLUTE path so launchd resolves it regardless of the fixed plist
2271
2266
  // PATH (npm global bin can live outside it under nvm / custom prefixes). Resolve
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
- // $HOME tokens are expanded in both.
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
- /** File-provided values for `keys`: config.json over zshrc, $HOME expanded. */
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 out: Record<string, string> = {}
28
- const expand = (v: string) => v.replace(/\$\{?HOME\}?/g, home)
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') out[key] = expand(v)
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
- if (zshrcContent !== null) {
34
- for (const key of keys) {
35
- if (out[key] !== undefined) continue // config.json wins
36
- const pattern = new RegExp(`(?:^|\\n)\\s*export\\s+${key}=(?:"([^"]*)"|'([^']*)'|([^\\s"'#]+))`)
37
- const value = zshrcContent.match(pattern)?.slice(1).find(v => v !== undefined)
38
- if (value !== undefined) out[key] = expand(value)
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 out
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). */