@hasna/recordings 0.1.18 → 0.1.20
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 +4 -155
- package/dist/db/database.d.ts.map +1 -1
- package/dist/index.js +1 -26
- package/dist/mcp/index.js +3 -32
- package/dist/version.d.ts +1 -1
- package/package.json +1 -5
- package/scripts/install_macos_app.sh +0 -32
- package/src/native/Recordings/RecordingsLib/MenuBarPopover.swift +3 -2
- package/src/native/Recordings/RecordingsLib/RealtimeTranscriptionClient.swift +12 -76
- package/src/native/Recordings/RecordingsLib/SettingsView.swift +1 -27
- package/src/native/Recordings/RecordingsTests/RealtimeTranscriptionTests.swift +0 -13
- package/src/native/Recordings/RecordingsTests/NativeAppDiagnosticsTests.swift +0 -23
package/dist/cli/index.js
CHANGED
|
@@ -7,13 +7,9 @@ var __require = import.meta.require;
|
|
|
7
7
|
var require_package = __commonJS((exports, module) => {
|
|
8
8
|
module.exports = {
|
|
9
9
|
name: "@hasna/recordings",
|
|
10
|
-
version: "0.1.
|
|
10
|
+
version: "0.1.11",
|
|
11
11
|
type: "module",
|
|
12
12
|
description: "Speech-to-text recording tool with MCP and CLI \u2014 records, transcribes, and optionally enhances text using AI",
|
|
13
|
-
repository: {
|
|
14
|
-
type: "git",
|
|
15
|
-
url: "git+https://github.com/hasna/recordings.git"
|
|
16
|
-
},
|
|
17
13
|
main: "dist/index.js",
|
|
18
14
|
types: "dist/index.d.ts",
|
|
19
15
|
bin: {
|
|
@@ -9893,40 +9889,15 @@ function runMigrations(db) {
|
|
|
9893
9889
|
const result = db.query("SELECT MAX(id) as max_id FROM _migrations").get();
|
|
9894
9890
|
const currentLevel = result?.max_id ?? -1;
|
|
9895
9891
|
for (let i = currentLevel + 1;i < MIGRATIONS.length; i++) {
|
|
9892
|
+
db.run(MIGRATIONS[i]);
|
|
9896
9893
|
try {
|
|
9897
|
-
db.run(MIGRATIONS[i]);
|
|
9898
9894
|
db.query("INSERT INTO _migrations (id) VALUES (?)").run(i);
|
|
9899
9895
|
} catch (e) {
|
|
9900
|
-
if (isBenignMigrationError(e)) {
|
|
9901
|
-
db.query("INSERT OR IGNORE INTO _migrations (id) VALUES (?)").run(i);
|
|
9902
|
-
continue;
|
|
9903
|
-
}
|
|
9904
9896
|
if (!(e instanceof Error && e.message.includes("UNIQUE constraint failed"))) {
|
|
9905
9897
|
throw e;
|
|
9906
9898
|
}
|
|
9907
9899
|
}
|
|
9908
9900
|
}
|
|
9909
|
-
repairSchemaDrift(db);
|
|
9910
|
-
}
|
|
9911
|
-
function isBenignMigrationError(error) {
|
|
9912
|
-
if (!(error instanceof Error))
|
|
9913
|
-
return false;
|
|
9914
|
-
return error.message.includes("duplicate column name");
|
|
9915
|
-
}
|
|
9916
|
-
function repairSchemaDrift(db) {
|
|
9917
|
-
ensureColumn(db, "recordings", "goal", "TEXT");
|
|
9918
|
-
ensureColumn(db, "recordings", "role", "TEXT");
|
|
9919
|
-
ensureColumn(db, "recordings", "task_list_id", "TEXT");
|
|
9920
|
-
ensureColumn(db, "recordings", "machine_id", "TEXT");
|
|
9921
|
-
ensureColumn(db, "recordings", "metadata", "TEXT DEFAULT '{}'");
|
|
9922
|
-
ensureColumn(db, "agents", "active_project_id", "TEXT REFERENCES projects(id) ON DELETE SET NULL");
|
|
9923
|
-
}
|
|
9924
|
-
function ensureColumn(db, table, column, definition) {
|
|
9925
|
-
const rows = db.query(`PRAGMA table_info(${table})`).all();
|
|
9926
|
-
if (rows.some((row) => row.name === column)) {
|
|
9927
|
-
return;
|
|
9928
|
-
}
|
|
9929
|
-
db.run(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
|
|
9930
9901
|
}
|
|
9931
9902
|
function getAdapter() {
|
|
9932
9903
|
if (!_adapter) {
|
|
@@ -10466,7 +10437,7 @@ async function processText(rawText, config, systemPrompt) {
|
|
|
10466
10437
|
}
|
|
10467
10438
|
|
|
10468
10439
|
// src/version.ts
|
|
10469
|
-
var VERSION = "0.1.
|
|
10440
|
+
var VERSION = "0.1.11";
|
|
10470
10441
|
|
|
10471
10442
|
// src/cli/index.ts
|
|
10472
10443
|
var program = new Command;
|
|
@@ -10792,65 +10763,6 @@ appCommand.command("status").description("Show installed Recordings.app status")
|
|
|
10792
10763
|
console.log(`Native sources: ${status.native_sources_available ? "available" : "missing"}`);
|
|
10793
10764
|
console.log(`Installed app: ${status.installed ? status.installed_app_path : "missing"}`);
|
|
10794
10765
|
console.log(`Executable: ${status.executable ? "available" : "missing"}`);
|
|
10795
|
-
console.log(`Code hash: ${status.app_code_hash ?? "unavailable"}`);
|
|
10796
|
-
if (process.platform === "darwin") {
|
|
10797
|
-
console.log(`Microphone: ${status.microphone_permission}`);
|
|
10798
|
-
console.log(`Accessibility: ${status.accessibility_permission}`);
|
|
10799
|
-
console.log(`Log: ${status.log_path}`);
|
|
10800
|
-
}
|
|
10801
|
-
});
|
|
10802
|
-
appCommand.command("permissions").description("Show macOS permission state for Recordings.app").action(() => {
|
|
10803
|
-
const status = getMacOSAppStatus();
|
|
10804
|
-
const permissions = {
|
|
10805
|
-
platform: status.platform,
|
|
10806
|
-
bundle_id: "com.hasna.recordings",
|
|
10807
|
-
microphone: status.microphone_permission,
|
|
10808
|
-
accessibility: status.accessibility_permission,
|
|
10809
|
-
app_code_hash: status.app_code_hash,
|
|
10810
|
-
ad_hoc_signed: status.ad_hoc_signed,
|
|
10811
|
-
log_path: status.log_path
|
|
10812
|
-
};
|
|
10813
|
-
if (program.opts().json) {
|
|
10814
|
-
console.log(JSON.stringify(permissions, null, 2));
|
|
10815
|
-
return;
|
|
10816
|
-
}
|
|
10817
|
-
console.log(`Microphone: ${permissions.microphone}`);
|
|
10818
|
-
console.log(`Accessibility: ${permissions.accessibility}`);
|
|
10819
|
-
console.log(`Log: ${permissions.log_path}`);
|
|
10820
|
-
});
|
|
10821
|
-
appCommand.command("reset-permissions").description("Reset macOS Microphone and Accessibility permissions for Recordings.app").action(() => {
|
|
10822
|
-
if (process.platform !== "darwin") {
|
|
10823
|
-
console.error(chalk.red("Permission reset is only available on macOS"));
|
|
10824
|
-
process.exit(1);
|
|
10825
|
-
}
|
|
10826
|
-
const services = ["Microphone", "Accessibility"];
|
|
10827
|
-
for (const service of services) {
|
|
10828
|
-
const result = spawnSync("tccutil", ["reset", service, "com.hasna.recordings"], {
|
|
10829
|
-
stdio: "inherit"
|
|
10830
|
-
});
|
|
10831
|
-
if (result.error) {
|
|
10832
|
-
console.error(chalk.red(result.error.message));
|
|
10833
|
-
process.exit(1);
|
|
10834
|
-
}
|
|
10835
|
-
}
|
|
10836
|
-
});
|
|
10837
|
-
appCommand.command("log").description("Show the Recordings.app diagnostic log").option("-n, --lines <lines>", "Number of lines to print", "120").action((opts) => {
|
|
10838
|
-
const status = getMacOSAppStatus();
|
|
10839
|
-
if (!existsSync6(status.log_path)) {
|
|
10840
|
-
console.log("");
|
|
10841
|
-
return;
|
|
10842
|
-
}
|
|
10843
|
-
const lines = Math.max(1, parseInt(opts.lines, 10) || 120);
|
|
10844
|
-
const result = spawnSync("tail", ["-n", String(lines), status.log_path], {
|
|
10845
|
-
encoding: "utf8"
|
|
10846
|
-
});
|
|
10847
|
-
if (result.error) {
|
|
10848
|
-
console.error(chalk.red(result.error.message));
|
|
10849
|
-
process.exit(1);
|
|
10850
|
-
}
|
|
10851
|
-
process.stdout.write(result.stdout);
|
|
10852
|
-
if (result.stderr)
|
|
10853
|
-
process.stderr.write(result.stderr);
|
|
10854
10766
|
});
|
|
10855
10767
|
appCommand.command("open").description("Open the installed Recordings.app").action(() => {
|
|
10856
10768
|
const status = getMacOSAppStatus();
|
|
@@ -11316,11 +11228,8 @@ function getMacOSAppStatus() {
|
|
|
11316
11228
|
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
11317
11229
|
const installedAppPath = pathJoin(home, ".hasna", "recordings", "Recordings.app");
|
|
11318
11230
|
const executablePath = pathJoin(installedAppPath, "Contents", "MacOS", "Recordings");
|
|
11319
|
-
const logPath = pathJoin(home, ".hasna", "recordings", "Recordings.log");
|
|
11320
11231
|
const installerPath = pathJoin(packageRoot, "scripts", "install_macos_app.sh");
|
|
11321
11232
|
const nativeSourcesPath = pathJoin(packageRoot, "src", "native", "Recordings");
|
|
11322
|
-
const signingInfo = getCodeSigningInfo(installedAppPath);
|
|
11323
|
-
const permissionCodeHash = signingInfo.adHoc ? signingInfo.cdHash : null;
|
|
11324
11233
|
return {
|
|
11325
11234
|
platform: process.platform,
|
|
11326
11235
|
package_root: packageRoot,
|
|
@@ -11331,69 +11240,9 @@ function getMacOSAppStatus() {
|
|
|
11331
11240
|
installed_app_path: installedAppPath,
|
|
11332
11241
|
installed: existsSync6(installedAppPath),
|
|
11333
11242
|
executable_path: executablePath,
|
|
11334
|
-
executable: existsSync6(executablePath)
|
|
11335
|
-
app_code_hash: signingInfo.cdHash,
|
|
11336
|
-
ad_hoc_signed: signingInfo.adHoc,
|
|
11337
|
-
microphone_permission: getTccPermission("kTCCServiceMicrophone", home, permissionCodeHash),
|
|
11338
|
-
accessibility_permission: getTccPermission("kTCCServiceAccessibility", home, permissionCodeHash),
|
|
11339
|
-
log_path: logPath
|
|
11243
|
+
executable: existsSync6(executablePath)
|
|
11340
11244
|
};
|
|
11341
11245
|
}
|
|
11342
|
-
function getCodeSigningInfo(appPath) {
|
|
11343
|
-
if (process.platform !== "darwin" || !existsSync6(appPath)) {
|
|
11344
|
-
return { cdHash: null, adHoc: false };
|
|
11345
|
-
}
|
|
11346
|
-
const result = spawnSync("codesign", ["-d", "--verbose=4", appPath], {
|
|
11347
|
-
encoding: "utf8",
|
|
11348
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
11349
|
-
});
|
|
11350
|
-
const output = `${result.stdout}
|
|
11351
|
-
${result.stderr}`;
|
|
11352
|
-
const cdHash = output.match(/^CDHash=([a-fA-F0-9]+)/m)?.[1]?.toLowerCase() ?? null;
|
|
11353
|
-
const adHoc = /Signature=adhoc/.test(output);
|
|
11354
|
-
return { cdHash, adHoc };
|
|
11355
|
-
}
|
|
11356
|
-
function getTccPermission(service, home, currentCodeHash) {
|
|
11357
|
-
if (process.platform !== "darwin")
|
|
11358
|
-
return "unsupported";
|
|
11359
|
-
const dbPaths = [
|
|
11360
|
-
pathJoin(home, "Library", "Application Support", "com.apple.TCC", "TCC.db"),
|
|
11361
|
-
pathJoin("/", "Library", "Application Support", "com.apple.TCC", "TCC.db")
|
|
11362
|
-
];
|
|
11363
|
-
const sql = "select auth_value || '|' || ifnull(hex(csreq), '') from access where service = '" + service.replace(/'/g, "''") + "' and client = 'com.hasna.recordings' order by last_modified desc limit 1;";
|
|
11364
|
-
for (const dbPath of dbPaths) {
|
|
11365
|
-
if (!existsSync6(dbPath))
|
|
11366
|
-
continue;
|
|
11367
|
-
const result = spawnSync("sqlite3", [dbPath, sql], {
|
|
11368
|
-
encoding: "utf8",
|
|
11369
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
11370
|
-
});
|
|
11371
|
-
const value = result.stdout.trim();
|
|
11372
|
-
if (!value)
|
|
11373
|
-
continue;
|
|
11374
|
-
const [authValue, csreqHex = ""] = value.split("|");
|
|
11375
|
-
const label = tccAuthValueLabel(authValue ?? "");
|
|
11376
|
-
if (label === "allowed" && currentCodeHash && csreqHex && !csreqHex.toLowerCase().includes(currentCodeHash.toLowerCase())) {
|
|
11377
|
-
return "stale_allowed_for_previous_app_build";
|
|
11378
|
-
}
|
|
11379
|
-
return label;
|
|
11380
|
-
}
|
|
11381
|
-
return "not_determined";
|
|
11382
|
-
}
|
|
11383
|
-
function tccAuthValueLabel(value) {
|
|
11384
|
-
switch (value) {
|
|
11385
|
-
case "0":
|
|
11386
|
-
return "denied";
|
|
11387
|
-
case "1":
|
|
11388
|
-
return "unknown";
|
|
11389
|
-
case "2":
|
|
11390
|
-
return "allowed";
|
|
11391
|
-
case "3":
|
|
11392
|
-
return "limited";
|
|
11393
|
-
default:
|
|
11394
|
-
return `unknown(${value})`;
|
|
11395
|
-
}
|
|
11396
|
-
}
|
|
11397
11246
|
function findPackageRoot() {
|
|
11398
11247
|
let current = dirname3(fileURLToPath(import.meta.url));
|
|
11399
11248
|
while (true) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/db/database.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AA+F7C,wBAAgB,WAAW,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,QAAQ,CAiBrD;
|
|
1
|
+
{"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/db/database.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AA+F7C,wBAAgB,WAAW,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,QAAQ,CAiBrD;AA8BD,wBAAgB,aAAa,IAAI,IAAI,CAMpC;AAED,wBAAgB,aAAa,IAAI,IAAI,CAGpC;AAED,oEAAoE;AACpE,wBAAgB,UAAU,IAAI,aAAa,CAK1C;AAED,wBAAgB,SAAS,IAAI,MAAM,CAElC;AAED,wBAAgB,SAAS,IAAI,MAAM,CAElC"}
|
package/dist/index.js
CHANGED
|
@@ -9844,40 +9844,15 @@ function runMigrations(db) {
|
|
|
9844
9844
|
const result = db.query("SELECT MAX(id) as max_id FROM _migrations").get();
|
|
9845
9845
|
const currentLevel = result?.max_id ?? -1;
|
|
9846
9846
|
for (let i = currentLevel + 1;i < MIGRATIONS.length; i++) {
|
|
9847
|
+
db.run(MIGRATIONS[i]);
|
|
9847
9848
|
try {
|
|
9848
|
-
db.run(MIGRATIONS[i]);
|
|
9849
9849
|
db.query("INSERT INTO _migrations (id) VALUES (?)").run(i);
|
|
9850
9850
|
} catch (e) {
|
|
9851
|
-
if (isBenignMigrationError(e)) {
|
|
9852
|
-
db.query("INSERT OR IGNORE INTO _migrations (id) VALUES (?)").run(i);
|
|
9853
|
-
continue;
|
|
9854
|
-
}
|
|
9855
9851
|
if (!(e instanceof Error && e.message.includes("UNIQUE constraint failed"))) {
|
|
9856
9852
|
throw e;
|
|
9857
9853
|
}
|
|
9858
9854
|
}
|
|
9859
9855
|
}
|
|
9860
|
-
repairSchemaDrift(db);
|
|
9861
|
-
}
|
|
9862
|
-
function isBenignMigrationError(error) {
|
|
9863
|
-
if (!(error instanceof Error))
|
|
9864
|
-
return false;
|
|
9865
|
-
return error.message.includes("duplicate column name");
|
|
9866
|
-
}
|
|
9867
|
-
function repairSchemaDrift(db) {
|
|
9868
|
-
ensureColumn(db, "recordings", "goal", "TEXT");
|
|
9869
|
-
ensureColumn(db, "recordings", "role", "TEXT");
|
|
9870
|
-
ensureColumn(db, "recordings", "task_list_id", "TEXT");
|
|
9871
|
-
ensureColumn(db, "recordings", "machine_id", "TEXT");
|
|
9872
|
-
ensureColumn(db, "recordings", "metadata", "TEXT DEFAULT '{}'");
|
|
9873
|
-
ensureColumn(db, "agents", "active_project_id", "TEXT REFERENCES projects(id) ON DELETE SET NULL");
|
|
9874
|
-
}
|
|
9875
|
-
function ensureColumn(db, table, column, definition) {
|
|
9876
|
-
const rows = db.query(`PRAGMA table_info(${table})`).all();
|
|
9877
|
-
if (rows.some((row) => row.name === column)) {
|
|
9878
|
-
return;
|
|
9879
|
-
}
|
|
9880
|
-
db.run(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
|
|
9881
9856
|
}
|
|
9882
9857
|
function closeDatabase() {
|
|
9883
9858
|
if (_db) {
|
package/dist/mcp/index.js
CHANGED
|
@@ -21,13 +21,9 @@ 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
|
-
repository: {
|
|
28
|
-
type: "git",
|
|
29
|
-
url: "git+https://github.com/hasna/recordings.git"
|
|
30
|
-
},
|
|
31
27
|
main: "dist/index.js",
|
|
32
28
|
types: "dist/index.d.ts",
|
|
33
29
|
bin: {
|
|
@@ -14530,40 +14526,15 @@ function runMigrations(db) {
|
|
|
14530
14526
|
const result = db.query("SELECT MAX(id) as max_id FROM _migrations").get();
|
|
14531
14527
|
const currentLevel = result?.max_id ?? -1;
|
|
14532
14528
|
for (let i = currentLevel + 1;i < MIGRATIONS.length; i++) {
|
|
14529
|
+
db.run(MIGRATIONS[i]);
|
|
14533
14530
|
try {
|
|
14534
|
-
db.run(MIGRATIONS[i]);
|
|
14535
14531
|
db.query("INSERT INTO _migrations (id) VALUES (?)").run(i);
|
|
14536
14532
|
} catch (e) {
|
|
14537
|
-
if (isBenignMigrationError(e)) {
|
|
14538
|
-
db.query("INSERT OR IGNORE INTO _migrations (id) VALUES (?)").run(i);
|
|
14539
|
-
continue;
|
|
14540
|
-
}
|
|
14541
14533
|
if (!(e instanceof Error && e.message.includes("UNIQUE constraint failed"))) {
|
|
14542
14534
|
throw e;
|
|
14543
14535
|
}
|
|
14544
14536
|
}
|
|
14545
14537
|
}
|
|
14546
|
-
repairSchemaDrift(db);
|
|
14547
|
-
}
|
|
14548
|
-
function isBenignMigrationError(error) {
|
|
14549
|
-
if (!(error instanceof Error))
|
|
14550
|
-
return false;
|
|
14551
|
-
return error.message.includes("duplicate column name");
|
|
14552
|
-
}
|
|
14553
|
-
function repairSchemaDrift(db) {
|
|
14554
|
-
ensureColumn(db, "recordings", "goal", "TEXT");
|
|
14555
|
-
ensureColumn(db, "recordings", "role", "TEXT");
|
|
14556
|
-
ensureColumn(db, "recordings", "task_list_id", "TEXT");
|
|
14557
|
-
ensureColumn(db, "recordings", "machine_id", "TEXT");
|
|
14558
|
-
ensureColumn(db, "recordings", "metadata", "TEXT DEFAULT '{}'");
|
|
14559
|
-
ensureColumn(db, "agents", "active_project_id", "TEXT REFERENCES projects(id) ON DELETE SET NULL");
|
|
14560
|
-
}
|
|
14561
|
-
function ensureColumn(db, table, column, definition) {
|
|
14562
|
-
const rows = db.query(`PRAGMA table_info(${table})`).all();
|
|
14563
|
-
if (rows.some((row) => row.name === column)) {
|
|
14564
|
-
return;
|
|
14565
|
-
}
|
|
14566
|
-
db.run(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
|
|
14567
14538
|
}
|
|
14568
14539
|
function getAdapter() {
|
|
14569
14540
|
if (!_adapter) {
|
|
@@ -14983,7 +14954,7 @@ async function processText(rawText, config, systemPrompt) {
|
|
|
14983
14954
|
}
|
|
14984
14955
|
|
|
14985
14956
|
// src/version.ts
|
|
14986
|
-
var VERSION = "0.1.
|
|
14957
|
+
var VERSION = "0.1.11";
|
|
14987
14958
|
|
|
14988
14959
|
// src/mcp/index.ts
|
|
14989
14960
|
var config = loadConfig();
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const VERSION = "0.1.
|
|
1
|
+
export declare const VERSION = "0.1.11";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hasna/recordings",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.20",
|
|
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
|
-
"repository": {
|
|
7
|
-
"type": "git",
|
|
8
|
-
"url": "git+https://github.com/hasna/recordings.git"
|
|
9
|
-
},
|
|
10
6
|
"main": "dist/index.js",
|
|
11
7
|
"types": "dist/index.d.ts",
|
|
12
8
|
"bin": {
|
|
@@ -73,36 +73,4 @@ rm -rf "$APP_DEST"
|
|
|
73
73
|
mkdir -p "$DATA_DIR"
|
|
74
74
|
cp -R "$APP_SOURCE" "$APP_DEST" || warn_or_fail "failed to copy app bundle"
|
|
75
75
|
|
|
76
|
-
current_cdhash() {
|
|
77
|
-
codesign -d --verbose=4 "$1" 2>&1 | awk -F= '/^CDHash=/ { print toupper($2); exit }'
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
tcc_csreq_hex() {
|
|
81
|
-
local db_path="$1"
|
|
82
|
-
local service="$2"
|
|
83
|
-
if [ ! -r "$db_path" ] || ! command -v sqlite3 >/dev/null 2>&1; then
|
|
84
|
-
return 0
|
|
85
|
-
fi
|
|
86
|
-
sqlite3 "$db_path" \
|
|
87
|
-
"SELECT hex(csreq) FROM access WHERE service = '${service}' AND client = 'com.hasna.recordings' ORDER BY last_modified DESC LIMIT 1;" \
|
|
88
|
-
2>/dev/null || true
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
reset_stale_permission() {
|
|
92
|
-
local service="$1"
|
|
93
|
-
local tcc_service="$2"
|
|
94
|
-
local db_path="$3"
|
|
95
|
-
local cdhash="$4"
|
|
96
|
-
local csreq_hex
|
|
97
|
-
csreq_hex="$(tcc_csreq_hex "$db_path" "$tcc_service" | tr '[:lower:]' '[:upper:]')"
|
|
98
|
-
if [ -n "$cdhash" ] && [ -n "$csreq_hex" ] && [[ "$csreq_hex" != *"$cdhash"* ]]; then
|
|
99
|
-
tccutil reset "$service" com.hasna.recordings >/dev/null 2>&1 || true
|
|
100
|
-
echo "Reset stale ${service} permission for the newly installed Recordings.app."
|
|
101
|
-
fi
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
APP_CDHASH="$(current_cdhash "$APP_DEST" || true)"
|
|
105
|
-
reset_stale_permission "Microphone" "kTCCServiceMicrophone" "${HOME}/Library/Application Support/com.apple.TCC/TCC.db" "$APP_CDHASH"
|
|
106
|
-
reset_stale_permission "Accessibility" "kTCCServiceAccessibility" "/Library/Application Support/com.apple.TCC/TCC.db" "$APP_CDHASH"
|
|
107
|
-
|
|
108
76
|
echo "Installed Recordings.app from package: ${APP_DEST}"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import SwiftUI
|
|
2
|
-
|
|
2
|
+
import KeyboardShortcuts
|
|
3
3
|
|
|
4
4
|
public struct MenuBarPopover: View {
|
|
5
5
|
@ObservedObject public var engine: RecordingEngine
|
|
@@ -237,10 +237,11 @@ public struct MenuBarPopover: View {
|
|
|
237
237
|
showProject: filterProjectId == nil,
|
|
238
238
|
isCopied: copiedIndex == i
|
|
239
239
|
) {
|
|
240
|
+
NSPasteboard.general.clearContents()
|
|
241
|
+
NSPasteboard.general.setString(item.displayText, forType: .string)
|
|
240
242
|
withAnimation(.easeInOut(duration: 0.15)) {
|
|
241
243
|
copiedIndex = i
|
|
242
244
|
}
|
|
243
|
-
engine.pasteIntoFrontApp(item.displayText)
|
|
244
245
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1.2) {
|
|
245
246
|
withAnimation { if copiedIndex == i { copiedIndex = nil } }
|
|
246
247
|
}
|
|
@@ -16,20 +16,12 @@ public final class RealtimeTranscriptionClient: ObservableObject, @unchecked Sen
|
|
|
16
16
|
|
|
17
17
|
private var ws: URLSessionWebSocketTask?
|
|
18
18
|
private var receiveTask: Task<Void, Never>?
|
|
19
|
-
private var outboundEventTask: Task<Void, Never>?
|
|
20
19
|
private var isConfigured = false
|
|
21
20
|
private var pendingAudioChunks: [Data] = []
|
|
22
21
|
private var itemOrder: [String] = []
|
|
23
22
|
private var deltaTextByItem: [String: String] = [:]
|
|
24
23
|
private var completedTextByItem: [String: String] = [:]
|
|
25
|
-
private var committedItemIDs = Set<String>()
|
|
26
|
-
private var completedItemIDs = Set<String>()
|
|
27
24
|
private var completedEventCount = 0
|
|
28
|
-
private var queuedCommitCount = 0
|
|
29
|
-
private var uncommittedAudioBytes = 0
|
|
30
|
-
private var lastRealtimeEventAt = Date.distantPast
|
|
31
|
-
|
|
32
|
-
private nonisolated static let minimumManualCommitBytes = 5_760
|
|
33
25
|
|
|
34
26
|
private let apiKey: String
|
|
35
27
|
private let homePath: String
|
|
@@ -60,12 +52,7 @@ public final class RealtimeTranscriptionClient: ObservableObject, @unchecked Sen
|
|
|
60
52
|
itemOrder.removeAll(keepingCapacity: true)
|
|
61
53
|
deltaTextByItem.removeAll(keepingCapacity: true)
|
|
62
54
|
completedTextByItem.removeAll(keepingCapacity: true)
|
|
63
|
-
committedItemIDs.removeAll(keepingCapacity: true)
|
|
64
|
-
completedItemIDs.removeAll(keepingCapacity: true)
|
|
65
55
|
completedEventCount = 0
|
|
66
|
-
queuedCommitCount = 0
|
|
67
|
-
uncommittedAudioBytes = 0
|
|
68
|
-
lastRealtimeEventAt = Date()
|
|
69
56
|
error = nil
|
|
70
57
|
|
|
71
58
|
var request = URLRequest(url: Self.transcriptionURL)
|
|
@@ -111,51 +98,33 @@ public final class RealtimeTranscriptionClient: ObservableObject, @unchecked Sen
|
|
|
111
98
|
return
|
|
112
99
|
}
|
|
113
100
|
let base64 = data.base64EncodedString()
|
|
114
|
-
uncommittedAudioBytes += data.count
|
|
115
101
|
let msg: [String: Any] = [
|
|
116
102
|
"type": "input_audio_buffer.append",
|
|
117
103
|
"audio": base64,
|
|
118
104
|
]
|
|
119
|
-
|
|
105
|
+
Task { [weak self] in
|
|
106
|
+
try? await self?.sendEvent(msg)
|
|
107
|
+
}
|
|
120
108
|
}
|
|
121
109
|
|
|
122
110
|
/// Signal end of input — triggers final transcription completion.
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
guard isStreaming, ws != nil else { return false }
|
|
111
|
+
public func commitInput() async {
|
|
112
|
+
guard isStreaming, ws != nil else { return }
|
|
126
113
|
flushPendingAudio()
|
|
127
|
-
let bytesToCommit = uncommittedAudioBytes
|
|
128
|
-
guard Self.shouldManuallyCommit(uncommittedAudioBytes: bytesToCommit) else {
|
|
129
|
-
return false
|
|
130
|
-
}
|
|
131
114
|
let msg: [String: Any] = [
|
|
132
115
|
"type": "input_audio_buffer.commit",
|
|
133
116
|
]
|
|
134
|
-
|
|
135
|
-
queuedCommitCount += 1
|
|
136
|
-
lastRealtimeEventAt = Date()
|
|
137
|
-
NativeAppLog.write("realtime commit queued reason=\(reason) bytes=\(bytesToCommit)", homePath: homePath)
|
|
138
|
-
let commitTask = enqueueOutboundEvent(msg)
|
|
139
|
-
await commitTask?.value
|
|
140
|
-
return true
|
|
117
|
+
try? await sendEvent(msg)
|
|
141
118
|
}
|
|
142
119
|
|
|
143
120
|
/// Commit buffered input, wait briefly for a final completed event, then close.
|
|
144
|
-
public func finish(timeoutMilliseconds: UInt64 =
|
|
121
|
+
public func finish(timeoutMilliseconds: UInt64 = 1_800) async -> String {
|
|
145
122
|
guard isStreaming else { return accumulatedText }
|
|
146
|
-
let
|
|
147
|
-
|
|
148
|
-
let expectedCommitCount = queuedCommitCount
|
|
123
|
+
let initialCompletedCount = completedEventCount
|
|
124
|
+
await commitInput()
|
|
149
125
|
|
|
150
126
|
let deadline = Date().addingTimeInterval(TimeInterval(timeoutMilliseconds) / 1_000)
|
|
151
|
-
while Date() < deadline, isStreaming {
|
|
152
|
-
let hasIncompleteCommittedItems = !committedItemIDs.subtracting(completedItemIDs).isEmpty
|
|
153
|
-
let hasManualCommitCompletion = !didManualCommit || completedEventCount > completedCountBeforeCommit
|
|
154
|
-
let hasQueuedCommitCompletion = completedEventCount >= expectedCommitCount
|
|
155
|
-
let quietLongEnough = Date().timeIntervalSince(lastRealtimeEventAt) >= 0.35
|
|
156
|
-
if !hasIncompleteCommittedItems, hasManualCommitCompletion, hasQueuedCommitCompletion, quietLongEnough {
|
|
157
|
-
break
|
|
158
|
-
}
|
|
127
|
+
while Date() < deadline, isStreaming, completedEventCount == initialCompletedCount {
|
|
159
128
|
try? await Task.sleep(for: .milliseconds(50))
|
|
160
129
|
}
|
|
161
130
|
|
|
@@ -170,10 +139,8 @@ public final class RealtimeTranscriptionClient: ObservableObject, @unchecked Sen
|
|
|
170
139
|
isConfigured = false
|
|
171
140
|
receiveTask?.cancel()
|
|
172
141
|
ws?.cancel(with: .normalClosure, reason: nil)
|
|
173
|
-
outboundEventTask?.cancel()
|
|
174
142
|
ws = nil
|
|
175
143
|
receiveTask = nil
|
|
176
|
-
outboundEventTask = nil
|
|
177
144
|
pendingAudioChunks.removeAll(keepingCapacity: true)
|
|
178
145
|
let text = accumulatedText
|
|
179
146
|
return text
|
|
@@ -213,14 +180,11 @@ public final class RealtimeTranscriptionClient: ObservableObject, @unchecked Sen
|
|
|
213
180
|
|
|
214
181
|
switch type {
|
|
215
182
|
case "input_audio_buffer.committed":
|
|
216
|
-
lastRealtimeEventAt = Date()
|
|
217
183
|
if let itemID = json["item_id"] as? String {
|
|
218
184
|
registerItem(itemID, previousItemID: json["previous_item_id"] as? String)
|
|
219
|
-
committedItemIDs.insert(itemID)
|
|
220
185
|
}
|
|
221
186
|
|
|
222
187
|
case "conversation.item.input_audio_transcription.delta":
|
|
223
|
-
lastRealtimeEventAt = Date()
|
|
224
188
|
let itemID = json["item_id"] as? String ?? "__default__"
|
|
225
189
|
registerItem(itemID, previousItemID: nil)
|
|
226
190
|
if let delta = json["delta"] as? String {
|
|
@@ -230,29 +194,21 @@ public final class RealtimeTranscriptionClient: ObservableObject, @unchecked Sen
|
|
|
230
194
|
|
|
231
195
|
case "conversation.item.input_audio_transcription.completed":
|
|
232
196
|
// The server may send the complete transcript here
|
|
233
|
-
lastRealtimeEventAt = Date()
|
|
234
197
|
let itemID = json["item_id"] as? String ?? "__default__"
|
|
235
198
|
registerItem(itemID, previousItemID: nil)
|
|
236
|
-
completedItemIDs.insert(itemID)
|
|
237
|
-
completedEventCount += 1
|
|
238
199
|
if let text = json["transcript"] as? String, !text.isEmpty {
|
|
239
200
|
completedTextByItem[itemID] = text
|
|
201
|
+
completedEventCount += 1
|
|
202
|
+
rebuildAccumulatedText()
|
|
240
203
|
}
|
|
241
|
-
rebuildAccumulatedText()
|
|
242
204
|
|
|
243
205
|
case "conversation.item.input_audio_transcription.failed":
|
|
244
|
-
lastRealtimeEventAt = Date()
|
|
245
|
-
if let itemID = json["item_id"] as? String {
|
|
246
|
-
completedItemIDs.insert(itemID)
|
|
247
|
-
completedEventCount += 1
|
|
248
|
-
}
|
|
249
206
|
if let msg = json["error"] as? [String: Any],
|
|
250
207
|
let message = msg["message"] as? String {
|
|
251
208
|
self.error = message
|
|
252
209
|
}
|
|
253
210
|
|
|
254
211
|
case "error":
|
|
255
|
-
lastRealtimeEventAt = Date()
|
|
256
212
|
if let detail = json["error"] as? [String: Any],
|
|
257
213
|
let msg = detail["message"] as? String {
|
|
258
214
|
fputs("[RealtimeClient] Error: \(msg)\n", stderr)
|
|
@@ -264,18 +220,6 @@ public final class RealtimeTranscriptionClient: ObservableObject, @unchecked Sen
|
|
|
264
220
|
}
|
|
265
221
|
}
|
|
266
222
|
|
|
267
|
-
@discardableResult
|
|
268
|
-
private func enqueueOutboundEvent(_ obj: [String: Any]) -> Task<Void, Never>? {
|
|
269
|
-
guard ws != nil else { return nil }
|
|
270
|
-
let previousTask = outboundEventTask
|
|
271
|
-
let task = Task { [weak self] in
|
|
272
|
-
await previousTask?.value
|
|
273
|
-
try? await self?.sendEvent(obj)
|
|
274
|
-
}
|
|
275
|
-
outboundEventTask = task
|
|
276
|
-
return task
|
|
277
|
-
}
|
|
278
|
-
|
|
279
223
|
private func registerItem(_ itemID: String, previousItemID: String?) {
|
|
280
224
|
guard !itemOrder.contains(itemID) else { return }
|
|
281
225
|
if let previousItemID, let previousIndex = itemOrder.firstIndex(of: previousItemID) {
|
|
@@ -351,10 +295,6 @@ public final class RealtimeTranscriptionClient: ObservableObject, @unchecked Sen
|
|
|
351
295
|
return output
|
|
352
296
|
}
|
|
353
297
|
|
|
354
|
-
private nonisolated static func shouldManuallyCommit(uncommittedAudioBytes: Int) -> Bool {
|
|
355
|
-
uncommittedAudioBytes >= minimumManualCommitBytes
|
|
356
|
-
}
|
|
357
|
-
|
|
358
298
|
private nonisolated static func transcriptionSessionUpdateEvent(transcription: [String: Any]) -> [String: Any] {
|
|
359
299
|
[
|
|
360
300
|
"type": "session.update",
|
|
@@ -423,10 +363,6 @@ extension RealtimeTranscriptionClient {
|
|
|
423
363
|
joinTranscriptParts(parts)
|
|
424
364
|
}
|
|
425
365
|
|
|
426
|
-
public nonisolated static func shouldManuallyCommitTestHelper(uncommittedAudioBytes: Int) -> Bool {
|
|
427
|
-
shouldManuallyCommit(uncommittedAudioBytes: uncommittedAudioBytes)
|
|
428
|
-
}
|
|
429
|
-
|
|
430
366
|
public nonisolated static func sessionUpdateTestHelper(prompt: String, language: String = "") -> [String: Any] {
|
|
431
367
|
var transcription: [String: Any] = [
|
|
432
368
|
"model": modelID,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import SwiftUI
|
|
2
|
-
|
|
2
|
+
import KeyboardShortcuts
|
|
3
3
|
|
|
4
4
|
public struct SettingsView: View {
|
|
5
5
|
@ObservedObject public var engine: RecordingEngine
|
|
@@ -45,32 +45,6 @@ public struct SettingsView: View {
|
|
|
45
45
|
.foregroundStyle(.secondary)
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
Section("Permissions") {
|
|
49
|
-
HStack {
|
|
50
|
-
Text("Microphone")
|
|
51
|
-
Spacer()
|
|
52
|
-
Text(engine.microphonePermissionLabel)
|
|
53
|
-
.foregroundStyle(.secondary)
|
|
54
|
-
}
|
|
55
|
-
Button("Request Microphone") {
|
|
56
|
-
engine.requestMicrophonePermission()
|
|
57
|
-
}
|
|
58
|
-
HStack {
|
|
59
|
-
Text("Accessibility")
|
|
60
|
-
Spacer()
|
|
61
|
-
Text(engine.accessibilityPermissionLabel)
|
|
62
|
-
.foregroundStyle(.secondary)
|
|
63
|
-
}
|
|
64
|
-
HStack {
|
|
65
|
-
Button("Request Accessibility") {
|
|
66
|
-
engine.requestAccessibilityPermission()
|
|
67
|
-
}
|
|
68
|
-
Button("Open Accessibility Settings") {
|
|
69
|
-
engine.openAccessibilitySettings()
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
48
|
Section("System Prompt") {
|
|
75
49
|
TextEditor(text: $projectStore.settings.globalSystemPrompt)
|
|
76
50
|
.frame(height: 80)
|
|
@@ -110,17 +110,4 @@ struct RealtimeTranscriptionTests {
|
|
|
110
110
|
let text = RealtimeTranscriptionClient.joinTranscriptPartsTestHelper(["Hello", "world.", " Next"])
|
|
111
111
|
#expect(text == "Hello world. Next")
|
|
112
112
|
}
|
|
113
|
-
|
|
114
|
-
@Test("Manual commit waits for enough buffered PCM audio")
|
|
115
|
-
func manualCommitThreshold() {
|
|
116
|
-
#expect(RealtimeTranscriptionClient.shouldManuallyCommitTestHelper(uncommittedAudioBytes: 4_799) == false)
|
|
117
|
-
#expect(RealtimeTranscriptionClient.shouldManuallyCommitTestHelper(uncommittedAudioBytes: 5_760) == true)
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
@Test("Partial realtime text falls back for longer recordings")
|
|
121
|
-
func partialRealtimeFallback() {
|
|
122
|
-
#expect(RecordingEngine.shouldFallbackFromPartialRealtime(text: "Hi", pcmByteCount: 96_000) == true)
|
|
123
|
-
#expect(RecordingEngine.shouldFallbackFromPartialRealtime(text: "This is a complete sentence.", pcmByteCount: 96_000) == false)
|
|
124
|
-
#expect(RecordingEngine.shouldFallbackFromPartialRealtime(text: "Hi", pcmByteCount: 12_000) == false)
|
|
125
|
-
}
|
|
126
113
|
}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import Foundation
|
|
2
|
-
import Testing
|
|
3
|
-
@testable import RecordingsLib
|
|
4
|
-
|
|
5
|
-
struct NativeAppDiagnosticsTests {
|
|
6
|
-
@Test("Native app log writes to recordings log file")
|
|
7
|
-
func logWritesFile() throws {
|
|
8
|
-
let home = try makeHome()
|
|
9
|
-
|
|
10
|
-
NativeAppLog.write("diagnostic-test", homePath: home)
|
|
11
|
-
|
|
12
|
-
let path = "\(home)/.hasna/recordings/Recordings.log"
|
|
13
|
-
let text = try String(contentsOfFile: path, encoding: .utf8)
|
|
14
|
-
#expect(text.contains("diagnostic-test"))
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
private func makeHome() throws -> String {
|
|
18
|
-
let url = FileManager.default.temporaryDirectory
|
|
19
|
-
.appendingPathComponent("recordings-diagnostics-tests-\(UUID().uuidString)")
|
|
20
|
-
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
|
|
21
|
-
return url.path
|
|
22
|
-
}
|
|
23
|
-
}
|