@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/.takumi/settings.local.json +7 -0
- package/dist/cli/index.js +73 -122
- package/dist/index.js +64 -25
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/enhancer.d.ts +2 -2
- package/dist/lib/enhancer.d.ts.map +1 -1
- package/dist/mcp/index.js +65 -26
- package/package.json +1 -1
- package/src/__tests__/enhancer.test.ts +65 -0
- package/src/cli/index.ts +3 -130
- package/src/lib/config.ts +56 -16
- package/src/lib/enhancer.ts +17 -11
- package/src/native/Recordings/Recordings/FnKeyMonitor.swift +58 -49
- package/src/native/Recordings/Recordings/MenuBarPopover.swift +247 -118
- package/src/native/Recordings/Recordings/ProjectStore.swift +126 -0
- package/src/native/Recordings/Recordings/RecordingEngine.swift +157 -57
- package/src/native/Recordings/Recordings/RecordingsApp.swift +12 -4
- package/src/native/Recordings/Recordings/SettingsView.swift +147 -36
- package/src/native/RecordingsHelper.swift +0 -395
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.
|
|
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
|
-
|
|
14354
|
-
|
|
14355
|
-
|
|
14356
|
-
|
|
14357
|
-
|
|
14358
|
-
|
|
14359
|
-
|
|
14360
|
-
|
|
14361
|
-
|
|
14362
|
-
|
|
14363
|
-
|
|
14364
|
-
|
|
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
|
-
|
|
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
|
@@ -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
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
);
|
|
163
|
-
if (match2) return match2[1]!;
|
|
186
|
+
return "";
|
|
187
|
+
}
|
|
164
188
|
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
209
|
+
return [];
|
|
169
210
|
}
|
|
170
|
-
return "";
|
|
171
211
|
}
|
|
172
212
|
|
|
173
213
|
export function ensureDataDir(config: RecordingsConfig): void {
|
package/src/lib/enhancer.ts
CHANGED
|
@@ -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
|
-
|
|
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,
|