@hasna/recordings 0.1.25 → 0.1.26

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/dist/cli/index.js CHANGED
@@ -5098,7 +5098,7 @@ var init_pg_migrate = __esm(() => {
5098
5098
  var require_package = __commonJS((exports, module) => {
5099
5099
  module.exports = {
5100
5100
  name: "@hasna/recordings",
5101
- version: "0.1.25",
5101
+ version: "0.1.26",
5102
5102
  type: "module",
5103
5103
  description: "Speech-to-text recording tool with MCP and CLI \u2014 records, transcribes, and optionally enhances text using AI",
5104
5104
  repository: {
@@ -6081,7 +6081,7 @@ async function processText(rawText, config, systemPrompt) {
6081
6081
  }
6082
6082
 
6083
6083
  // src/version.ts
6084
- var VERSION = "0.1.25";
6084
+ var VERSION = "0.1.26";
6085
6085
 
6086
6086
  // src/cli/storage.ts
6087
6087
  import chalk from "chalk";
@@ -6442,6 +6442,14 @@ function registerStorageCommands(program) {
6442
6442
  });
6443
6443
  }
6444
6444
 
6445
+ // src/cli/options.ts
6446
+ function applyEnhancementOptions(config, opts) {
6447
+ if (opts.enhance === false || opts.noEnhance === false) {
6448
+ config.auto_enhance = false;
6449
+ }
6450
+ return config;
6451
+ }
6452
+
6445
6453
  // src/cli/index.ts
6446
6454
  var program = new Command;
6447
6455
  program.name("recordings").description("Speech-to-text recording tool \u2014 record, transcribe, and enhance with AI").version(VERSION).option("--json", "Output as JSON").option("--agent <name>", "Agent name or ID").option("--project <name>", "Project name or ID").option("--session <id>", "Session ID");
@@ -6451,8 +6459,7 @@ program.command("record").description("Record from microphone, transcribe, and o
6451
6459
  ensureDataDir(config);
6452
6460
  if (opts.language)
6453
6461
  config.language = opts.language;
6454
- if (opts.noEnhance === false)
6455
- config.auto_enhance = false;
6462
+ applyEnhancementOptions(config, opts);
6456
6463
  const deps = await checkRecordingDeps();
6457
6464
  if (!deps.available) {
6458
6465
  console.error(chalk2.red(`Error: ${deps.message}`));
@@ -6518,8 +6525,7 @@ Saved as ${recording.id.slice(0, 8)}`));
6518
6525
  program.command("transcribe <file>").description("Transcribe an existing audio file").option("--no-enhance", "Skip AI enhancement").option("--stream", "Stream transcription deltas while the file is processed").option("-t, --tags <tags>", "Comma-separated tags").option("--prompt <prompt>", "Vocabulary/context prompt for transcription").option("--system-prompt <prompt>", "System prompt for enhancement context").action(async (file, opts) => {
6519
6526
  const config = loadConfig();
6520
6527
  ensureDataDir(config);
6521
- if (opts.noEnhance === false)
6522
- config.auto_enhance = false;
6528
+ applyEnhancementOptions(config, opts);
6523
6529
  const parentOpts = program.opts();
6524
6530
  if (!parentOpts.json) {
6525
6531
  console.log(chalk2.blue("Transcribing..."));
@@ -6883,8 +6889,7 @@ program.command("listen").description("Push-to-talk mode \u2014 press Space to s
6883
6889
  ensureDataDir(config);
6884
6890
  if (opts.language)
6885
6891
  config.language = opts.language;
6886
- if (opts.noEnhance === false)
6887
- config.auto_enhance = false;
6892
+ applyEnhancementOptions(config, opts);
6888
6893
  const deps = await checkRecordingDeps();
6889
6894
  if (!deps.available) {
6890
6895
  console.error(chalk2.red(`Error: ${deps.message}`));
@@ -0,0 +1,7 @@
1
+ import type { RecordingsConfig } from "../types/index.js";
2
+ export type EnhancementOptionBag = {
3
+ enhance?: boolean;
4
+ noEnhance?: boolean;
5
+ };
6
+ export declare function applyEnhancementOptions(config: RecordingsConfig, opts: EnhancementOptionBag): RecordingsConfig;
7
+ //# sourceMappingURL=options.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../../src/cli/options.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAE1D,MAAM,MAAM,oBAAoB,GAAG;IACjC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB,CAAC;AAEF,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,gBAAgB,EACxB,IAAI,EAAE,oBAAoB,GACzB,gBAAgB,CAKlB"}
package/dist/mcp/index.js CHANGED
@@ -4920,7 +4920,7 @@ var require_lib2 = __commonJS((exports, module) => {
4920
4920
  var require_package = __commonJS((exports, module) => {
4921
4921
  module.exports = {
4922
4922
  name: "@hasna/recordings",
4923
- version: "0.1.25",
4923
+ version: "0.1.26",
4924
4924
  type: "module",
4925
4925
  description: "Speech-to-text recording tool with MCP and CLI \u2014 records, transcribes, and optionally enhances text using AI",
4926
4926
  repository: {
@@ -9797,7 +9797,7 @@ async function processText(rawText, config, systemPrompt) {
9797
9797
  }
9798
9798
 
9799
9799
  // src/version.ts
9800
- var VERSION = "0.1.25";
9800
+ var VERSION = "0.1.26";
9801
9801
 
9802
9802
  // src/db/storage-config.ts
9803
9803
  import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export declare const VERSION = "0.1.25";
1
+ export declare const VERSION = "0.1.26";
2
2
  //# sourceMappingURL=version.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/recordings",
3
- "version": "0.1.25",
3
+ "version": "0.1.26",
4
4
  "type": "module",
5
5
  "description": "Speech-to-text recording tool with MCP and CLI — records, transcribes, and optionally enhances text using AI",
6
6
  "repository": {
@@ -44,7 +44,7 @@ public struct TranscriptionResult: Sendable {
44
44
  var displayText: String { processedText ?? rawText }
45
45
  }
46
46
 
47
- public enum RecordingTrigger {
47
+ public enum RecordingTrigger: Equatable, Sendable {
48
48
  case manual
49
49
  case fnKey
50
50
  case keyboardShortcut
@@ -121,6 +121,7 @@ public final class RecordingEngine: ObservableObject {
121
121
  private var recordingTimer: Timer?
122
122
  private var activeTrigger: RecordingTrigger?
123
123
  private var keyboardShortcutIsDown = false
124
+ private var fnKeyIsDown = false
124
125
  private var targetAppBundleIdentifier: String?
125
126
  private var targetAppPid: pid_t?
126
127
  public var projectStore: ProjectStore?
@@ -165,13 +166,23 @@ public final class RecordingEngine: ObservableObject {
165
166
  // Set up fn key monitor — hold fn to record, release to stop (like WisprFlow)
166
167
  fnMonitor.onFnKeyDown = { [weak self] in
167
168
  Task { @MainActor [weak self] in
168
- guard let self, self.useFnKey, !self.isRecording else { return }
169
+ guard let self else { return }
170
+ self.fnKeyIsDown = true
171
+ guard self.useFnKey, Self.canBeginRecording(isRecording: self.isRecording, isTranscribing: self.isTranscribing) else { return }
169
172
  self.startRecording(trigger: .fnKey)
170
173
  }
171
174
  }
172
175
  fnMonitor.onFnKeyUp = { [weak self] in
173
176
  Task { @MainActor [weak self] in
174
- guard let self, self.useFnKey, self.isRecording, self.activeTrigger == .fnKey else { return }
177
+ guard let self else { return }
178
+ self.fnKeyIsDown = false
179
+ guard self.useFnKey, self.activeTrigger == .fnKey else { return }
180
+ guard self.isRecording else {
181
+ self.log("fn released before recording started; cancelling pending start")
182
+ self.resetRecordingIntent()
183
+ self.updateStatus()
184
+ return
185
+ }
175
186
  self.stopAndTranscribe()
176
187
  }
177
188
  }
@@ -181,7 +192,7 @@ public final class RecordingEngine: ObservableObject {
181
192
  Task { @MainActor [weak self] in
182
193
  guard let self, !self.keyboardShortcutIsDown else { return }
183
194
  self.keyboardShortcutIsDown = true
184
- guard !self.isRecording else { return }
195
+ guard Self.canBeginRecording(isRecording: self.isRecording, isTranscribing: self.isTranscribing) else { return }
185
196
  self.startRecording(trigger: .keyboardShortcut)
186
197
  }
187
198
  }
@@ -189,7 +200,13 @@ public final class RecordingEngine: ObservableObject {
189
200
  Task { @MainActor [weak self] in
190
201
  guard let self, self.keyboardShortcutIsDown else { return }
191
202
  self.keyboardShortcutIsDown = false
192
- guard self.isRecording, self.activeTrigger == .keyboardShortcut else { return }
203
+ guard self.activeTrigger == .keyboardShortcut else { return }
204
+ guard self.isRecording else {
205
+ self.log("shortcut released before recording started; cancelling pending start")
206
+ self.resetRecordingIntent()
207
+ self.updateStatus()
208
+ return
209
+ }
193
210
  self.stopAndTranscribe()
194
211
  }
195
212
  }
@@ -289,7 +306,12 @@ public final class RecordingEngine: ObservableObject {
289
306
  // MARK: - Start Recording (Streaming)
290
307
 
291
308
  public func startRecording(trigger: RecordingTrigger = .manual) {
292
- guard !isRecording else { return }
309
+ guard Self.canBeginRecording(isRecording: isRecording, isTranscribing: isTranscribing) else {
310
+ if isTranscribing {
311
+ statusMessage = "Finish transcribing before recording again"
312
+ }
313
+ return
314
+ }
293
315
  log("startRecording trigger=\(trigger) microphoneStatus=\(AVCaptureDevice.authorizationStatus(for: .audio).rawValue) accessibility=\(AXIsProcessTrusted())")
294
316
  activeTrigger = trigger
295
317
  keyboardShortcutIsDown = trigger == .keyboardShortcut
@@ -320,6 +342,12 @@ public final class RecordingEngine: ObservableObject {
320
342
  guard let self else { return }
321
343
  self.log("microphone access response granted=\(granted)")
322
344
  if granted {
345
+ guard self.shouldContinueStarting(trigger: trigger) else {
346
+ self.log("recording start cancelled before microphone permission completed trigger=\(trigger)")
347
+ self.resetRecordingIntent()
348
+ self.updateStatus()
349
+ return
350
+ }
323
351
  self.startNativeRecording()
324
352
  } else {
325
353
  self.resetRecordingIntent()
@@ -337,6 +365,33 @@ public final class RecordingEngine: ObservableObject {
337
365
  }
338
366
  }
339
367
 
368
+ nonisolated static func canBeginRecording(isRecording: Bool, isTranscribing: Bool) -> Bool {
369
+ !isRecording && !isTranscribing
370
+ }
371
+
372
+ nonisolated static func shouldContinueStartingAfterPermission(
373
+ trigger: RecordingTrigger,
374
+ keyboardShortcutIsDown: Bool,
375
+ fnKeyIsDown: Bool
376
+ ) -> Bool {
377
+ switch trigger {
378
+ case .manual:
379
+ return true
380
+ case .keyboardShortcut:
381
+ return keyboardShortcutIsDown
382
+ case .fnKey:
383
+ return fnKeyIsDown
384
+ }
385
+ }
386
+
387
+ private func shouldContinueStarting(trigger: RecordingTrigger) -> Bool {
388
+ activeTrigger == trigger && Self.shouldContinueStartingAfterPermission(
389
+ trigger: trigger,
390
+ keyboardShortcutIsDown: keyboardShortcutIsDown,
391
+ fnKeyIsDown: fnKeyIsDown
392
+ )
393
+ }
394
+
340
395
  private func startNativeRecording() {
341
396
  let streamState = PCMStreamState()
342
397
  pcmStreamState = streamState
@@ -667,6 +722,7 @@ public final class RecordingEngine: ObservableObject {
667
722
  private func resetRecordingIntent() {
668
723
  activeTrigger = nil
669
724
  keyboardShortcutIsDown = false
725
+ fnKeyIsDown = false
670
726
  targetAppBundleIdentifier = nil
671
727
  targetAppPid = nil
672
728
  }
@@ -0,0 +1,48 @@
1
+ import Testing
2
+ @testable import RecordingsLib
3
+
4
+ struct RecordingStartGateTests {
5
+ @Test("recording cannot begin while already recording or transcribing")
6
+ func cannotBeginWhenBusy() {
7
+ #expect(RecordingEngine.canBeginRecording(isRecording: false, isTranscribing: false) == true)
8
+ #expect(RecordingEngine.canBeginRecording(isRecording: true, isTranscribing: false) == false)
9
+ #expect(RecordingEngine.canBeginRecording(isRecording: false, isTranscribing: true) == false)
10
+ }
11
+
12
+ @Test("manual recording can continue after permission grant")
13
+ func manualPermissionContinuation() {
14
+ #expect(RecordingEngine.shouldContinueStartingAfterPermission(
15
+ trigger: .manual,
16
+ keyboardShortcutIsDown: false,
17
+ fnKeyIsDown: false
18
+ ))
19
+ }
20
+
21
+ @Test("keyboard shortcut recording is cancelled when released before permission grant")
22
+ func keyboardPermissionContinuationRequiresHeldKey() {
23
+ #expect(RecordingEngine.shouldContinueStartingAfterPermission(
24
+ trigger: .keyboardShortcut,
25
+ keyboardShortcutIsDown: true,
26
+ fnKeyIsDown: false
27
+ ))
28
+ #expect(RecordingEngine.shouldContinueStartingAfterPermission(
29
+ trigger: .keyboardShortcut,
30
+ keyboardShortcutIsDown: false,
31
+ fnKeyIsDown: false
32
+ ) == false)
33
+ }
34
+
35
+ @Test("fn recording is cancelled when released before permission grant")
36
+ func fnPermissionContinuationRequiresHeldKey() {
37
+ #expect(RecordingEngine.shouldContinueStartingAfterPermission(
38
+ trigger: .fnKey,
39
+ keyboardShortcutIsDown: false,
40
+ fnKeyIsDown: true
41
+ ))
42
+ #expect(RecordingEngine.shouldContinueStartingAfterPermission(
43
+ trigger: .fnKey,
44
+ keyboardShortcutIsDown: false,
45
+ fnKeyIsDown: false
46
+ ) == false)
47
+ }
48
+ }