@hasna/recordings 0.1.10 → 0.1.11

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/mcp/index.js CHANGED
@@ -21,7 +21,7 @@ var __require = import.meta.require;
21
21
  var require_package = __commonJS((exports, module) => {
22
22
  module.exports = {
23
23
  name: "@hasna/recordings",
24
- version: "0.1.10",
24
+ version: "0.1.11",
25
25
  type: "module",
26
26
  description: "Speech-to-text recording tool with MCP and CLI \u2014 records, transcribes, and optionally enhances text using AI",
27
27
  main: "dist/index.js",
@@ -14226,7 +14226,7 @@ var coerce2 = {
14226
14226
  };
14227
14227
  var NEVER2 = INVALID2;
14228
14228
  // src/lib/config.ts
14229
- import { existsSync as existsSync4, readFileSync as readFileSync2, mkdirSync as mkdirSync3, cpSync } from "fs";
14229
+ import { existsSync as existsSync4, readFileSync as readFileSync2, mkdirSync as mkdirSync3, cpSync, readdirSync as readdirSync3, statSync } from "fs";
14230
14230
  import { join as join5 } from "path";
14231
14231
  import { homedir as homedir6 } from "os";
14232
14232
  var DEFAULT_CONFIG = {
@@ -14263,7 +14263,7 @@ function loadConfig(configPath) {
14263
14263
  try {
14264
14264
  const raw = readFileSync2(filePath, "utf-8");
14265
14265
  const fileConfig = JSON.parse(raw);
14266
- Object.assign(config, fileConfig);
14266
+ Object.assign(config, expandEnvBackedConfig(fileConfig));
14267
14267
  } catch {}
14268
14268
  }
14269
14269
  if (process.env.OPENAI_API_KEY) {
@@ -14309,6 +14309,16 @@ function loadConfig(configPath) {
14309
14309
  }
14310
14310
  return config;
14311
14311
  }
14312
+ function expandEnvBackedConfig(config) {
14313
+ const expanded = { ...config };
14314
+ for (const key of ["openai_api_key", "enhancement_api_key"]) {
14315
+ const value = expanded[key];
14316
+ if (typeof value === "string" && value.startsWith("$") && value.length > 1) {
14317
+ expanded[key] = process.env[value.slice(1)] || value;
14318
+ }
14319
+ }
14320
+ return expanded;
14321
+ }
14312
14322
  function findConfigFile() {
14313
14323
  let dir = process.cwd();
14314
14324
  const root = "/";
@@ -14350,20 +14360,44 @@ function loadSecretKey(keyName) {
14350
14360
  const secretsPath = join5(homedir6(), ".secrets");
14351
14361
  if (!existsSync4(secretsPath))
14352
14362
  return "";
14353
- try {
14354
- const content = readFileSync2(secretsPath, "utf-8");
14355
- const match = content.match(new RegExp(`export\\s+${keyName}\\s*=\\s*"([^"]+)"`));
14356
- if (match)
14357
- return match[1];
14358
- const match2 = content.match(new RegExp(`export\\s+${keyName}\\s*=\\s*'([^']+)'`));
14359
- if (match2)
14360
- return match2[1];
14361
- const match3 = content.match(new RegExp(`${keyName}\\s*=\\s*(.+)`));
14362
- if (match3)
14363
- return match3[1].trim().replace(/^["']|["']$/g, "");
14364
- } catch {}
14363
+ for (const candidate of listSecretFiles(secretsPath)) {
14364
+ try {
14365
+ const content = readFileSync2(candidate, "utf-8");
14366
+ const match = content.match(new RegExp(`export\\s+${keyName}\\s*=\\s*"([^"]+)"`));
14367
+ if (match)
14368
+ return match[1];
14369
+ const match2 = content.match(new RegExp(`export\\s+${keyName}\\s*=\\s*'([^']+)'`));
14370
+ if (match2)
14371
+ return match2[1];
14372
+ const match3 = content.match(new RegExp(`${keyName}\\s*=\\s*(.+)`));
14373
+ if (match3)
14374
+ return match3[1].trim().replace(/^["']|["']$/g, "");
14375
+ } catch {}
14376
+ }
14365
14377
  return "";
14366
14378
  }
14379
+ function listSecretFiles(path) {
14380
+ try {
14381
+ const stats = statSync(path);
14382
+ if (stats.isFile())
14383
+ return [path];
14384
+ if (!stats.isDirectory())
14385
+ return [];
14386
+ return readdirSync3(path).sort().flatMap((entry) => {
14387
+ const child = join5(path, entry);
14388
+ try {
14389
+ const childStats = statSync(child);
14390
+ if (childStats.isDirectory())
14391
+ return listSecretFiles(child);
14392
+ if (childStats.isFile() && child.endsWith(".env"))
14393
+ return [child];
14394
+ } catch {}
14395
+ return [];
14396
+ });
14397
+ } catch {
14398
+ return [];
14399
+ }
14400
+ }
14367
14401
  function ensureDataDir(config) {
14368
14402
  const { mkdirSync: mkdirSync4 } = __require("fs");
14369
14403
  mkdirSync4(config.audio_dir, { recursive: true });
@@ -14814,15 +14848,9 @@ function extractInstruction(text, trigger) {
14814
14848
  }
14815
14849
  return text;
14816
14850
  }
14817
- async function enhanceText(rawText, instruction, config) {
14851
+ async function enhanceText(rawText, instruction, config, systemPrompt) {
14818
14852
  const client = getEnhancementClient(config);
14819
- try {
14820
- const response = await client.chat.completions.create({
14821
- model: config.enhancement_model,
14822
- messages: [
14823
- {
14824
- role: "system",
14825
- content: `You are a writing assistant. The user has dictated speech that needs to be transformed into polished output.
14853
+ const basePrompt = `You are a writing assistant. The user has dictated speech that needs to be transformed into polished output.
14826
14854
 
14827
14855
  Rules:
14828
14856
  - Output ONLY the enhanced/rewritten text \u2014 no explanations, no preamble
@@ -14830,7 +14858,18 @@ Rules:
14830
14858
  - Fix grammar, structure, and clarity
14831
14859
  - If the user is giving instructions (e.g., "write an email saying..."), produce the actual output (the email), not a description of it
14832
14860
  - If the user says "say it better" or similar, rewrite their preceding text to be clearer and more professional
14833
- - Match the appropriate tone (formal for business, casual for personal)`
14861
+ - Match the appropriate tone (formal for business, casual for personal)`;
14862
+ const fullPrompt = systemPrompt ? `${basePrompt}
14863
+
14864
+ Additional context:
14865
+ ${systemPrompt}` : basePrompt;
14866
+ try {
14867
+ const response = await client.chat.completions.create({
14868
+ model: config.enhancement_model,
14869
+ messages: [
14870
+ {
14871
+ role: "system",
14872
+ content: fullPrompt
14834
14873
  },
14835
14874
  {
14836
14875
  role: "user",
@@ -14852,7 +14891,7 @@ Rules:
14852
14891
  throw new EnhancementError(`Enhancement failed: ${msg}`);
14853
14892
  }
14854
14893
  }
14855
- async function processText(rawText, config) {
14894
+ async function processText(rawText, config, systemPrompt) {
14856
14895
  if (!config.auto_enhance) {
14857
14896
  return { text: rawText, mode: "raw", enhancement_model: null };
14858
14897
  }
@@ -14860,7 +14899,7 @@ async function processText(rawText, config) {
14860
14899
  if (!detection.needs) {
14861
14900
  return { text: rawText, mode: "raw", enhancement_model: null };
14862
14901
  }
14863
- const result = await enhanceText(rawText, detection.instruction, config);
14902
+ const result = await enhanceText(rawText, detection.instruction, config, systemPrompt);
14864
14903
  return {
14865
14904
  text: result.enhanced,
14866
14905
  mode: "enhanced",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/recordings",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
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
  "main": "dist/index.js",
@@ -544,6 +544,71 @@ describe("processText", () => {
544
544
  });
545
545
  });
546
546
 
547
+ // ── systemPrompt support ──────────────────────────────────────────────────
548
+
549
+ describe("systemPrompt support", () => {
550
+ test("processText passes systemPrompt through to enhanceText", async () => {
551
+ let capturedMessages: Array<{ role: string; content: string }> = [];
552
+ mock.module("openai", () => ({
553
+ default: class MockOpenAI {
554
+ chat = {
555
+ completions: {
556
+ create: mock((opts: { messages: Array<{ role: string; content: string }> }) => {
557
+ capturedMessages = opts.messages;
558
+ return Promise.resolve({
559
+ choices: [{ message: { content: "Enhanced with context" } }],
560
+ });
561
+ }),
562
+ },
563
+ };
564
+ },
565
+ }));
566
+
567
+ resetEnhancementClient();
568
+ const { processText: process } = await import("../lib/enhancer.js");
569
+ resetEnhancementClient();
570
+
571
+ await process("write an email saying thanks", config, "You are working on the Acme project");
572
+ expect(capturedMessages[0]!.content).toContain("Additional context:");
573
+ expect(capturedMessages[0]!.content).toContain("Acme project");
574
+
575
+ resetEnhancementClient();
576
+ });
577
+
578
+ test("processText works without systemPrompt", async () => {
579
+ let capturedMessages: Array<{ role: string; content: string }> = [];
580
+ mock.module("openai", () => ({
581
+ default: class MockOpenAI {
582
+ chat = {
583
+ completions: {
584
+ create: mock((opts: { messages: Array<{ role: string; content: string }> }) => {
585
+ capturedMessages = opts.messages;
586
+ return Promise.resolve({
587
+ choices: [{ message: { content: "Enhanced without context" } }],
588
+ });
589
+ }),
590
+ },
591
+ };
592
+ },
593
+ }));
594
+
595
+ resetEnhancementClient();
596
+ const { processText: process } = await import("../lib/enhancer.js");
597
+ resetEnhancementClient();
598
+
599
+ await process("write an email saying thanks", config);
600
+ expect(capturedMessages[0]!.content).not.toContain("Additional context:");
601
+
602
+ resetEnhancementClient();
603
+ });
604
+
605
+ test("processText ignores systemPrompt for raw dictation", async () => {
606
+ const result = await processText("Just a regular note", config, "project context");
607
+ expect(result.mode).toBe("raw");
608
+ expect(result.text).toBe("Just a regular note");
609
+ });
610
+ });
611
+
547
612
  // ── extractInstruction (tested via needsEnhancement) ────────────────────────
548
613
 
549
614
  describe("extractInstruction behavior via needsEnhancement", () => {
package/src/cli/index.ts CHANGED
@@ -148,6 +148,7 @@ program
148
148
  .description("Transcribe an existing audio file")
149
149
  .option("--no-enhance", "Skip AI enhancement")
150
150
  .option("-t, --tags <tags>", "Comma-separated tags")
151
+ .option("--system-prompt <prompt>", "System prompt for enhancement context")
151
152
  .action(async (file, opts) => {
152
153
  const config = loadConfig();
153
154
  ensureDataDir(config);
@@ -156,7 +157,7 @@ program
156
157
  console.log(chalk.blue("Transcribing..."));
157
158
  const transcription = await transcribeAudio(file, config);
158
159
 
159
- const processed = await processText(transcription.text, config);
160
+ const processed = await processText(transcription.text, config, opts.systemPrompt);
160
161
  const parentOpts = program.opts();
161
162
  const tags = opts.tags ? opts.tags.split(",").map((t: string) => t.trim()) : [];
162
163
 
@@ -472,64 +473,6 @@ program
472
473
  }
473
474
  });
474
475
 
475
- // ── start ───────────────────────────────────────────────────────────────────
476
-
477
- program
478
- .command("start")
479
- .description("Launch the menu bar helper app (F5 to toggle recording)")
480
- .option("--login", "Also add to Login Items so it starts automatically")
481
- .action(async (opts) => {
482
- const { execSync } = require("node:child_process") as typeof import("node:child_process");
483
- const { join: pathJoin } = require("node:path") as typeof import("node:path");
484
- const { homedir: getHome } = require("node:os") as typeof import("node:os");
485
- const { existsSync: fileExists } = require("node:fs") as typeof import("node:fs");
486
- const home = getHome();
487
-
488
- const appPath = pathJoin(home, ".hasna", "recordings", "RecordingsHelper.app");
489
- const oldAppPath = pathJoin(home, ".recordings", "RecordingsHelper.app");
490
-
491
- if (!fileExists(appPath) && !fileExists(oldAppPath)) {
492
- console.error(chalk.red("RecordingsHelper.app not found. Run: recordings shortcut --install"));
493
- process.exit(1);
494
- }
495
-
496
- const resolvedAppPath = fileExists(appPath) ? appPath : oldAppPath;
497
-
498
- // Kill existing instance
499
- try { execSync("pkill -f RecordingsHelper", { stdio: "pipe" }); } catch { /* not running */ }
500
-
501
- // Launch
502
- execSync(`open "${resolvedAppPath}"`, { stdio: "pipe" });
503
- console.log(chalk.green("Recordings helper launched — press F5 to record"));
504
-
505
- if (opts.login) {
506
- try {
507
- execSync(
508
- `osascript -e 'tell application "System Events" to make login item at end with properties {path:"${resolvedAppPath}", hidden:true}'`,
509
- { stdio: "pipe" }
510
- );
511
- console.log(chalk.green("Added to Login Items — will start on boot"));
512
- } catch {
513
- console.log(chalk.yellow("Could not add to Login Items — add manually in System Settings > General > Login Items"));
514
- }
515
- }
516
- });
517
-
518
- // ── stop ────────────────────────────────────────────────────────────────────
519
-
520
- program
521
- .command("stop")
522
- .description("Stop the menu bar helper app")
523
- .action(() => {
524
- const { execSync } = require("node:child_process") as typeof import("node:child_process");
525
- try {
526
- execSync("pkill -f RecordingsHelper", { stdio: "pipe" });
527
- console.log(chalk.green("Recordings helper stopped"));
528
- } catch {
529
- console.log(chalk.dim("Not running"));
530
- }
531
- });
532
-
533
476
  // ── listen ───────────────────────────────────────────────────────────────────
534
477
 
535
478
  program
@@ -680,13 +623,12 @@ program
680
623
  .command("shortcut")
681
624
  .description("Set up a global keyboard shortcut for recording (macOS)")
682
625
  .option("--raycast", "Generate Raycast script command")
683
- .option("--install", "Set up F5 global shortcut via macOS Services (no extra installs)")
684
626
  .option("--karabiner", "Set up Fn key via Karabiner-Elements")
685
627
  .option("--skhd", "Generate skhd hotkey config")
686
628
  .option("--hammerspoon", "Generate Hammerspoon config")
687
629
  .option("--script", "Just output the shell script path")
688
630
  .action((opts) => {
689
- const { writeFileSync, mkdirSync, chmodSync, existsSync: fileExists } = require("node:fs") as typeof import("node:fs");
631
+ const { writeFileSync, mkdirSync, chmodSync } = require("node:fs") as typeof import("node:fs");
690
632
  const { join: pathJoin } = require("node:path") as typeof import("node:path");
691
633
  const { homedir: getHome } = require("node:os") as typeof import("node:os");
692
634
  const home = getHome();
@@ -746,72 +688,6 @@ fi
746
688
  writeFileSync(scriptPath, script, "utf-8");
747
689
  chmodSync(scriptPath, 0o755);
748
690
 
749
- if (opts.install) {
750
- // Install the native menu bar app — no config needed, just works with F5
751
- const { execSync: exec } = require("node:child_process") as typeof import("node:child_process");
752
-
753
- const appPath = pathJoin(home, ".hasna", "recordings", "RecordingsHelper.app");
754
- const srcSwift = pathJoin(__dirname, "..", "native", "RecordingsHelper.swift");
755
- const distApp = pathJoin(__dirname, "..", "RecordingsHelper.app");
756
-
757
- // Copy pre-built app if available, otherwise compile
758
- if (fileExists(pathJoin(distApp, "Contents", "MacOS", "RecordingsHelper"))) {
759
- exec(`rm -rf "${appPath}" && cp -R "${distApp}" "${appPath}"`, { stdio: "pipe", shell: "/bin/bash" });
760
- } else if (fileExists(srcSwift)) {
761
- // Compile from source
762
- console.log(chalk.blue("Compiling native helper app..."));
763
- const appDir = pathJoin(home, ".hasna", "recordings", "RecordingsHelper.app", "Contents", "MacOS");
764
- mkdirSync(appDir, { recursive: true });
765
-
766
- const plistDir = pathJoin(home, ".hasna", "recordings", "RecordingsHelper.app", "Contents");
767
- const plist = `<?xml version="1.0" encoding="UTF-8"?>
768
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
769
- <plist version="1.0"><dict>
770
- <key>CFBundleExecutable</key><string>RecordingsHelper</string>
771
- <key>CFBundleIdentifier</key><string>com.hasna.recordings-helper</string>
772
- <key>CFBundleName</key><string>Recordings</string>
773
- <key>LSUIElement</key><true/>
774
- <key>NSMicrophoneUsageDescription</key><string>Recordings needs microphone access for speech transcription.</string>
775
- </dict></plist>`;
776
- writeFileSync(pathJoin(plistDir, "Info.plist"), plist, "utf-8");
777
-
778
- try {
779
- exec(
780
- `DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer xcrun swiftc -O -o "${appDir}/RecordingsHelper" "${srcSwift}" -framework Cocoa -framework Carbon`,
781
- { stdio: "pipe" }
782
- );
783
- } catch {
784
- // Fallback to default toolchain
785
- exec(
786
- `swiftc -O -o "${appDir}/RecordingsHelper" "${srcSwift}" -framework Cocoa -framework Carbon`,
787
- { stdio: "pipe" }
788
- );
789
- }
790
- } else {
791
- console.error(chalk.red("Cannot find RecordingsHelper. Run from the project directory or rebuild."));
792
- process.exit(1);
793
- }
794
-
795
- // Kill existing instance and launch
796
- try { exec("pkill -f RecordingsHelper", { stdio: "pipe" }); } catch { /* not running */ }
797
- exec(`open "${appPath}"`, { stdio: "pipe" });
798
-
799
- // Add to Login Items
800
- try {
801
- exec(
802
- `osascript -e 'tell application "System Events" to make login item at end with properties {path:"${appPath}", hidden:true}'`,
803
- { stdio: "pipe" }
804
- );
805
- } catch { /* already exists or no permission */ }
806
-
807
- console.log(chalk.green("\nRecordings helper installed and running!\n"));
808
- console.log(` ${chalk.yellow("F5")} Start/stop recording`);
809
- console.log(` ${chalk.dim("🎙")} Menu bar icon (click for options)`);
810
- console.log(` ${chalk.dim("Auto")} Starts on login\n`);
811
- console.log(chalk.dim(" Press F5 → speak → F5 → text is pasted where your cursor is."));
812
- return;
813
- }
814
-
815
691
  if (opts.karabiner) {
816
692
  const karabinerDir = pathJoin(home, ".config", "karabiner", "assets", "complex_modifications");
817
693
  mkdirSync(karabinerDir, { recursive: true });
@@ -900,9 +776,6 @@ ${scriptPath}
900
776
  console.log(chalk.cyan(` ${scriptPath}\n`));
901
777
  console.log("Bind it to a hotkey using any of these:\n");
902
778
 
903
- console.log(chalk.bold(" macOS built-in") + chalk.dim(" (no extra installs — recommended)"));
904
- console.log(` recordings shortcut --install\n`);
905
-
906
779
  console.log(chalk.bold(" Karabiner-Elements") + chalk.dim(" (for Fn key specifically)"));
907
780
  console.log(` brew install --cask karabiner-elements`);
908
781
  console.log(` recordings shortcut --karabiner\n`);
package/src/lib/config.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { existsSync, readFileSync, mkdirSync, cpSync } from "fs";
1
+ import { existsSync, readFileSync, mkdirSync, cpSync, readdirSync, statSync } from "fs";
2
2
  import { join } from "path";
3
3
  import { homedir } from "os";
4
4
  import type { RecordingsConfig } from "../types/index.js";
@@ -42,7 +42,7 @@ export function loadConfig(configPath?: string): RecordingsConfig {
42
42
  try {
43
43
  const raw = readFileSync(filePath, "utf-8");
44
44
  const fileConfig = JSON.parse(raw) as Partial<RecordingsConfig>;
45
- Object.assign(config, fileConfig);
45
+ Object.assign(config, expandEnvBackedConfig(fileConfig));
46
46
  } catch {
47
47
  // Ignore invalid config files
48
48
  }
@@ -102,6 +102,19 @@ export function loadConfig(configPath?: string): RecordingsConfig {
102
102
  return config;
103
103
  }
104
104
 
105
+ function expandEnvBackedConfig(config: Partial<RecordingsConfig>): Partial<RecordingsConfig> {
106
+ const expanded = { ...config };
107
+
108
+ for (const key of ["openai_api_key", "enhancement_api_key"] as const) {
109
+ const value = expanded[key];
110
+ if (typeof value === "string" && value.startsWith("$") && value.length > 1) {
111
+ expanded[key] = process.env[value.slice(1)] || value;
112
+ }
113
+ }
114
+
115
+ return expanded;
116
+ }
117
+
105
118
  function findConfigFile(): string | null {
106
119
  // Walk up from cwd looking for .recordings/config.json
107
120
  let dir = process.cwd();
@@ -150,24 +163,51 @@ function loadSecretKey(keyName: string): string {
150
163
  const secretsPath = join(homedir(), ".secrets");
151
164
  if (!existsSync(secretsPath)) return "";
152
165
 
153
- try {
154
- const content = readFileSync(secretsPath, "utf-8");
155
- const match = content.match(
156
- new RegExp(`export\\s+${keyName}\\s*=\\s*"([^"]+)"`)
157
- );
158
- if (match) return match[1]!;
166
+ for (const candidate of listSecretFiles(secretsPath)) {
167
+ try {
168
+ const content = readFileSync(candidate, "utf-8");
169
+ const match = content.match(
170
+ new RegExp(`export\\s+${keyName}\\s*=\\s*"([^"]+)"`)
171
+ );
172
+ if (match) return match[1]!;
173
+
174
+ const match2 = content.match(
175
+ new RegExp(`export\\s+${keyName}\\s*=\\s*'([^']+)'`)
176
+ );
177
+ if (match2) return match2[1]!;
178
+
179
+ const match3 = content.match(new RegExp(`${keyName}\\s*=\\s*(.+)`));
180
+ if (match3) return match3[1]!.trim().replace(/^["']|["']$/g, "");
181
+ } catch {
182
+ // Ignore unreadable secret files
183
+ }
184
+ }
159
185
 
160
- const match2 = content.match(
161
- new RegExp(`export\\s+${keyName}\\s*=\\s*'([^']+)'`)
162
- );
163
- if (match2) return match2[1]!;
186
+ return "";
187
+ }
164
188
 
165
- const match3 = content.match(new RegExp(`${keyName}\\s*=\\s*(.+)`));
166
- if (match3) return match3[1]!.trim().replace(/^["']|["']$/g, "");
189
+ function listSecretFiles(path: string): string[] {
190
+ try {
191
+ const stats = statSync(path);
192
+ if (stats.isFile()) return [path];
193
+ if (!stats.isDirectory()) return [];
194
+
195
+ return readdirSync(path)
196
+ .sort()
197
+ .flatMap((entry) => {
198
+ const child = join(path, entry);
199
+ try {
200
+ const childStats = statSync(child);
201
+ if (childStats.isDirectory()) return listSecretFiles(child);
202
+ if (childStats.isFile() && child.endsWith(".env")) return [child];
203
+ } catch {
204
+ // Ignore entries that disappear or are unreadable
205
+ }
206
+ return [];
207
+ });
167
208
  } catch {
168
- // Ignore
209
+ return [];
169
210
  }
170
- return "";
171
211
  }
172
212
 
173
213
  export function ensureDataDir(config: RecordingsConfig): void {
@@ -92,17 +92,12 @@ function extractInstruction(text: string, trigger: string): string {
92
92
  export async function enhanceText(
93
93
  rawText: string,
94
94
  instruction: string,
95
- config: RecordingsConfig
95
+ config: RecordingsConfig,
96
+ systemPrompt?: string
96
97
  ): Promise<EnhancementResult> {
97
98
  const client = getEnhancementClient(config);
98
99
 
99
- try {
100
- const response = await client.chat.completions.create({
101
- model: config.enhancement_model,
102
- messages: [
103
- {
104
- role: "system",
105
- content: `You are a writing assistant. The user has dictated speech that needs to be transformed into polished output.
100
+ const basePrompt = `You are a writing assistant. The user has dictated speech that needs to be transformed into polished output.
106
101
 
107
102
  Rules:
108
103
  - Output ONLY the enhanced/rewritten text — no explanations, no preamble
@@ -110,7 +105,17 @@ Rules:
110
105
  - Fix grammar, structure, and clarity
111
106
  - If the user is giving instructions (e.g., "write an email saying..."), produce the actual output (the email), not a description of it
112
107
  - If the user says "say it better" or similar, rewrite their preceding text to be clearer and more professional
113
- - Match the appropriate tone (formal for business, casual for personal)`,
108
+ - Match the appropriate tone (formal for business, casual for personal)`;
109
+
110
+ const fullPrompt = systemPrompt ? `${basePrompt}\n\nAdditional context:\n${systemPrompt}` : basePrompt;
111
+
112
+ try {
113
+ const response = await client.chat.completions.create({
114
+ model: config.enhancement_model,
115
+ messages: [
116
+ {
117
+ role: "system",
118
+ content: fullPrompt,
114
119
  },
115
120
  {
116
121
  role: "user",
@@ -141,7 +146,8 @@ Rules:
141
146
  */
142
147
  export async function processText(
143
148
  rawText: string,
144
- config: RecordingsConfig
149
+ config: RecordingsConfig,
150
+ systemPrompt?: string
145
151
  ): Promise<{
146
152
  text: string;
147
153
  mode: "raw" | "enhanced";
@@ -157,7 +163,7 @@ export async function processText(
157
163
  return { text: rawText, mode: "raw", enhancement_model: null };
158
164
  }
159
165
 
160
- const result = await enhanceText(rawText, detection.instruction, config);
166
+ const result = await enhanceText(rawText, detection.instruction, config, systemPrompt);
161
167
 
162
168
  return {
163
169
  text: result.enhanced,