@fastagent-sh/voicenote 0.20.0 → 0.22.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/src/jobs.ts CHANGED
@@ -20,10 +20,10 @@
20
20
  // fields have to agree about the same fact — an earlier cut expressed "gave up"
21
21
  // as `code` while leaving `state` alone, and the view and the classifier
22
22
  // promptly disagreed about what such a record was.
23
- export type JobState = 'queued' | 'running' | 'done' | 'filtered' | 'error' | 'gave_up'
23
+ type JobState = 'queued' | 'running' | 'done' | 'filtered' | 'error' | 'gave_up'
24
24
 
25
25
  /** Which stage produced a failure; also carries the filter reason. */
26
- export type JobCode = 'transcribe_failed' | 'summary_failed' | 'interrupted' | 'too_small' | 'too_short' | 'too_old' | null
26
+ type JobCode = 'transcribe_failed' | 'summary_failed' | 'interrupted' | 'too_small' | 'too_short' | 'too_old' | null
27
27
 
28
28
  export type JobRecord = {
29
29
  name: string
@@ -60,18 +60,18 @@ export const ownsOutput = (j: JobRecord): boolean => j.paths != null
60
60
  export const MAX_ATTEMPTS = 3
61
61
 
62
62
  /** The subset of JobCode a refusal may write to disk. */
63
- export type FilterCode = Extract<JobCode, 'too_small' | 'too_short' | 'too_old'>
63
+ type FilterCode = Extract<JobCode, 'too_small' | 'too_short' | 'too_old'>
64
64
 
65
65
  // Split by whether the refusal is persisted, so the code that reaches disk is
66
66
  // typed as such. A single `code: string` needed a cast at the write site, and
67
67
  // the cast was the only thing keeping a display-only reason out of the record.
68
- export type Verdict =
68
+ type Verdict =
69
69
  | { run: true }
70
70
  | { run: false; persist: true; code: FilterCode; detail: string | null }
71
71
  | { run: false; persist: false; code: 'already_done' | 'gave_up'; detail: string | null }
72
72
 
73
- export type ScanFacts = { recordedAt: Date; sizeBytes: number; durationSeconds: number | null }
74
- export type Limits = { maxAgeHours: number; minBytes: number; minDurationSeconds: number }
73
+ type ScanFacts = { recordedAt: Date; sizeBytes: number; durationSeconds: number | null }
74
+ type Limits = { maxAgeHours: number; minBytes: number; minDurationSeconds: number }
75
75
 
76
76
  /**
77
77
  * The single answer to "should this recording run now, and in what form".
@@ -114,7 +114,7 @@ export function classify(
114
114
  const RUNNABLE_STATES = new Set<JobState>(['queued', 'running', 'error'])
115
115
 
116
116
  /** Every way a started attempt can end. */
117
- export type Outcome =
117
+ type Outcome =
118
118
  | { kind: 'done'; title: string | null; paths: Record<string, string | null> | null }
119
119
  | { kind: 'summary_failed'; title: string | null; paths: Record<string, string | null> | null; message: string }
120
120
  | { kind: 'failed'; message: string }
@@ -240,7 +240,7 @@ export function pruneUnseen(jobs: Record<string, JobRecord>, seen: Set<string>,
240
240
 
241
241
  export type CurrentJob = { pid: number; source_id: string; step: string; started_at: string }
242
242
 
243
- export type JobView = {
243
+ type JobView = {
244
244
  status: 'running' | 'queued' | 'done' | 'notes_failed' | 'error' | 'gave_up' | 'filtered'
245
245
  name: string
246
246
  title: string | null
@@ -275,7 +275,7 @@ export function patchJob(entry: JobRecord, patch: Partial<JobRecord>, now: strin
275
275
  export const emptyState = (): StateFile => ({ version: 2, jobs: {} })
276
276
 
277
277
  /**
278
- * One meaning of `limit` for both front doors (the CLI flag and the GUI's RPC):
278
+ * One meaning of `limit` for both front doors (the CLI flag and the GUI's call):
279
279
  * 0 = no limit, absent = `fallback`, anything else must be a non-negative
280
280
  * integer. Coercing garbage to a default is how a truncated list gets mistaken
281
281
  * for a complete one — the exact bug this module exists to remove.
@@ -342,7 +342,7 @@ const FILTER_LABELS: Record<string, string> = {
342
342
  }
343
343
 
344
344
  /** `2026-07-29T12:06:29` → `2026-07-29 12:06`. */
345
- export function displayTime(recordedAt: string | null): string | null {
345
+ function displayTime(recordedAt: string | null): string | null {
346
346
  if (!recordedAt) return null
347
347
  return recordedAt.slice(0, 16).replace('T', ' ')
348
348
  }
package/src/runLock.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  // to 'reclaimed'. The heartbeat and release paths branch on these three
5
5
  // states, and collapsing 'unknown' into 'reclaimed' (or 'mine') is exactly
6
6
  // the bug that would either hand a live lock away or delete a reclaimer's lock.
7
- export type LockOwnership = "mine" | "reclaimed" | "unknown";
7
+ type LockOwnership = "mine" | "reclaimed" | "unknown";
8
8
 
9
9
  /**
10
10
  * @param raw lock-file contents, or null if the file could not be read
package/src/tos.ts ADDED
@@ -0,0 +1,18 @@
1
+ export type TosConfig = {
2
+ endpoint: string
3
+ region: string
4
+ bucket: string
5
+ accessKey: string
6
+ secretKey: string
7
+ keep: boolean
8
+ }
9
+
10
+ export function tosObject(config: TosConfig, key: string): Bun.S3File {
11
+ return new Bun.S3Client({
12
+ accessKeyId: config.accessKey,
13
+ secretAccessKey: config.secretKey,
14
+ region: config.region,
15
+ endpoint: `https://${config.bucket}.${config.endpoint}`,
16
+ virtualHostedStyle: true,
17
+ }).file(key)
18
+ }
package/src/envConfig.ts DELETED
@@ -1,129 +0,0 @@
1
- // Pure logic behind cli.ts's env-config provenance. Extracted (no fs, no
2
- // process.env) so its invariants are testable — see envConfig.test.ts:
3
- //
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.
7
- // 2. Hydration: only keys the real environment does NOT set are filled from
8
- // files — an explicit empty string in the environment (e.g.
9
- // VOICENOTE_PI_SUMMARY_TOOLS="") counts as set and is never overridden.
10
- // 3. Scheduler embedding: a value is embedded only when it is NOT recoverable
11
- // from the files at run time — i.e. it is a real-environment value that the
12
- // files either don't provide or provide differently. Hydrated values and
13
- // real-env values equal to the file value are skipped (vn run re-reads the
14
- // files each start, and scheduler env outranks config.json, so embedding a
15
- // recoverable value would freeze it against future config edits).
16
- // 4. A real-env value that DIFFERS from the file value is embedded as a
17
- // deliberate override, but reported (frozenOverrides) so the caller can
18
- // warn: it may equally be a stale shell session shadowing a fresh config
19
- // edit, and it will keep overriding until the scheduler is reinstalled.
20
-
21
- /** Omit `environment` when checking which values files can reproduce on their own. */
22
- export function parseFileEnv(
23
- keys: readonly string[],
24
- configData: Record<string, unknown>,
25
- zshrcContent: string | null,
26
- home: string,
27
- environment: Record<string, string | undefined> = {},
28
- ): Record<string, string> {
29
- const raw = new Map<string, string>()
30
- const literal = new Set<string>()
31
- for (const key of keys) {
32
- const v = configData[key]
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)
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
59
- }
60
- return Object.fromEntries(Array.from(raw.keys(), key => [key, resolve(key)]))
61
- }
62
-
63
- /** Which keys to copy from fileEnv into an environment (invariant 2). */
64
- export function hydrateFromFileEnv(
65
- keys: readonly string[],
66
- processEnv: Record<string, string | undefined>,
67
- fileEnv: Record<string, string>,
68
- ): Record<string, string> {
69
- const out: Record<string, string> = {}
70
- for (const key of keys) {
71
- if (processEnv[key] !== undefined) continue // real env wins, incl. ""
72
- const v = fileEnv[key]
73
- if (v !== undefined) out[key] = v
74
- }
75
- return out
76
- }
77
-
78
- /**
79
- * Per-key no_proxy/NO_PROXY derivation, made pure so its provenance invariant
80
- * is testable (it is the whole reason envKeysToEmbed exists). Given the
81
- * current env value, any previously-captured pre-merge original, whether the
82
- * key is already marked hydrated, and whether a proxy is active, returns:
83
- * - runtime: the value to put in the environment (always volcano-merged)
84
- * - capture: the pre-merge real-env original to remember (or undefined:
85
- * either we synthesized the value, or it was already captured) — this is
86
- * what the scheduler embeds, never the merged value
87
- * - hydrate: true when we synthesized the value from nothing (fully
88
- * rebuildable at run time, so it must NOT be embedded)
89
- * Merge is idempotent, so re-running on an already-merged value (reload) is
90
- * safe; capture-once is preserved by honoring capturedOriginal.
91
- */
92
- export function deriveNoProxy(
93
- current: string | undefined,
94
- capturedOriginal: string | undefined,
95
- alreadyHydrated: boolean,
96
- proxyActive: boolean,
97
- base: string,
98
- volcanoHosts: readonly string[],
99
- ): { runtime: string; capture: string | undefined; hydrate: boolean } {
100
- const merge = (v: string): string => {
101
- const items = v.split(',').map(s => s.trim()).filter(Boolean)
102
- for (const h of volcanoHosts) if (!items.includes(h)) items.push(h)
103
- return items.join(',')
104
- }
105
- if (current === undefined) {
106
- return { runtime: merge(proxyActive ? base : ''), capture: undefined, hydrate: true }
107
- }
108
- const capture = (!alreadyHydrated && capturedOriginal === undefined) ? current : undefined
109
- return { runtime: merge(current), capture, hydrate: false }
110
- }
111
-
112
- /** Which env values the scheduler must snapshot (invariants 3 + 4). */
113
- export function envKeysToEmbed(
114
- keys: readonly string[],
115
- processEnv: Record<string, string | undefined>,
116
- hydratedKeys: ReadonlySet<string>,
117
- fileEnv: Record<string, string>,
118
- ): { embed: Record<string, string>; frozenOverrides: string[] } {
119
- const embed: Record<string, string> = {}
120
- const frozenOverrides: string[] = []
121
- for (const k of keys) {
122
- const v = processEnv[k]
123
- if (v === undefined) continue
124
- if (hydratedKeys.has(k) || fileEnv[k] === v) continue // recoverable at run time
125
- embed[k] = v
126
- if (fileEnv[k] !== undefined) frozenOverrides.push(k)
127
- }
128
- return { embed, frozenOverrides }
129
- }