@hasna/recordings 0.2.10 → 0.2.13
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/README.md +350 -35
- package/bun.lock +3 -0
- package/dist/__tests__/helpers/native-fs-guard.d.ts +2 -0
- package/dist/__tests__/helpers/native-fs-guard.d.ts.map +1 -0
- package/dist/cli/index.js +1728 -320
- package/dist/cli/macos-permissions.d.ts +13 -0
- package/dist/cli/macos-permissions.d.ts.map +1 -0
- package/dist/cli/options.d.ts +12 -0
- package/dist/cli/options.d.ts.map +1 -1
- package/dist/db/pg-migrations.d.ts.map +1 -1
- package/dist/db/recordings.d.ts +2 -1
- package/dist/db/recordings.d.ts.map +1 -1
- package/dist/db/remote-storage.d.ts +5 -1
- package/dist/db/remote-storage.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +628 -147
- package/dist/lib/bun-runtime.d.ts +11 -0
- package/dist/lib/bun-runtime.d.ts.map +1 -0
- package/dist/lib/config.d.ts +8 -1
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/enhancer.d.ts +10 -2
- package/dist/lib/enhancer.d.ts.map +1 -1
- package/dist/lib/install-maintenance.d.ts +24 -0
- package/dist/lib/install-maintenance.d.ts.map +1 -0
- package/dist/lib/machine.d.ts +2 -0
- package/dist/lib/machine.d.ts.map +1 -0
- package/dist/lib/recorder.d.ts +2 -1
- package/dist/lib/recorder.d.ts.map +1 -1
- package/dist/lib/recording-create-identity.d.ts +13 -0
- package/dist/lib/recording-create-identity.d.ts.map +1 -0
- package/dist/lib/release-install-policy.d.ts +31 -0
- package/dist/lib/release-install-policy.d.ts.map +1 -0
- package/dist/lib/transcriber.d.ts.map +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +977 -194
- package/dist/sdk/index.js +5 -1
- package/dist/sdk/v1.generated.d.ts +4 -0
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/cloud-config.d.ts +9 -0
- package/dist/server/cloud-config.d.ts.map +1 -0
- package/dist/server/cloud-readiness.d.ts +10 -0
- package/dist/server/cloud-readiness.d.ts.map +1 -0
- package/dist/server/cloud.d.ts +9 -18
- package/dist/server/cloud.d.ts.map +1 -1
- package/dist/server/index.js +1654 -304
- package/dist/server/migrate-command.d.ts +11 -0
- package/dist/server/migrate-command.d.ts.map +1 -0
- package/dist/server/openapi.d.ts +22 -0
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/repo.d.ts +8 -1
- package/dist/server/repo.d.ts.map +1 -1
- package/dist/server/serve.d.ts +6 -1
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/server/v1.d.ts.map +1 -1
- package/dist/storage.js +515 -80
- package/dist/store.d.ts +3 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/types/index.d.ts +10 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +7 -4
- package/packaging/macos/Empty.entitlements +5 -0
- package/packaging/macos/Library/LaunchDaemons/com.hasna.recordings.updater.plist +34 -0
- package/packaging/macos/Verifier.entitlements +10 -0
- package/packaging/macos/artifact-verifier.sb +33 -0
- package/packaging/macos/build_release_pkg.sh +872 -0
- package/packaging/macos/managed_bootstrap.sh +623 -0
- package/packaging/macos/pkgutil_fingerprint.awk +37 -0
- package/packaging/macos/release_lifecycle.ts +463 -0
- package/packaging/macos/scripts/postinstall +565 -0
- package/packaging/macos/scripts/preinstall +297 -0
- package/scripts/build_companion_cli.sh +337 -0
- package/scripts/build_native_fs_guard.sh +59 -0
- package/scripts/generate-sdk.ts +19 -1
- package/scripts/install_macos_app.sh +1922 -77
- package/scripts/macos_artifact.ts +5316 -0
- package/scripts/migrate.ts +38 -10
- package/scripts/native/prebuilds/darwin-universal/recordings_fs_guard.node +0 -0
- package/scripts/native/recordings_fs_guard.c +1167 -0
- package/scripts/native_fs_guard.ts +158 -0
- package/scripts/release-guard.ts +20 -1
- package/scripts/resolve_tailscale_cli.sh +389 -0
- package/scripts/smoke_macos_app.sh +573 -0
- package/src/native/Recordings/App/ContentView.swift +83 -0
- package/src/native/Recordings/App/MenuBarStatusView.swift +102 -0
- package/src/native/Recordings/App/RecordWorkspaceView.swift +468 -0
- package/src/native/Recordings/App/RecordingDetailView.swift +138 -0
- package/src/native/Recordings/App/RecordingsApp.swift +367 -44
- package/src/native/Recordings/App/RecordingsListView.swift +130 -0
- package/src/native/Recordings/App/RecordingsStore.swift +206 -0
- package/src/native/Recordings/App/RuntimeSmoke.swift +158 -0
- package/src/native/Recordings/App/SidebarView.swift +212 -0
- package/src/native/Recordings/App/Theme.swift +87 -0
- package/src/native/Recordings/Package.resolved +5 -5
- package/src/native/Recordings/Package.swift +62 -2
- package/src/native/Recordings/RecordingsLib/AccessibilityPromptGate.swift +104 -0
- package/src/native/Recordings/RecordingsLib/BrandAssets.swift +0 -32
- package/src/native/Recordings/RecordingsLib/ChromeSurface.swift +17 -0
- package/src/native/Recordings/RecordingsLib/Info.plist +7 -3
- package/src/native/Recordings/RecordingsLib/MenuBarPresentation.swift +37 -0
- package/src/native/Recordings/RecordingsLib/NativeAppDiagnostics.swift +32 -2
- package/src/native/Recordings/RecordingsLib/NativeMachineIdentity.swift +24 -0
- package/src/native/Recordings/RecordingsLib/NativePCMRecorder.swift +262 -23
- package/src/native/Recordings/RecordingsLib/OpenAIAPIKeyStore.swift +56 -0
- package/src/native/Recordings/RecordingsLib/PermissionRequestLaunchPlan.swift +56 -0
- package/src/native/Recordings/RecordingsLib/ProjectStore.swift +321 -25
- package/src/native/Recordings/RecordingsLib/RealtimeTranscriptionClient.swift +605 -90
- package/src/native/Recordings/RecordingsLib/Recording.swift +141 -0
- package/src/native/Recordings/RecordingsLib/RecordingEngine.swift +3704 -376
- package/src/native/Recordings/RecordingsLib/RecordingStartControlPresentation.swift +29 -0
- package/src/native/Recordings/RecordingsLib/RecordingsCLI.entitlements +10 -0
- package/src/native/Recordings/RecordingsLib/RecordingsCLI.swift +222 -0
- package/src/native/Recordings/RecordingsLib/SettingsView.swift +45 -11
- package/src/native/Recordings/RecordingsLib/SpeechIntent.swift +331 -0
- package/src/native/Recordings/RecordingsLib/SpeechIntentClassifier.swift +232 -0
- package/src/native/Recordings/RecordingsLib/VoiceShortcuts.swift +17 -7
- package/src/native/Recordings/RecordingsTests/CLIRunnerTests.swift +1343 -0
- package/src/native/Recordings/RecordingsTests/MenuBarPresentationTests.swift +98 -0
- package/src/native/Recordings/RecordingsTests/NativeAppDiagnosticsTests.swift +20 -0
- package/src/native/Recordings/RecordingsTests/NativePCMRecorderTests.swift +370 -1
- package/src/native/Recordings/RecordingsTests/PasteTargetTests.swift +987 -1
- package/src/native/Recordings/RecordingsTests/ProjectStoreTests.swift +385 -0
- package/src/native/Recordings/RecordingsTests/RealtimeTranscriptionTests.swift +835 -8
- package/src/native/Recordings/RecordingsTests/RecordingBridgeTests.swift +180 -0
- package/src/native/Recordings/RecordingsTests/RecordingEngineDeliveryTests.swift +760 -0
- package/src/native/Recordings/RecordingsTests/RecordingStartControlPresentationTests.swift +42 -0
- package/src/native/Recordings/RecordingsTests/RecordingStartGateTests.swift +264 -0
- package/src/native/Recordings/RecordingsTests/RecordingStartTimingTests.swift +129 -0
- package/src/native/Recordings/RecordingsTests/SpeechIntentTests.swift +600 -0
- package/src/native/Recordings/RecordingsTests/TranscriptResolutionTests.swift +62 -1
- package/src/native/Recordings/RecordingsTests/TranscriptionResultIdentityTests.swift +21 -0
- package/src/native/Recordings/Updater/BootstrapPreflight/BootstrapPreflightMain.swift +739 -0
- package/src/native/Recordings/Updater/Broker/ActivationRecoveryPolicy.swift +254 -0
- package/src/native/Recordings/Updater/Broker/ApplicationNamespace.swift +463 -0
- package/src/native/Recordings/Updater/Broker/ApplicationProcessQuiescence.swift +55 -0
- package/src/native/Recordings/Updater/Broker/ArtifactIngest.swift +341 -0
- package/src/native/Recordings/Updater/Broker/AtomicActivation.swift +477 -0
- package/src/native/Recordings/Updater/Broker/BrokerMain.swift +624 -0
- package/src/native/Recordings/Updater/Broker/CanonicalTreeCopy.swift +63 -0
- package/src/native/Recordings/Updater/Broker/CodeValidation.swift +561 -0
- package/src/native/Recordings/Updater/Broker/DarwinACLValidator.swift +66 -0
- package/src/native/Recordings/Updater/Broker/HostOSProductVersion.swift +50 -0
- package/src/native/Recordings/Updater/Broker/InstallJournal.swift +362 -0
- package/src/native/Recordings/Updater/Broker/InstallRecovery.swift +662 -0
- package/src/native/Recordings/Updater/Broker/MonotonicState.swift +456 -0
- package/src/native/Recordings/Updater/Broker/PeerIdentity.swift +274 -0
- package/src/native/Recordings/Updater/Broker/VerifierRunner.swift +81 -0
- package/src/native/Recordings/Updater/BrokerTests/ActivationRecoveryPolicyTests.swift +259 -0
- package/src/native/Recordings/Updater/BrokerTests/CanonicalReleaseOrderTests.swift +21 -0
- package/src/native/Recordings/Updater/Client/ClientMain.swift +119 -0
- package/src/native/Recordings/Updater/Protocol/BoundedProcess.swift +159 -0
- package/src/native/Recordings/Updater/Protocol/CandidateMetadataPolicy.swift +214 -0
- package/src/native/Recordings/Updater/Protocol/HostOSVersionPolicy.swift +55 -0
- package/src/native/Recordings/Updater/Protocol/MonotonicReleasePolicy.swift +152 -0
- package/src/native/Recordings/Updater/Protocol/ReleaseEnvelope.swift +229 -0
- package/src/native/Recordings/Updater/Protocol/UpdateProtocol.swift +119 -0
- package/src/native/Recordings/Updater/ProtocolTests/BoundedProcessRunnerTests.swift +60 -0
- package/src/native/Recordings/Updater/ProtocolTests/CandidateMetadataPolicyTests.swift +168 -0
- package/src/native/Recordings/Updater/ProtocolTests/HostOSVersionPolicyTests.swift +62 -0
- package/src/native/Recordings/Updater/ProtocolTests/MonotonicReleasePolicyTests.swift +172 -0
- package/src/native/Recordings/Updater/ProtocolTests/ReleaseEnvelopeValidationTests.swift +65 -0
- package/src/native/Recordings/Updater/Signer/SignerMain.swift +210 -0
- package/src/native/Recordings/Updater/VerifierLauncher/RecordingsVerifierLauncher.c +725 -0
- package/src/native/Recordings/Updater/VerifierLauncher/include/RecordingsVerifierLauncher.h +33 -0
- package/src/native/Recordings/build.sh +1990 -29
- package/src/native/Recordings/RecordingsLib/MenuBarPopover.swift +0 -380
package/dist/mcp/index.js
CHANGED
|
@@ -4039,19 +4039,196 @@ var coerce = {
|
|
|
4039
4039
|
};
|
|
4040
4040
|
var NEVER = INVALID;
|
|
4041
4041
|
// src/lib/config.ts
|
|
4042
|
-
import { copyFileSync, existsSync, readFileSync, mkdirSync, readdirSync, statSync } from "fs";
|
|
4043
|
-
import { dirname, join, resolve } from "path";
|
|
4042
|
+
import { copyFileSync, existsSync, readFileSync, mkdirSync as mkdirSync2, readdirSync, realpathSync as realpathSync2, statSync } from "fs";
|
|
4043
|
+
import { dirname as dirname2, join as join2, resolve as resolve2, sep as sep2 } from "path";
|
|
4044
|
+
import { homedir as homedir2 } from "os";
|
|
4045
|
+
|
|
4046
|
+
// src/lib/install-maintenance.ts
|
|
4047
|
+
import { randomUUID } from "crypto";
|
|
4048
|
+
import {
|
|
4049
|
+
chmodSync,
|
|
4050
|
+
lstatSync,
|
|
4051
|
+
mkdirSync,
|
|
4052
|
+
realpathSync,
|
|
4053
|
+
renameSync,
|
|
4054
|
+
rmSync,
|
|
4055
|
+
writeFileSync
|
|
4056
|
+
} from "fs";
|
|
4044
4057
|
import { homedir } from "os";
|
|
4058
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "path";
|
|
4059
|
+
import { spawnSync } from "child_process";
|
|
4060
|
+
var INSTALL_MAINTENANCE_MARKER_NAME = ".recordings-install-maintenance";
|
|
4061
|
+
var STORE_READER_LEASES_NAME = ".recordings-store-readers";
|
|
4062
|
+
function stateParent(env = process.env) {
|
|
4063
|
+
const home = env["HOME"] || env["USERPROFILE"] || homedir();
|
|
4064
|
+
return join(home, ".hasna");
|
|
4065
|
+
}
|
|
4066
|
+
function installMaintenanceMarkerPath(env = process.env) {
|
|
4067
|
+
return join(stateParent(env), INSTALL_MAINTENANCE_MARKER_NAME);
|
|
4068
|
+
}
|
|
4069
|
+
function storeReaderLeasesPath(env = process.env) {
|
|
4070
|
+
return join(stateParent(env), STORE_READER_LEASES_NAME);
|
|
4071
|
+
}
|
|
4072
|
+
function pathExists(path) {
|
|
4073
|
+
try {
|
|
4074
|
+
lstatSync(path);
|
|
4075
|
+
return true;
|
|
4076
|
+
} catch (error) {
|
|
4077
|
+
if (error.code === "ENOENT")
|
|
4078
|
+
return false;
|
|
4079
|
+
throw error;
|
|
4080
|
+
}
|
|
4081
|
+
}
|
|
4082
|
+
function assertPrivateDirectory(path) {
|
|
4083
|
+
const details = lstatSync(path);
|
|
4084
|
+
if (!details.isDirectory() || details.isSymbolicLink()) {
|
|
4085
|
+
throw new Error(`Local Recordings coordination path is not a secure directory: ${path}`);
|
|
4086
|
+
}
|
|
4087
|
+
if (typeof process.getuid === "function" && details.uid !== process.getuid()) {
|
|
4088
|
+
throw new Error(`Local Recordings coordination path has an unexpected owner: ${path}`);
|
|
4089
|
+
}
|
|
4090
|
+
if ((details.mode & 18) !== 0) {
|
|
4091
|
+
throw new Error(`Local Recordings coordination path is group/world writable: ${path}`);
|
|
4092
|
+
}
|
|
4093
|
+
}
|
|
4094
|
+
function ensureReaderRoot(env) {
|
|
4095
|
+
const parent = stateParent(env);
|
|
4096
|
+
const readerRoot = storeReaderLeasesPath(env);
|
|
4097
|
+
mkdirSync(parent, { recursive: true, mode: 448 });
|
|
4098
|
+
assertPrivateDirectory(parent);
|
|
4099
|
+
try {
|
|
4100
|
+
mkdirSync(readerRoot, { mode: 448 });
|
|
4101
|
+
} catch (error) {
|
|
4102
|
+
if (error.code !== "EEXIST")
|
|
4103
|
+
throw error;
|
|
4104
|
+
}
|
|
4105
|
+
assertPrivateDirectory(readerRoot);
|
|
4106
|
+
return readerRoot;
|
|
4107
|
+
}
|
|
4108
|
+
function processStartIdentity() {
|
|
4109
|
+
const result = spawnSync("/bin/ps", ["-o", "lstart=", "-p", String(process.pid)], {
|
|
4110
|
+
encoding: "utf8",
|
|
4111
|
+
env: { ...process.env, LC_ALL: "C", LANG: "C", TZ: "UTC0" }
|
|
4112
|
+
});
|
|
4113
|
+
const identity = result.status === 0 ? result.stdout.trim().replace(/\s+/g, " ") : "";
|
|
4114
|
+
if (!identity) {
|
|
4115
|
+
throw new Error("Could not establish the local Store reader process identity");
|
|
4116
|
+
}
|
|
4117
|
+
return identity;
|
|
4118
|
+
}
|
|
4119
|
+
function maintenanceUnavailable() {
|
|
4120
|
+
return new Error("Local Recordings storage is temporarily unavailable during app installation maintenance");
|
|
4121
|
+
}
|
|
4122
|
+
function assertMaintenanceAbsent(env) {
|
|
4123
|
+
if (pathExists(installMaintenanceMarkerPath(env)))
|
|
4124
|
+
throw maintenanceUnavailable();
|
|
4125
|
+
}
|
|
4126
|
+
function canonicalPotentialPath(path) {
|
|
4127
|
+
const absolutePath = resolve(path);
|
|
4128
|
+
let existingPath = absolutePath;
|
|
4129
|
+
const missingEntries = [];
|
|
4130
|
+
while (true) {
|
|
4131
|
+
try {
|
|
4132
|
+
return join(realpathSync(existingPath), ...missingEntries);
|
|
4133
|
+
} catch (error) {
|
|
4134
|
+
if (error.code !== "ENOENT")
|
|
4135
|
+
return absolutePath;
|
|
4136
|
+
const parent = dirname(existingPath);
|
|
4137
|
+
if (parent === existingPath)
|
|
4138
|
+
return absolutePath;
|
|
4139
|
+
missingEntries.unshift(basename(existingPath));
|
|
4140
|
+
existingPath = parent;
|
|
4141
|
+
}
|
|
4142
|
+
}
|
|
4143
|
+
}
|
|
4144
|
+
function pathIsWithin(root, candidate) {
|
|
4145
|
+
const relativePath = relative(root, candidate);
|
|
4146
|
+
return relativePath === "" || relativePath !== ".." && !relativePath.startsWith(`..${sep}`) && !isAbsolute(relativePath);
|
|
4147
|
+
}
|
|
4148
|
+
function isGlobalRecordingsStatePath(path, env = process.env) {
|
|
4149
|
+
const globalStateRoot = join(stateParent(env), "recordings");
|
|
4150
|
+
return pathIsWithin(resolve(globalStateRoot), resolve(path)) || pathIsWithin(canonicalPotentialPath(globalStateRoot), canonicalPotentialPath(path));
|
|
4151
|
+
}
|
|
4152
|
+
function removeActiveLease(activeLease, releasedLease) {
|
|
4153
|
+
try {
|
|
4154
|
+
renameSync(activeLease, releasedLease);
|
|
4155
|
+
} catch (error) {
|
|
4156
|
+
if (error.code !== "ENOENT")
|
|
4157
|
+
throw error;
|
|
4158
|
+
}
|
|
4159
|
+
rmSync(releasedLease, { recursive: true, force: true });
|
|
4160
|
+
}
|
|
4161
|
+
function acquireLocalStoreReaderLease(env = process.env) {
|
|
4162
|
+
assertMaintenanceAbsent(env);
|
|
4163
|
+
const readerRoot = ensureReaderRoot(env);
|
|
4164
|
+
const nonce = randomUUID();
|
|
4165
|
+
const pendingLease = join(readerRoot, `.pending-${process.pid}-${nonce}`);
|
|
4166
|
+
const activeLease = join(readerRoot, `lease-${process.pid}-${nonce}`);
|
|
4167
|
+
const releasedLease = join(readerRoot, `.released-${process.pid}-${nonce}`);
|
|
4168
|
+
let active = false;
|
|
4169
|
+
try {
|
|
4170
|
+
mkdirSync(pendingLease, { mode: 448 });
|
|
4171
|
+
const owner = join(pendingLease, "owner");
|
|
4172
|
+
writeFileSync(owner, `${process.pid}
|
|
4173
|
+
${processStartIdentity()}
|
|
4174
|
+
`, {
|
|
4175
|
+
encoding: "utf8",
|
|
4176
|
+
flag: "wx",
|
|
4177
|
+
mode: 384
|
|
4178
|
+
});
|
|
4179
|
+
chmodSync(owner, 384);
|
|
4180
|
+
renameSync(pendingLease, activeLease);
|
|
4181
|
+
active = true;
|
|
4182
|
+
assertMaintenanceAbsent(env);
|
|
4183
|
+
} catch (error) {
|
|
4184
|
+
if (active)
|
|
4185
|
+
removeActiveLease(activeLease, releasedLease);
|
|
4186
|
+
else
|
|
4187
|
+
rmSync(pendingLease, { recursive: true, force: true });
|
|
4188
|
+
throw error;
|
|
4189
|
+
}
|
|
4190
|
+
let released = false;
|
|
4191
|
+
return () => {
|
|
4192
|
+
if (released)
|
|
4193
|
+
return;
|
|
4194
|
+
removeActiveLease(activeLease, releasedLease);
|
|
4195
|
+
released = true;
|
|
4196
|
+
};
|
|
4197
|
+
}
|
|
4198
|
+
async function withLocalStoreReaderLease(operation, env = process.env) {
|
|
4199
|
+
const release = acquireLocalStoreReaderLease(env);
|
|
4200
|
+
try {
|
|
4201
|
+
return await operation();
|
|
4202
|
+
} finally {
|
|
4203
|
+
release();
|
|
4204
|
+
}
|
|
4205
|
+
}
|
|
4206
|
+
|
|
4207
|
+
// src/lib/config.ts
|
|
4208
|
+
var POST_PROCESSING_MODES = new Set([
|
|
4209
|
+
"off",
|
|
4210
|
+
"auto",
|
|
4211
|
+
"always"
|
|
4212
|
+
]);
|
|
4213
|
+
var DEFAULT_TRANSCRIPTION_MODEL = "gpt-4o-transcribe";
|
|
4214
|
+
var DEFAULT_REALTIME_SESSION_MODEL = "gpt-realtime";
|
|
4215
|
+
var DEFAULT_REALTIME_TRANSCRIPTION_MODEL = "gpt-realtime-whisper";
|
|
4045
4216
|
var DEFAULT_CONFIG = {
|
|
4046
4217
|
openai_api_key: "",
|
|
4047
4218
|
enhancement_api_key: "",
|
|
4048
|
-
transcription_model:
|
|
4219
|
+
transcription_model: DEFAULT_TRANSCRIPTION_MODEL,
|
|
4220
|
+
realtime_session_model: DEFAULT_REALTIME_SESSION_MODEL,
|
|
4221
|
+
realtime_transcription_model: DEFAULT_REALTIME_TRANSCRIPTION_MODEL,
|
|
4049
4222
|
enhancement_model: "gpt-4o",
|
|
4223
|
+
transcriber_model: "gpt-4o",
|
|
4050
4224
|
language: "en",
|
|
4051
4225
|
audio_format: "wav",
|
|
4052
4226
|
sample_rate: 16000,
|
|
4053
4227
|
record_command: "sox",
|
|
4054
4228
|
hotkey: "space",
|
|
4229
|
+
transcription_prompt: "",
|
|
4230
|
+
transcriber_prompt: "",
|
|
4231
|
+
post_processing_mode: "auto",
|
|
4055
4232
|
auto_enhance: true,
|
|
4056
4233
|
enhance_triggers: [
|
|
4057
4234
|
"say it better",
|
|
@@ -4068,17 +4245,23 @@ var DEFAULT_CONFIG = {
|
|
|
4068
4245
|
keyword_transforms: {},
|
|
4069
4246
|
db_path: "",
|
|
4070
4247
|
audio_dir: "",
|
|
4071
|
-
max_recording_seconds: 1800
|
|
4248
|
+
max_recording_seconds: 1800,
|
|
4249
|
+
config_warnings: []
|
|
4072
4250
|
};
|
|
4073
4251
|
function loadConfig(configPath) {
|
|
4074
4252
|
const config = { ...DEFAULT_CONFIG };
|
|
4075
|
-
|
|
4253
|
+
config.config_warnings = [];
|
|
4254
|
+
let explicitPostProcessingMode = false;
|
|
4255
|
+
let explicitTranscriberModel = false;
|
|
4256
|
+
const filePath = configPath || findConfigFile() || join2(getDataDir(), "config.json");
|
|
4076
4257
|
let fileProvidedOpenAIKey = false;
|
|
4077
4258
|
if (existsSync(filePath)) {
|
|
4078
4259
|
try {
|
|
4079
4260
|
const raw = readFileSync(filePath, "utf-8");
|
|
4080
4261
|
const fileConfig = JSON.parse(raw);
|
|
4081
4262
|
const expanded = expandEnvBackedConfig(fileConfig);
|
|
4263
|
+
explicitPostProcessingMode = typeof fileConfig.post_processing_mode === "string";
|
|
4264
|
+
explicitTranscriberModel = typeof fileConfig.transcriber_model === "string";
|
|
4082
4265
|
Object.assign(config, expanded);
|
|
4083
4266
|
fileProvidedOpenAIKey = typeof expanded.openai_api_key === "string" && expanded.openai_api_key.length > 0;
|
|
4084
4267
|
} catch {}
|
|
@@ -4095,12 +4278,35 @@ function loadConfig(configPath) {
|
|
|
4095
4278
|
if (process.env.RECORDINGS_MODEL) {
|
|
4096
4279
|
config.transcription_model = process.env.RECORDINGS_MODEL;
|
|
4097
4280
|
}
|
|
4281
|
+
if (process.env.RECORDINGS_REALTIME_SESSION_MODEL) {
|
|
4282
|
+
config.realtime_session_model = process.env.RECORDINGS_REALTIME_SESSION_MODEL;
|
|
4283
|
+
}
|
|
4284
|
+
if (process.env.RECORDINGS_REALTIME_TRANSCRIPTION_MODEL) {
|
|
4285
|
+
config.realtime_transcription_model = process.env.RECORDINGS_REALTIME_TRANSCRIPTION_MODEL;
|
|
4286
|
+
}
|
|
4098
4287
|
if (process.env.RECORDINGS_ENHANCEMENT_MODEL) {
|
|
4099
4288
|
config.enhancement_model = process.env.RECORDINGS_ENHANCEMENT_MODEL;
|
|
4100
4289
|
}
|
|
4290
|
+
if (process.env.RECORDINGS_TRANSCRIBER_MODEL) {
|
|
4291
|
+
config.transcriber_model = process.env.RECORDINGS_TRANSCRIBER_MODEL;
|
|
4292
|
+
explicitTranscriberModel = true;
|
|
4293
|
+
}
|
|
4101
4294
|
if (process.env.RECORDINGS_LANGUAGE) {
|
|
4102
4295
|
config.language = process.env.RECORDINGS_LANGUAGE;
|
|
4103
4296
|
}
|
|
4297
|
+
if (process.env.RECORDINGS_TRANSCRIPTION_PROMPT) {
|
|
4298
|
+
config.transcription_prompt = process.env.RECORDINGS_TRANSCRIPTION_PROMPT;
|
|
4299
|
+
}
|
|
4300
|
+
if (process.env.RECORDINGS_TRANSCRIBER_PROMPT) {
|
|
4301
|
+
config.transcriber_prompt = process.env.RECORDINGS_TRANSCRIBER_PROMPT;
|
|
4302
|
+
}
|
|
4303
|
+
if (process.env.RECORDINGS_POST_PROCESSING_MODE) {
|
|
4304
|
+
config.post_processing_mode = normalizePostProcessingMode(process.env.RECORDINGS_POST_PROCESSING_MODE, config.post_processing_mode ?? "auto");
|
|
4305
|
+
explicitPostProcessingMode = true;
|
|
4306
|
+
}
|
|
4307
|
+
if (process.env.RECORDINGS_AUTO_ENHANCE) {
|
|
4308
|
+
config.auto_enhance = parseBooleanEnv(process.env.RECORDINGS_AUTO_ENHANCE, config.auto_enhance);
|
|
4309
|
+
}
|
|
4104
4310
|
if (process.env.HASNA_RECORDINGS_DB_PATH) {
|
|
4105
4311
|
config.db_path = process.env.HASNA_RECORDINGS_DB_PATH;
|
|
4106
4312
|
} else if (process.env.RECORDINGS_DB_PATH) {
|
|
@@ -4118,14 +4324,86 @@ function loadConfig(configPath) {
|
|
|
4118
4324
|
if (!config.enhancement_api_key) {
|
|
4119
4325
|
config.enhancement_api_key = config.openai_api_key || loadSecretKey("OPENAI_API_KEY");
|
|
4120
4326
|
}
|
|
4327
|
+
if (!explicitTranscriberModel) {
|
|
4328
|
+
config.transcriber_model = config.enhancement_model;
|
|
4329
|
+
}
|
|
4330
|
+
normalizeModelSlots(config);
|
|
4331
|
+
normalizePostProcessingConfig(config, explicitPostProcessingMode);
|
|
4121
4332
|
if (!config.db_path) {
|
|
4122
|
-
config.db_path =
|
|
4333
|
+
config.db_path = join2(getDataDir(), "recordings.db");
|
|
4123
4334
|
}
|
|
4124
4335
|
if (!config.audio_dir) {
|
|
4125
|
-
config.audio_dir =
|
|
4336
|
+
config.audio_dir = join2(getDataDir(), "audio");
|
|
4337
|
+
}
|
|
4338
|
+
return config;
|
|
4339
|
+
}
|
|
4340
|
+
function normalizeModelSlots(config) {
|
|
4341
|
+
const warnings = config.config_warnings ?? [];
|
|
4342
|
+
config.config_warnings = warnings;
|
|
4343
|
+
const boundedModel = config.transcription_model?.trim() || DEFAULT_TRANSCRIPTION_MODEL;
|
|
4344
|
+
if (isRealtimeOnlyModel(boundedModel)) {
|
|
4345
|
+
warnings.push(`Ignoring RECORDINGS_MODEL=${boundedModel}; bounded transcription uses ${DEFAULT_TRANSCRIPTION_MODEL}.`);
|
|
4346
|
+
config.transcription_model = DEFAULT_TRANSCRIPTION_MODEL;
|
|
4347
|
+
} else {
|
|
4348
|
+
config.transcription_model = boundedModel;
|
|
4349
|
+
}
|
|
4350
|
+
const realtimeSessionModel = config.realtime_session_model?.trim() || DEFAULT_REALTIME_SESSION_MODEL;
|
|
4351
|
+
if (isTranscriptionOnlyModel(realtimeSessionModel)) {
|
|
4352
|
+
warnings.push(`Ignoring realtime session model ${realtimeSessionModel}; use ${DEFAULT_REALTIME_TRANSCRIPTION_MODEL} as realtime_transcription_model instead.`);
|
|
4353
|
+
config.realtime_session_model = DEFAULT_REALTIME_SESSION_MODEL;
|
|
4354
|
+
} else {
|
|
4355
|
+
config.realtime_session_model = realtimeSessionModel;
|
|
4356
|
+
}
|
|
4357
|
+
const realtimeTranscriptionModel = config.realtime_transcription_model?.trim() || DEFAULT_REALTIME_TRANSCRIPTION_MODEL;
|
|
4358
|
+
if (!isRealtimeTranscriptionModel(realtimeTranscriptionModel)) {
|
|
4359
|
+
warnings.push(`Ignoring realtime transcription model ${realtimeTranscriptionModel}; realtime transcription uses ${DEFAULT_REALTIME_TRANSCRIPTION_MODEL}.`);
|
|
4360
|
+
config.realtime_transcription_model = DEFAULT_REALTIME_TRANSCRIPTION_MODEL;
|
|
4361
|
+
} else {
|
|
4362
|
+
config.realtime_transcription_model = realtimeTranscriptionModel;
|
|
4363
|
+
}
|
|
4364
|
+
return config;
|
|
4365
|
+
}
|
|
4366
|
+
function isTranscriptionOnlyModel(model) {
|
|
4367
|
+
const m = model.trim().toLowerCase();
|
|
4368
|
+
return m === "whisper-1" || m === DEFAULT_REALTIME_TRANSCRIPTION_MODEL || m.includes("transcribe");
|
|
4369
|
+
}
|
|
4370
|
+
function isRealtimeOnlyModel(model) {
|
|
4371
|
+
const m = model.trim().toLowerCase();
|
|
4372
|
+
return m.startsWith("gpt-realtime");
|
|
4373
|
+
}
|
|
4374
|
+
function isRealtimeTranscriptionModel(model) {
|
|
4375
|
+
const m = model.trim().toLowerCase();
|
|
4376
|
+
return m === DEFAULT_REALTIME_TRANSCRIPTION_MODEL || m.startsWith("gpt-realtime") && m.includes("whisper");
|
|
4377
|
+
}
|
|
4378
|
+
function normalizePostProcessingMode(value, fallback = "auto") {
|
|
4379
|
+
const mode = value?.trim().toLowerCase();
|
|
4380
|
+
if (mode && POST_PROCESSING_MODES.has(mode)) {
|
|
4381
|
+
return mode;
|
|
4382
|
+
}
|
|
4383
|
+
return fallback;
|
|
4384
|
+
}
|
|
4385
|
+
function normalizePostProcessingConfig(config, preferPostProcessingMode = true) {
|
|
4386
|
+
if (preferPostProcessingMode) {
|
|
4387
|
+
config.post_processing_mode = normalizePostProcessingMode(config.post_processing_mode, "auto");
|
|
4388
|
+
config.auto_enhance = config.post_processing_mode !== "off";
|
|
4389
|
+
return config;
|
|
4390
|
+
}
|
|
4391
|
+
if (config.auto_enhance === false) {
|
|
4392
|
+
config.post_processing_mode = "off";
|
|
4393
|
+
} else {
|
|
4394
|
+
config.post_processing_mode = normalizePostProcessingMode(config.post_processing_mode, "auto");
|
|
4126
4395
|
}
|
|
4396
|
+
config.auto_enhance = config.post_processing_mode !== "off";
|
|
4127
4397
|
return config;
|
|
4128
4398
|
}
|
|
4399
|
+
function parseBooleanEnv(value, fallback) {
|
|
4400
|
+
const normalized = value.trim().toLowerCase();
|
|
4401
|
+
if (["1", "true", "yes", "on"].includes(normalized))
|
|
4402
|
+
return true;
|
|
4403
|
+
if (["0", "false", "no", "off"].includes(normalized))
|
|
4404
|
+
return false;
|
|
4405
|
+
return fallback;
|
|
4406
|
+
}
|
|
4129
4407
|
function expandEnvBackedConfig(config) {
|
|
4130
4408
|
const expanded = { ...config };
|
|
4131
4409
|
for (const key of ["openai_api_key", "enhancement_api_key"]) {
|
|
@@ -4144,36 +4422,68 @@ function getDataDir() {
|
|
|
4144
4422
|
if (projectLocalDir)
|
|
4145
4423
|
return projectLocalDir;
|
|
4146
4424
|
const home = getHomeDir();
|
|
4147
|
-
const newDir =
|
|
4148
|
-
const oldDir =
|
|
4425
|
+
const newDir = join2(home, ".hasna", "recordings");
|
|
4426
|
+
const oldDir = join2(home, ".recordings");
|
|
4149
4427
|
if (existsSync(oldDir)) {
|
|
4428
|
+
const releaseLease = acquireLocalStoreReaderLease();
|
|
4150
4429
|
try {
|
|
4151
|
-
|
|
4152
|
-
|
|
4430
|
+
try {
|
|
4431
|
+
if (existsSync(oldDir))
|
|
4432
|
+
mergeDirectoryContents(oldDir, newDir);
|
|
4433
|
+
} catch {}
|
|
4434
|
+
} finally {
|
|
4435
|
+
releaseLease();
|
|
4436
|
+
}
|
|
4153
4437
|
}
|
|
4154
4438
|
return newDir;
|
|
4155
4439
|
}
|
|
4156
4440
|
function findProjectRecordingsPath(entry) {
|
|
4157
|
-
const
|
|
4158
|
-
|
|
4441
|
+
const cwd = canonicalExistingPath(process.cwd());
|
|
4442
|
+
const home = canonicalExistingPath(getHomeDir());
|
|
4443
|
+
const repositoryRoot = findRepositoryRoot(cwd);
|
|
4444
|
+
const cwdIsInsideHome = cwd === home || cwd.startsWith(`${home}${sep2}`);
|
|
4445
|
+
const repositoryIsInsideHome = repositoryRoot !== null && repositoryRoot !== home && repositoryRoot.startsWith(`${home}${sep2}`);
|
|
4446
|
+
const boundary = cwdIsInsideHome ? repositoryIsInsideHome ? repositoryRoot : home : repositoryRoot ?? cwd;
|
|
4447
|
+
const excludeBoundary = boundary === home;
|
|
4448
|
+
let dir = cwd;
|
|
4159
4449
|
while (true) {
|
|
4160
|
-
if (dir
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4450
|
+
if (excludeBoundary && dir === boundary)
|
|
4451
|
+
break;
|
|
4452
|
+
const candidate = entry ? join2(dir, ".recordings", entry) : join2(dir, ".recordings");
|
|
4453
|
+
if (existsSync(candidate))
|
|
4454
|
+
return candidate;
|
|
4455
|
+
if (dir === boundary)
|
|
4456
|
+
break;
|
|
4457
|
+
const parent = dirname2(dir);
|
|
4166
4458
|
if (parent === dir)
|
|
4167
4459
|
break;
|
|
4168
4460
|
dir = parent;
|
|
4169
4461
|
}
|
|
4170
4462
|
return null;
|
|
4171
4463
|
}
|
|
4464
|
+
function findRepositoryRoot(start) {
|
|
4465
|
+
let dir = start;
|
|
4466
|
+
while (true) {
|
|
4467
|
+
if (existsSync(join2(dir, ".git")))
|
|
4468
|
+
return dir;
|
|
4469
|
+
const parent = dirname2(dir);
|
|
4470
|
+
if (parent === dir)
|
|
4471
|
+
return null;
|
|
4472
|
+
dir = parent;
|
|
4473
|
+
}
|
|
4474
|
+
}
|
|
4475
|
+
function canonicalExistingPath(path) {
|
|
4476
|
+
try {
|
|
4477
|
+
return realpathSync2(path);
|
|
4478
|
+
} catch {
|
|
4479
|
+
return resolve2(path);
|
|
4480
|
+
}
|
|
4481
|
+
}
|
|
4172
4482
|
function mergeDirectoryContents(sourceDir, targetDir) {
|
|
4173
|
-
|
|
4483
|
+
mkdirSync2(targetDir, { recursive: true });
|
|
4174
4484
|
for (const entry of readdirSync(sourceDir)) {
|
|
4175
|
-
const sourcePath =
|
|
4176
|
-
const targetPath =
|
|
4485
|
+
const sourcePath = join2(sourceDir, entry);
|
|
4486
|
+
const targetPath = join2(targetDir, entry);
|
|
4177
4487
|
const sourceStats = statSync(sourcePath);
|
|
4178
4488
|
if (sourceStats.isDirectory()) {
|
|
4179
4489
|
mergeDirectoryContents(sourcePath, targetPath);
|
|
@@ -4183,7 +4493,7 @@ function mergeDirectoryContents(sourceDir, targetDir) {
|
|
|
4183
4493
|
}
|
|
4184
4494
|
}
|
|
4185
4495
|
function loadSecretKey(keyName) {
|
|
4186
|
-
const secretsPath =
|
|
4496
|
+
const secretsPath = join2(getHomeDir(), ".secrets");
|
|
4187
4497
|
if (!existsSync(secretsPath))
|
|
4188
4498
|
return "";
|
|
4189
4499
|
for (const candidate of listSecretFiles(secretsPath)) {
|
|
@@ -4210,7 +4520,7 @@ function listSecretFiles(path) {
|
|
|
4210
4520
|
if (!stats.isDirectory())
|
|
4211
4521
|
return [];
|
|
4212
4522
|
return readdirSync(path).sort().flatMap((entry) => {
|
|
4213
|
-
const child =
|
|
4523
|
+
const child = join2(path, entry);
|
|
4214
4524
|
try {
|
|
4215
4525
|
const childStats = statSync(child);
|
|
4216
4526
|
if (childStats.isDirectory())
|
|
@@ -4225,14 +4535,19 @@ function listSecretFiles(path) {
|
|
|
4225
4535
|
}
|
|
4226
4536
|
}
|
|
4227
4537
|
function getHomeDir() {
|
|
4228
|
-
return process.env["HOME"] || process.env["USERPROFILE"] ||
|
|
4538
|
+
return process.env["HOME"] || process.env["USERPROFILE"] || homedir2();
|
|
4229
4539
|
}
|
|
4230
4540
|
function ensureDataDir(config) {
|
|
4231
|
-
const { mkdirSync:
|
|
4232
|
-
mkdirSync2(config.audio_dir, { recursive: true });
|
|
4541
|
+
const { mkdirSync: mkdirSync3 } = __require("fs");
|
|
4233
4542
|
const dbDir = config.db_path.substring(0, config.db_path.lastIndexOf("/"));
|
|
4234
|
-
|
|
4235
|
-
|
|
4543
|
+
const releaseLease = [config.audio_dir, dbDir].some((path) => path.length > 0 && isGlobalRecordingsStatePath(path)) ? acquireLocalStoreReaderLease() : () => {};
|
|
4544
|
+
try {
|
|
4545
|
+
mkdirSync3(config.audio_dir, { recursive: true });
|
|
4546
|
+
if (dbDir)
|
|
4547
|
+
mkdirSync3(dbDir, { recursive: true });
|
|
4548
|
+
} finally {
|
|
4549
|
+
releaseLease();
|
|
4550
|
+
}
|
|
4236
4551
|
}
|
|
4237
4552
|
|
|
4238
4553
|
// src/db/sqlite-adapter.ts
|
|
@@ -4267,8 +4582,8 @@ class SqliteAdapter {
|
|
|
4267
4582
|
}
|
|
4268
4583
|
|
|
4269
4584
|
// src/db/database.ts
|
|
4270
|
-
import { mkdirSync as
|
|
4271
|
-
import { dirname as
|
|
4585
|
+
import { mkdirSync as mkdirSync3 } from "fs";
|
|
4586
|
+
import { dirname as dirname3 } from "path";
|
|
4272
4587
|
var _db = null;
|
|
4273
4588
|
var _adapter = null;
|
|
4274
4589
|
var MIGRATIONS = [
|
|
@@ -4354,8 +4669,8 @@ function getDatabase(dbPath) {
|
|
|
4354
4669
|
if (_db)
|
|
4355
4670
|
return _db;
|
|
4356
4671
|
const path = dbPath || loadConfig().db_path;
|
|
4357
|
-
const dir =
|
|
4358
|
-
|
|
4672
|
+
const dir = dirname3(path);
|
|
4673
|
+
mkdirSync3(dir, { recursive: true });
|
|
4359
4674
|
_adapter = new SqliteAdapter(path);
|
|
4360
4675
|
_db = _adapter.raw;
|
|
4361
4676
|
_db.run("PRAGMA busy_timeout = 5000");
|
|
@@ -4411,6 +4726,63 @@ function shortUuid() {
|
|
|
4411
4726
|
return crypto.randomUUID().slice(0, 8);
|
|
4412
4727
|
}
|
|
4413
4728
|
|
|
4729
|
+
// src/db/errors.ts
|
|
4730
|
+
class ProjectNotFoundError extends Error {
|
|
4731
|
+
ref;
|
|
4732
|
+
constructor(ref) {
|
|
4733
|
+
super(`project not found: ${ref}`);
|
|
4734
|
+
this.name = "ProjectNotFoundError";
|
|
4735
|
+
this.ref = ref;
|
|
4736
|
+
}
|
|
4737
|
+
}
|
|
4738
|
+
|
|
4739
|
+
class ValidationError extends Error {
|
|
4740
|
+
constructor(message) {
|
|
4741
|
+
super(message);
|
|
4742
|
+
this.name = "ValidationError";
|
|
4743
|
+
}
|
|
4744
|
+
}
|
|
4745
|
+
|
|
4746
|
+
// src/lib/recording-create-identity.ts
|
|
4747
|
+
var MAX_RECORDING_IDENTITY_LENGTH = 255;
|
|
4748
|
+
function validateIdentityValue(value, label, nullMeansAbsent = false) {
|
|
4749
|
+
if (value === undefined || nullMeansAbsent && value === null)
|
|
4750
|
+
return;
|
|
4751
|
+
if (typeof value !== "string") {
|
|
4752
|
+
throw new ValidationError(`${label} must be a string`);
|
|
4753
|
+
}
|
|
4754
|
+
if (value.length === 0) {
|
|
4755
|
+
throw new ValidationError(`${label} must not be empty`);
|
|
4756
|
+
}
|
|
4757
|
+
if (value.length > MAX_RECORDING_IDENTITY_LENGTH) {
|
|
4758
|
+
throw new ValidationError(`${label} must not exceed ${MAX_RECORDING_IDENTITY_LENGTH} characters`);
|
|
4759
|
+
}
|
|
4760
|
+
if (value !== value.trim()) {
|
|
4761
|
+
throw new ValidationError(`${label} must not contain leading or trailing whitespace`);
|
|
4762
|
+
}
|
|
4763
|
+
if (/[\u0000-\u001f\u007f]/.test(value)) {
|
|
4764
|
+
throw new ValidationError(`${label} must not contain control characters`);
|
|
4765
|
+
}
|
|
4766
|
+
if (/[^\u0020-\u007e]/.test(value)) {
|
|
4767
|
+
throw new ValidationError(`${label} must contain only printable ASCII characters`);
|
|
4768
|
+
}
|
|
4769
|
+
return value;
|
|
4770
|
+
}
|
|
4771
|
+
function recordingCreateIdentity(input, idempotencyKey, options = {}) {
|
|
4772
|
+
const bodyId = validateIdentityValue(input.id, "recording id", true);
|
|
4773
|
+
const headerKey = validateIdentityValue(idempotencyKey, "idempotency key");
|
|
4774
|
+
if (bodyId !== undefined && headerKey !== undefined && bodyId !== headerKey) {
|
|
4775
|
+
throw new ValidationError("recording id conflicts with idempotency key");
|
|
4776
|
+
}
|
|
4777
|
+
const effectiveKey = headerKey ?? bodyId;
|
|
4778
|
+
const { id: _runtimeId, ...inputWithoutId } = input;
|
|
4779
|
+
const persistedId = options.bindIdempotencyKeyToId === false ? bodyId : effectiveKey;
|
|
4780
|
+
return {
|
|
4781
|
+
input: persistedId === undefined ? inputWithoutId : { ...inputWithoutId, id: persistedId },
|
|
4782
|
+
idempotencyKey: effectiveKey
|
|
4783
|
+
};
|
|
4784
|
+
}
|
|
4785
|
+
|
|
4414
4786
|
// src/db/recordings.ts
|
|
4415
4787
|
function parseRow(row) {
|
|
4416
4788
|
return {
|
|
@@ -4435,20 +4807,36 @@ function parseRow(row) {
|
|
|
4435
4807
|
created_at: row["created_at"]
|
|
4436
4808
|
};
|
|
4437
4809
|
}
|
|
4438
|
-
function createRecording(input, db) {
|
|
4810
|
+
function createRecording(input, db, idempotencyKey) {
|
|
4439
4811
|
const d = db || getDatabase();
|
|
4440
|
-
|
|
4441
|
-
const
|
|
4442
|
-
const
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
|
|
4448
|
-
|
|
4812
|
+
input = recordingCreateIdentity(input, idempotencyKey).input;
|
|
4813
|
+
const id = input.id || shortUuid();
|
|
4814
|
+
const create = d.transaction(() => {
|
|
4815
|
+
if (input.id) {
|
|
4816
|
+
const existing = getRecording(input.id, d);
|
|
4817
|
+
if (existing?.id === input.id)
|
|
4818
|
+
return existing;
|
|
4819
|
+
}
|
|
4820
|
+
const tagsJson = JSON.stringify(input.tags || []);
|
|
4821
|
+
const metadataJson = JSON.stringify(input.metadata || {});
|
|
4822
|
+
const insertResult = d.query(`INSERT INTO recordings (id, audio_path, raw_text, processed_text, processing_mode, model_used, enhancement_model, duration_ms, language, tags, agent_id, project_id, session_id, goal, role, task_list_id, machine_id, metadata)
|
|
4823
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
4824
|
+
ON CONFLICT(id) DO NOTHING`).run(id, input.audio_path || null, input.raw_text, input.processed_text || null, input.processing_mode || "raw", input.model_used || "gpt-4o-transcribe", input.enhancement_model || null, input.duration_ms || 0, input.language || null, tagsJson, input.agent_id || null, input.project_id || null, input.session_id || null, input.goal || null, input.role || null, input.task_list_id || null, input.machine_id || null, metadataJson);
|
|
4825
|
+
if (insertResult.changes === 0) {
|
|
4826
|
+
const existing = getRecording(id, d);
|
|
4827
|
+
if (existing?.id === id)
|
|
4828
|
+
return existing;
|
|
4829
|
+
throw new Error("recording id conflict could not be read back");
|
|
4830
|
+
}
|
|
4831
|
+
if (input.tags && input.tags.length > 0) {
|
|
4832
|
+
const insertTag = d.query("INSERT OR IGNORE INTO recording_tags (recording_id, tag) VALUES (?, ?)");
|
|
4833
|
+
for (const tag of input.tags) {
|
|
4834
|
+
insertTag.run(id, tag);
|
|
4835
|
+
}
|
|
4449
4836
|
}
|
|
4450
|
-
|
|
4451
|
-
|
|
4837
|
+
return getRecording(id, d);
|
|
4838
|
+
});
|
|
4839
|
+
return create();
|
|
4452
4840
|
}
|
|
4453
4841
|
function getRecording(id, db) {
|
|
4454
4842
|
const d = db || getDatabase();
|
|
@@ -4460,6 +4848,20 @@ function getRecording(id, db) {
|
|
|
4460
4848
|
}
|
|
4461
4849
|
function listRecordings(filter, db) {
|
|
4462
4850
|
const d = db || getDatabase();
|
|
4851
|
+
const { where, params } = buildRecordingWhere(filter);
|
|
4852
|
+
const limit = filter?.limit || 50;
|
|
4853
|
+
const offset = filter?.offset || 0;
|
|
4854
|
+
const sql = `SELECT * FROM recordings ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`;
|
|
4855
|
+
const rows = d.query(sql).all(...params, limit, offset);
|
|
4856
|
+
return rows.map(parseRow);
|
|
4857
|
+
}
|
|
4858
|
+
function countRecordings(filter, db) {
|
|
4859
|
+
const d = db || getDatabase();
|
|
4860
|
+
const { where, params } = buildRecordingWhere(filter);
|
|
4861
|
+
const row = d.query(`SELECT COUNT(*) as c FROM recordings ${where}`).get(...params);
|
|
4862
|
+
return row.c;
|
|
4863
|
+
}
|
|
4864
|
+
function buildRecordingWhere(filter) {
|
|
4463
4865
|
const conditions = [];
|
|
4464
4866
|
const params = [];
|
|
4465
4867
|
if (filter?.agent_id) {
|
|
@@ -4498,12 +4900,7 @@ function listRecordings(filter, db) {
|
|
|
4498
4900
|
params.push(filter.until);
|
|
4499
4901
|
}
|
|
4500
4902
|
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
4501
|
-
|
|
4502
|
-
const offset = filter?.offset || 0;
|
|
4503
|
-
const sql = `SELECT * FROM recordings ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`;
|
|
4504
|
-
params.push(limit, offset);
|
|
4505
|
-
const rows = d.query(sql).all(...params);
|
|
4506
|
-
return rows.map(parseRow);
|
|
4903
|
+
return { where, params };
|
|
4507
4904
|
}
|
|
4508
4905
|
function deleteRecording(id, db) {
|
|
4509
4906
|
const d = db || getDatabase();
|
|
@@ -4576,16 +4973,6 @@ function listProjects(db) {
|
|
|
4576
4973
|
return rows.map(parseProject);
|
|
4577
4974
|
}
|
|
4578
4975
|
|
|
4579
|
-
// src/db/errors.ts
|
|
4580
|
-
class ProjectNotFoundError extends Error {
|
|
4581
|
-
ref;
|
|
4582
|
-
constructor(ref) {
|
|
4583
|
-
super(`project not found: ${ref}`);
|
|
4584
|
-
this.name = "ProjectNotFoundError";
|
|
4585
|
-
this.ref = ref;
|
|
4586
|
-
}
|
|
4587
|
-
}
|
|
4588
|
-
|
|
4589
4976
|
// src/db/agents.ts
|
|
4590
4977
|
function parseAgent(row) {
|
|
4591
4978
|
return {
|
|
@@ -4651,7 +5038,7 @@ function setAgentFocus(idOrName, projectId, db) {
|
|
|
4651
5038
|
}
|
|
4652
5039
|
|
|
4653
5040
|
// src/version.ts
|
|
4654
|
-
var VERSION = "0.2.
|
|
5041
|
+
var VERSION = "0.2.13";
|
|
4655
5042
|
|
|
4656
5043
|
// src/db/feedback.ts
|
|
4657
5044
|
function saveFeedback(input) {
|
|
@@ -4944,6 +5331,7 @@ function resolveStorageClient(name, env = process.env, fetchImpl) {
|
|
|
4944
5331
|
}
|
|
4945
5332
|
|
|
4946
5333
|
// src/store.ts
|
|
5334
|
+
import { createHash, randomUUID as randomUUID2 } from "crypto";
|
|
4947
5335
|
var APP = "recordings";
|
|
4948
5336
|
function listQuery(filter) {
|
|
4949
5337
|
if (!filter)
|
|
@@ -4953,6 +5341,7 @@ function listQuery(filter) {
|
|
|
4953
5341
|
project_id: filter.project_id,
|
|
4954
5342
|
session_id: filter.session_id,
|
|
4955
5343
|
processing_mode: filter.processing_mode,
|
|
5344
|
+
tags: filter.tags,
|
|
4956
5345
|
search: filter.search,
|
|
4957
5346
|
since: filter.since,
|
|
4958
5347
|
until: filter.until,
|
|
@@ -4969,58 +5358,63 @@ function unwrap(res, key) {
|
|
|
4969
5358
|
var localStore = {
|
|
4970
5359
|
mode: "local",
|
|
4971
5360
|
baseUrl: null,
|
|
4972
|
-
async createRecording(input) {
|
|
4973
|
-
return createRecording(input);
|
|
5361
|
+
async createRecording(input, idempotencyKey) {
|
|
5362
|
+
return withLocalStoreReaderLease(() => createRecording(input, undefined, idempotencyKey));
|
|
4974
5363
|
},
|
|
4975
5364
|
async getRecording(id) {
|
|
4976
|
-
return getRecording(id);
|
|
5365
|
+
return withLocalStoreReaderLease(() => getRecording(id));
|
|
4977
5366
|
},
|
|
4978
5367
|
async listRecordings(filter) {
|
|
4979
|
-
return listRecordings(filter);
|
|
5368
|
+
return withLocalStoreReaderLease(() => listRecordings(filter));
|
|
5369
|
+
},
|
|
5370
|
+
async countRecordings(filter) {
|
|
5371
|
+
return withLocalStoreReaderLease(() => countRecordings(filter));
|
|
4980
5372
|
},
|
|
4981
5373
|
async searchRecordings(query, filter) {
|
|
4982
|
-
return searchRecordings(query, filter);
|
|
5374
|
+
return withLocalStoreReaderLease(() => searchRecordings(query, filter));
|
|
4983
5375
|
},
|
|
4984
5376
|
async deleteRecording(id) {
|
|
4985
|
-
return deleteRecording(id);
|
|
5377
|
+
return withLocalStoreReaderLease(() => deleteRecording(id));
|
|
4986
5378
|
},
|
|
4987
5379
|
async getRecordingStats() {
|
|
4988
|
-
return getRecordingStats();
|
|
5380
|
+
return withLocalStoreReaderLease(() => getRecordingStats());
|
|
4989
5381
|
},
|
|
4990
5382
|
async registerAgent(name, description, role) {
|
|
4991
|
-
return registerAgent(name, description, role);
|
|
5383
|
+
return withLocalStoreReaderLease(() => registerAgent(name, description, role));
|
|
4992
5384
|
},
|
|
4993
5385
|
async getAgent(idOrName) {
|
|
4994
|
-
return getAgent(idOrName);
|
|
5386
|
+
return withLocalStoreReaderLease(() => getAgent(idOrName));
|
|
4995
5387
|
},
|
|
4996
5388
|
async listAgents() {
|
|
4997
|
-
return listAgents();
|
|
5389
|
+
return withLocalStoreReaderLease(() => listAgents());
|
|
4998
5390
|
},
|
|
4999
5391
|
async heartbeatAgent(idOrName) {
|
|
5000
|
-
return heartbeatAgent(idOrName);
|
|
5392
|
+
return withLocalStoreReaderLease(() => heartbeatAgent(idOrName));
|
|
5001
5393
|
},
|
|
5002
5394
|
async setAgentFocus(idOrName, projectId) {
|
|
5003
|
-
return setAgentFocus(idOrName, projectId);
|
|
5395
|
+
return withLocalStoreReaderLease(() => setAgentFocus(idOrName, projectId));
|
|
5004
5396
|
},
|
|
5005
5397
|
async registerProject(name, path, description) {
|
|
5006
|
-
return registerProject(name, path, description);
|
|
5398
|
+
return withLocalStoreReaderLease(() => registerProject(name, path, description));
|
|
5007
5399
|
},
|
|
5008
5400
|
async getProject(idOrPath) {
|
|
5009
|
-
return getProject(idOrPath);
|
|
5401
|
+
return withLocalStoreReaderLease(() => getProject(idOrPath));
|
|
5010
5402
|
},
|
|
5011
5403
|
async listProjects() {
|
|
5012
|
-
return listProjects();
|
|
5404
|
+
return withLocalStoreReaderLease(() => listProjects());
|
|
5013
5405
|
},
|
|
5014
5406
|
async saveFeedback(input) {
|
|
5015
|
-
saveFeedback(input);
|
|
5407
|
+
await withLocalStoreReaderLease(() => saveFeedback(input));
|
|
5016
5408
|
}
|
|
5017
5409
|
};
|
|
5018
5410
|
function apiStore(client) {
|
|
5019
5411
|
return {
|
|
5020
5412
|
mode: "cloud-http",
|
|
5021
5413
|
baseUrl: client.baseUrl,
|
|
5022
|
-
async createRecording(input) {
|
|
5023
|
-
const
|
|
5414
|
+
async createRecording(input, idempotencyKey) {
|
|
5415
|
+
const keyCandidate = idempotencyKey === undefined && (input.id === undefined || input.id === null) ? randomUUID2() : idempotencyKey;
|
|
5416
|
+
const identity = recordingCreateIdentity(input, keyCandidate, { bindIdempotencyKeyToId: false });
|
|
5417
|
+
const res = await client.create("recordings", identity.input, identity.idempotencyKey);
|
|
5024
5418
|
return unwrap(res, "recording");
|
|
5025
5419
|
},
|
|
5026
5420
|
async getRecording(id) {
|
|
@@ -5031,6 +5425,36 @@ function apiStore(client) {
|
|
|
5031
5425
|
const { items } = await client.list("recordings", listQuery(filter));
|
|
5032
5426
|
return items;
|
|
5033
5427
|
},
|
|
5428
|
+
async countRecordings(filter) {
|
|
5429
|
+
const pageLimit = 500;
|
|
5430
|
+
const maxPageRequests = 1e4;
|
|
5431
|
+
let offset = 0;
|
|
5432
|
+
let pageRequests = 0;
|
|
5433
|
+
const seenPageKeys = new Set;
|
|
5434
|
+
while (pageRequests < maxPageRequests) {
|
|
5435
|
+
pageRequests += 1;
|
|
5436
|
+
const { items, raw } = await client.list("recordings", {
|
|
5437
|
+
...listQuery(filter),
|
|
5438
|
+
limit: pageLimit,
|
|
5439
|
+
offset
|
|
5440
|
+
});
|
|
5441
|
+
const count = raw && typeof raw === "object" ? raw.count : undefined;
|
|
5442
|
+
if (typeof count !== "number" || !Number.isFinite(count)) {
|
|
5443
|
+
throw new Error("Recordings API response is missing a valid count");
|
|
5444
|
+
}
|
|
5445
|
+
if (count > items.length)
|
|
5446
|
+
return count;
|
|
5447
|
+
if (items.length === 0)
|
|
5448
|
+
return offset;
|
|
5449
|
+
offset += items.length;
|
|
5450
|
+
const pageKey = recordingPageFingerprint(items);
|
|
5451
|
+
if (seenPageKeys.has(pageKey)) {
|
|
5452
|
+
throw new Error("Recordings API ignored pagination while counting legacy results");
|
|
5453
|
+
}
|
|
5454
|
+
seenPageKeys.add(pageKey);
|
|
5455
|
+
}
|
|
5456
|
+
throw new Error(`Recordings API exceeded ${maxPageRequests} pages while counting legacy results`);
|
|
5457
|
+
},
|
|
5034
5458
|
async searchRecordings(query, filter) {
|
|
5035
5459
|
const { items } = await client.list("recordings", listQuery({ ...filter ?? {}, search: query }));
|
|
5036
5460
|
return items;
|
|
@@ -5112,6 +5536,42 @@ function apiStore(client) {
|
|
|
5112
5536
|
}
|
|
5113
5537
|
};
|
|
5114
5538
|
}
|
|
5539
|
+
function recordingPageFingerprint(items) {
|
|
5540
|
+
const hash = createHash("sha256");
|
|
5541
|
+
const ids = items.map((item) => String(item.id ?? "")).sort();
|
|
5542
|
+
for (const id of ids) {
|
|
5543
|
+
hash.update(String(id.length));
|
|
5544
|
+
hash.update(":");
|
|
5545
|
+
hash.update(id);
|
|
5546
|
+
hash.update(";");
|
|
5547
|
+
}
|
|
5548
|
+
return `${items.length}:${hash.digest("hex")}`;
|
|
5549
|
+
}
|
|
5550
|
+
async function countStoreRecordings(store, filter) {
|
|
5551
|
+
if (store.countRecordings)
|
|
5552
|
+
return store.countRecordings(filter);
|
|
5553
|
+
const pageLimit = 500;
|
|
5554
|
+
const maxPageRequests = 1e4;
|
|
5555
|
+
const { limit: _limit, offset: _offset, ...unpaginated } = filter ?? {};
|
|
5556
|
+
const seenPageKeys = new Set;
|
|
5557
|
+
let offset = 0;
|
|
5558
|
+
for (let pageRequests = 0;pageRequests < maxPageRequests; pageRequests += 1) {
|
|
5559
|
+
const items = await store.listRecordings({
|
|
5560
|
+
...unpaginated,
|
|
5561
|
+
limit: pageLimit,
|
|
5562
|
+
offset
|
|
5563
|
+
});
|
|
5564
|
+
if (items.length === 0)
|
|
5565
|
+
return offset;
|
|
5566
|
+
const pageKey = recordingPageFingerprint(items);
|
|
5567
|
+
if (seenPageKeys.has(pageKey)) {
|
|
5568
|
+
throw new Error("Legacy Store ignored pagination while counting recordings");
|
|
5569
|
+
}
|
|
5570
|
+
seenPageKeys.add(pageKey);
|
|
5571
|
+
offset += items.length;
|
|
5572
|
+
}
|
|
5573
|
+
throw new Error(`Legacy Store exceeded ${maxPageRequests} pages while counting recordings`);
|
|
5574
|
+
}
|
|
5115
5575
|
var cached = null;
|
|
5116
5576
|
function getStore(env = process.env) {
|
|
5117
5577
|
if (env === process.env && cached)
|
|
@@ -5144,13 +5604,15 @@ class EnhancementError extends Error {
|
|
|
5144
5604
|
|
|
5145
5605
|
// src/lib/transcriber.ts
|
|
5146
5606
|
var _client = null;
|
|
5607
|
+
var _clientApiKey = null;
|
|
5147
5608
|
function getClient(config) {
|
|
5148
|
-
if (_client)
|
|
5149
|
-
return _client;
|
|
5150
5609
|
if (!config.openai_api_key) {
|
|
5151
5610
|
throw new TranscriptionError("OpenAI API key not configured. Set OPENAI_API_KEY env var or add to ~/.secrets");
|
|
5152
5611
|
}
|
|
5612
|
+
if (_client && _clientApiKey === config.openai_api_key)
|
|
5613
|
+
return _client;
|
|
5153
5614
|
_client = new OpenAI({ apiKey: config.openai_api_key });
|
|
5615
|
+
_clientApiKey = config.openai_api_key;
|
|
5154
5616
|
return _client;
|
|
5155
5617
|
}
|
|
5156
5618
|
async function transcribeAudio(audioPath, config, options = {}) {
|
|
@@ -5162,7 +5624,7 @@ async function transcribeAudio(audioPath, config, options = {}) {
|
|
|
5162
5624
|
file: stream,
|
|
5163
5625
|
model: config.transcription_model,
|
|
5164
5626
|
language: config.language || undefined,
|
|
5165
|
-
prompt: buildVerbatimPrompt(options.prompt),
|
|
5627
|
+
prompt: buildVerbatimPrompt(options.prompt ?? config.transcription_prompt),
|
|
5166
5628
|
response_format: "json"
|
|
5167
5629
|
});
|
|
5168
5630
|
stream.destroy();
|
|
@@ -5201,14 +5663,16 @@ ${trimmed}`;
|
|
|
5201
5663
|
// src/lib/enhancer.ts
|
|
5202
5664
|
import OpenAI2 from "openai";
|
|
5203
5665
|
var _enhancementClient = null;
|
|
5666
|
+
var _enhancementClientApiKey = null;
|
|
5204
5667
|
function getEnhancementClient(config) {
|
|
5205
|
-
if (_enhancementClient)
|
|
5206
|
-
return _enhancementClient;
|
|
5207
5668
|
const key = config.enhancement_api_key || config.openai_api_key;
|
|
5208
5669
|
if (!key) {
|
|
5209
5670
|
throw new EnhancementError("API key not configured for enhancement. Set OPENAI_API_KEY or RECORDINGS_ENHANCEMENT_KEY");
|
|
5210
5671
|
}
|
|
5672
|
+
if (_enhancementClient && _enhancementClientApiKey === key)
|
|
5673
|
+
return _enhancementClient;
|
|
5211
5674
|
_enhancementClient = new OpenAI2({ apiKey: key });
|
|
5675
|
+
_enhancementClientApiKey = key;
|
|
5212
5676
|
return _enhancementClient;
|
|
5213
5677
|
}
|
|
5214
5678
|
function needsEnhancement(text, config) {
|
|
@@ -5255,6 +5719,7 @@ function extractInstruction(text, trigger) {
|
|
|
5255
5719
|
}
|
|
5256
5720
|
async function enhanceText(rawText, instruction, config, systemPrompt) {
|
|
5257
5721
|
const client = getEnhancementClient(config);
|
|
5722
|
+
const model = resolveTranscriberModel(config);
|
|
5258
5723
|
let basePrompt = `You are a writing assistant. The user has dictated speech that needs to be transformed into polished output.
|
|
5259
5724
|
|
|
5260
5725
|
Rules:
|
|
@@ -5275,13 +5740,14 @@ Rules:
|
|
|
5275
5740
|
Keyword Transformations:
|
|
5276
5741
|
${transformRules}`;
|
|
5277
5742
|
}
|
|
5278
|
-
const
|
|
5743
|
+
const transcriberPrompt = combinePrompts(config.transcriber_prompt, systemPrompt);
|
|
5744
|
+
const fullPrompt = transcriberPrompt ? `${basePrompt}
|
|
5279
5745
|
|
|
5280
|
-
|
|
5281
|
-
${
|
|
5746
|
+
Transcriber instructions:
|
|
5747
|
+
${transcriberPrompt}` : basePrompt;
|
|
5282
5748
|
try {
|
|
5283
5749
|
const response = await client.chat.completions.create({
|
|
5284
|
-
model
|
|
5750
|
+
model,
|
|
5285
5751
|
messages: [
|
|
5286
5752
|
{
|
|
5287
5753
|
role: "system",
|
|
@@ -5299,7 +5765,7 @@ ${systemPrompt}` : basePrompt;
|
|
|
5299
5765
|
return {
|
|
5300
5766
|
original: rawText,
|
|
5301
5767
|
enhanced,
|
|
5302
|
-
model
|
|
5768
|
+
model,
|
|
5303
5769
|
reasoning: null
|
|
5304
5770
|
};
|
|
5305
5771
|
} catch (error) {
|
|
@@ -5307,27 +5773,77 @@ ${systemPrompt}` : basePrompt;
|
|
|
5307
5773
|
throw new EnhancementError(`Enhancement failed: ${describeTranscriptionFailure(msg)}`);
|
|
5308
5774
|
}
|
|
5309
5775
|
}
|
|
5310
|
-
async function processText(rawText, config,
|
|
5311
|
-
const
|
|
5312
|
-
|
|
5313
|
-
|
|
5776
|
+
async function processText(rawText, config, systemPromptOrOptions, legacyOptions) {
|
|
5777
|
+
const options = {
|
|
5778
|
+
...normalizeProcessTextOptions(systemPromptOrOptions),
|
|
5779
|
+
...legacyOptions
|
|
5780
|
+
};
|
|
5781
|
+
const postProcessingMode = options.force ? "always" : options.postProcessingMode ?? normalizePostProcessingMode(config.post_processing_mode, config.auto_enhance === false ? "off" : "auto");
|
|
5782
|
+
if (postProcessingMode === "off") {
|
|
5783
|
+
return {
|
|
5784
|
+
text: rawText,
|
|
5785
|
+
mode: "raw",
|
|
5786
|
+
enhancement_model: null,
|
|
5787
|
+
post_processing_mode: "off",
|
|
5788
|
+
enhancement_reason: null
|
|
5789
|
+
};
|
|
5314
5790
|
}
|
|
5315
|
-
const detection =
|
|
5316
|
-
|
|
5317
|
-
|
|
5791
|
+
const detection = postProcessingMode === "always" ? {
|
|
5792
|
+
needs: true,
|
|
5793
|
+
reason: "Always-on post-processing",
|
|
5794
|
+
instruction: rawText
|
|
5795
|
+
} : needsEnhancement(rawText, config);
|
|
5796
|
+
if (!detection.needs) {
|
|
5797
|
+
return {
|
|
5798
|
+
text: rawText,
|
|
5799
|
+
mode: "raw",
|
|
5800
|
+
enhancement_model: null,
|
|
5801
|
+
post_processing_mode: postProcessingMode,
|
|
5802
|
+
enhancement_reason: detection.reason
|
|
5803
|
+
};
|
|
5318
5804
|
}
|
|
5319
|
-
const
|
|
5320
|
-
const result = await enhanceText(rawText, instruction, config, systemPrompt);
|
|
5805
|
+
const result = await enhanceText(rawText, detection.instruction, config, options.systemPrompt);
|
|
5321
5806
|
return {
|
|
5322
5807
|
text: result.enhanced,
|
|
5323
5808
|
mode: "enhanced",
|
|
5324
|
-
enhancement_model: result.model
|
|
5809
|
+
enhancement_model: result.model,
|
|
5810
|
+
post_processing_mode: postProcessingMode,
|
|
5811
|
+
enhancement_reason: detection.reason
|
|
5325
5812
|
};
|
|
5326
5813
|
}
|
|
5814
|
+
function resolveTranscriberModel(config) {
|
|
5815
|
+
return config.transcriber_model || config.enhancement_model;
|
|
5816
|
+
}
|
|
5817
|
+
function normalizeProcessTextOptions(value) {
|
|
5818
|
+
if (typeof value === "string")
|
|
5819
|
+
return { systemPrompt: value };
|
|
5820
|
+
return value ?? {};
|
|
5821
|
+
}
|
|
5822
|
+
function combinePrompts(...prompts) {
|
|
5823
|
+
return prompts.map((prompt) => prompt?.trim() ?? "").filter(Boolean).join(`
|
|
5824
|
+
|
|
5825
|
+
`);
|
|
5826
|
+
}
|
|
5827
|
+
|
|
5828
|
+
// src/lib/machine.ts
|
|
5829
|
+
import { hostname } from "os";
|
|
5830
|
+
function currentMachineId(env = process.env, hostName = hostname()) {
|
|
5831
|
+
const configured = env.HASNA_MACHINE_ID?.trim();
|
|
5832
|
+
if (configured)
|
|
5833
|
+
return configured;
|
|
5834
|
+
return hostName.trim();
|
|
5835
|
+
}
|
|
5327
5836
|
|
|
5328
5837
|
// src/mcp/index.ts
|
|
5329
5838
|
var config = loadConfig();
|
|
5330
5839
|
ensureDataDir(config);
|
|
5840
|
+
function runtimeConfig() {
|
|
5841
|
+
return {
|
|
5842
|
+
...loadConfig(),
|
|
5843
|
+
db_path: config.db_path,
|
|
5844
|
+
audio_dir: config.audio_dir
|
|
5845
|
+
};
|
|
5846
|
+
}
|
|
5331
5847
|
function buildServer() {
|
|
5332
5848
|
const server = new McpServer({
|
|
5333
5849
|
name: "recordings",
|
|
@@ -5339,34 +5855,205 @@ function buildServer() {
|
|
|
5339
5855
|
}
|
|
5340
5856
|
function errorResult(e) {
|
|
5341
5857
|
const msg = e instanceof Error ? e.message : String(e);
|
|
5342
|
-
return { content: [{ type: "text", text: `Error: ${msg}` }], isError: true };
|
|
5858
|
+
return { content: [{ type: "text", text: `Error: ${truncateText(msg, 500)}` }], isError: true };
|
|
5343
5859
|
}
|
|
5344
5860
|
function compact(r) {
|
|
5345
|
-
const
|
|
5346
|
-
return `${r.id
|
|
5861
|
+
const preview = truncateText(r.processed_text || r.raw_text, 100);
|
|
5862
|
+
return `${truncateText(r.id, 8)} | ${truncateText(r.processing_mode, 16)} | ${truncateText(r.created_at, 16)} | ${preview}`;
|
|
5347
5863
|
}
|
|
5348
|
-
function
|
|
5349
|
-
const lines = [`ID: ${r.id}`, `Mode: ${r.processing_mode}`, `Model: ${r.model_used}`];
|
|
5864
|
+
function detail(r) {
|
|
5865
|
+
const lines = [`ID: ${truncateText(r.id, 80)}`, `Mode: ${truncateText(r.processing_mode, 16)}`, `Model: ${truncateText(r.model_used, 80)}`];
|
|
5350
5866
|
if (r.enhancement_model)
|
|
5351
|
-
lines.push(`Enhanced by: ${r.enhancement_model}`);
|
|
5867
|
+
lines.push(`Enhanced by: ${truncateText(r.enhancement_model, 80)}`);
|
|
5352
5868
|
if (r.duration_ms)
|
|
5353
5869
|
lines.push(`Duration: ${(r.duration_ms / 1000).toFixed(1)}s`);
|
|
5354
5870
|
if (r.language)
|
|
5355
|
-
lines.push(`Language: ${r.language}`);
|
|
5871
|
+
lines.push(`Language: ${truncateText(r.language, 20)}`);
|
|
5356
5872
|
if (r.tags.length > 0)
|
|
5357
|
-
lines.push(`Tags: ${r.tags.join(", ")}`);
|
|
5873
|
+
lines.push(`Tags: ${r.tags.map((tag) => truncateText(tag, 80)).join(", ")}`);
|
|
5358
5874
|
if (r.agent_id)
|
|
5359
|
-
lines.push(`Agent: ${r.agent_id}`);
|
|
5875
|
+
lines.push(`Agent: ${truncateText(r.agent_id, 80)}`);
|
|
5360
5876
|
if (r.project_id)
|
|
5361
|
-
lines.push(`Project: ${r.project_id}`);
|
|
5877
|
+
lines.push(`Project: ${truncateText(r.project_id, 80)}`);
|
|
5362
5878
|
if (r.session_id)
|
|
5363
|
-
lines.push(`Session: ${r.session_id}`);
|
|
5364
|
-
lines.push(`Created: ${r.created_at}`);
|
|
5365
|
-
lines.push(`Text: ${r.raw_text}`);
|
|
5879
|
+
lines.push(`Session: ${truncateText(r.session_id, 80)}`);
|
|
5880
|
+
lines.push(`Created: ${truncateText(r.created_at, 40)}`);
|
|
5881
|
+
lines.push(`Text: ${stripTerminalControls(r.raw_text)}`);
|
|
5366
5882
|
if (r.processed_text && r.processed_text !== r.raw_text) {
|
|
5367
|
-
lines.push(`Enhanced: ${r.processed_text}`);
|
|
5883
|
+
lines.push(`Enhanced: ${stripTerminalControls(r.processed_text)}`);
|
|
5368
5884
|
}
|
|
5369
5885
|
return lines.join(`
|
|
5886
|
+
`);
|
|
5887
|
+
}
|
|
5888
|
+
function verboseRecording(r) {
|
|
5889
|
+
const lines = [
|
|
5890
|
+
`${truncateText(r.id, 8)} | ${truncateText(r.processing_mode, 16)} | ${truncateText(r.created_at, 16)}`,
|
|
5891
|
+
`Preview: ${truncateText(r.processed_text || r.raw_text, 180)}`,
|
|
5892
|
+
`Model: ${r.enhancement_model ? `${truncateText(r.model_used, 80)} -> ${truncateText(r.enhancement_model, 80)}` : truncateText(r.model_used, 80)}`
|
|
5893
|
+
];
|
|
5894
|
+
if (r.duration_ms)
|
|
5895
|
+
lines.push(`Duration: ${(r.duration_ms / 1000).toFixed(1)}s`);
|
|
5896
|
+
if (r.language)
|
|
5897
|
+
lines.push(`Language: ${truncateText(r.language, 20)}`);
|
|
5898
|
+
if (r.tags.length > 0)
|
|
5899
|
+
lines.push(`Tags: ${summarizeTags(r.tags)}`);
|
|
5900
|
+
const scopes = [
|
|
5901
|
+
r.agent_id ? `agent=${truncateText(r.agent_id, 80)}` : null,
|
|
5902
|
+
r.project_id ? `project=${truncateText(r.project_id, 80)}` : null,
|
|
5903
|
+
r.session_id ? `session=${truncateText(r.session_id, 80)}` : null
|
|
5904
|
+
].filter(Boolean);
|
|
5905
|
+
if (scopes.length > 0)
|
|
5906
|
+
lines.push(`Scope: ${scopes.join(" ")}`);
|
|
5907
|
+
lines.push(`Details: get_recording { "id": "${truncateText(r.id, 8)}" }`);
|
|
5908
|
+
return lines.join(`
|
|
5909
|
+
`);
|
|
5910
|
+
}
|
|
5911
|
+
function applyTranscriptionArgs(cfg, args) {
|
|
5912
|
+
if (args.language)
|
|
5913
|
+
cfg.language = args.language;
|
|
5914
|
+
const transcriptionPrompt = args.transcription_prompt ?? args.prompt;
|
|
5915
|
+
if (transcriptionPrompt !== undefined) {
|
|
5916
|
+
cfg.transcription_prompt = transcriptionPrompt;
|
|
5917
|
+
}
|
|
5918
|
+
const transcriberPrompt = args.transcriber_prompt ?? args.system_prompt;
|
|
5919
|
+
if (transcriberPrompt !== undefined) {
|
|
5920
|
+
cfg.transcriber_prompt = transcriberPrompt;
|
|
5921
|
+
}
|
|
5922
|
+
if (args.transcriber_model) {
|
|
5923
|
+
cfg.transcriber_model = args.transcriber_model;
|
|
5924
|
+
} else if (args.enhancement_model) {
|
|
5925
|
+
cfg.enhancement_model = args.enhancement_model;
|
|
5926
|
+
cfg.transcriber_model = args.enhancement_model;
|
|
5927
|
+
}
|
|
5928
|
+
const requestedMode = args.post_processing_mode ?? args.post_processing;
|
|
5929
|
+
if (args.no_enhance || args.enhance === false) {
|
|
5930
|
+
cfg.post_processing_mode = "off";
|
|
5931
|
+
normalizePostProcessingConfig(cfg, true);
|
|
5932
|
+
} else if (args.enhance === true) {
|
|
5933
|
+
cfg.post_processing_mode = "always";
|
|
5934
|
+
normalizePostProcessingConfig(cfg, true);
|
|
5935
|
+
} else if (requestedMode) {
|
|
5936
|
+
cfg.post_processing_mode = normalizePostProcessingMode(requestedMode, cfg.post_processing_mode ?? "auto");
|
|
5937
|
+
normalizePostProcessingConfig(cfg, true);
|
|
5938
|
+
} else {
|
|
5939
|
+
normalizePostProcessingConfig(cfg, false);
|
|
5940
|
+
}
|
|
5941
|
+
return cfg;
|
|
5942
|
+
}
|
|
5943
|
+
function buildTranscriptionMetadata(cfg, processed, sources = {}) {
|
|
5944
|
+
const transcriptionPromptConfigured = Boolean(cfg.transcription_prompt?.trim());
|
|
5945
|
+
const transcriberPromptConfigured = Boolean(cfg.transcriber_prompt?.trim());
|
|
5946
|
+
return {
|
|
5947
|
+
transcription_prompt: {
|
|
5948
|
+
configured: transcriptionPromptConfigured,
|
|
5949
|
+
source: sources.transcriptionPromptFromRequest ? "request" : transcriptionPromptConfigured ? "config" : "none"
|
|
5950
|
+
},
|
|
5951
|
+
transcriber_prompt: {
|
|
5952
|
+
configured: transcriberPromptConfigured,
|
|
5953
|
+
source: sources.transcriberPromptFromRequest ? "request" : transcriberPromptConfigured ? "config" : "none"
|
|
5954
|
+
},
|
|
5955
|
+
post_processing: {
|
|
5956
|
+
mode: processed.post_processing_mode,
|
|
5957
|
+
applied: processed.mode === "enhanced",
|
|
5958
|
+
reason: processed.enhancement_reason,
|
|
5959
|
+
model: processed.enhancement_model
|
|
5960
|
+
},
|
|
5961
|
+
transcriber_model: resolveTranscriberModel(cfg)
|
|
5962
|
+
};
|
|
5963
|
+
}
|
|
5964
|
+
function recordingJson(r) {
|
|
5965
|
+
return JSON.stringify({
|
|
5966
|
+
id: r.id,
|
|
5967
|
+
raw_text: r.raw_text,
|
|
5968
|
+
processed_text: r.processed_text,
|
|
5969
|
+
processing_mode: r.processing_mode,
|
|
5970
|
+
model_used: r.model_used,
|
|
5971
|
+
enhancement_model: r.enhancement_model,
|
|
5972
|
+
language: r.language,
|
|
5973
|
+
tags: r.tags,
|
|
5974
|
+
metadata: r.metadata,
|
|
5975
|
+
created_at: r.created_at
|
|
5976
|
+
}, null, 2);
|
|
5977
|
+
}
|
|
5978
|
+
function truncateText(value, max) {
|
|
5979
|
+
const normalized = sanitizeInline(value);
|
|
5980
|
+
const prefix = [];
|
|
5981
|
+
for (const point of normalized) {
|
|
5982
|
+
if (prefix.length === max) {
|
|
5983
|
+
return `${prefix.slice(0, Math.max(0, max - 3)).join("")}...`;
|
|
5984
|
+
}
|
|
5985
|
+
prefix.push(point);
|
|
5986
|
+
}
|
|
5987
|
+
return normalized;
|
|
5988
|
+
}
|
|
5989
|
+
function stripTerminalControls(value) {
|
|
5990
|
+
return value.replace(/(?:\u001b\]|\u009d)[\s\S]*?(?:\u0007|\u001b\\|\u009c)/g, "").replace(/(?:\u001b[PX^_]|\u0090|\u0098|\u009e|\u009f)[\s\S]*?(?:\u001b\\|\u009c)/g, "").replace(/(?:\u001b\[|\u009b)[0-?]*[ -/]*[@-~]/g, "").replace(/\u001b[@-_]/g, "").replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/g, "");
|
|
5991
|
+
}
|
|
5992
|
+
function sanitizeInline(value) {
|
|
5993
|
+
return stripTerminalControls(value).replace(/[\r\n\t]+/g, " ").replace(/\s+/g, " ").trim();
|
|
5994
|
+
}
|
|
5995
|
+
function summarizeTags(tags) {
|
|
5996
|
+
const shown = tags.slice(0, 3).map((tag) => truncateText(tag, 20));
|
|
5997
|
+
if (tags.length > shown.length)
|
|
5998
|
+
shown.push(`+${tags.length - shown.length}`);
|
|
5999
|
+
return shown.join(", ");
|
|
6000
|
+
}
|
|
6001
|
+
function resolveMcpPagination(args, defaultLimit = 10) {
|
|
6002
|
+
const requested = Number.isFinite(args.limit) && args.limit && args.limit > 0 ? Math.floor(args.limit) : defaultLimit;
|
|
6003
|
+
const max = args.full ? 10 : 50;
|
|
6004
|
+
const rawOffset = args.cursor ?? args.offset ?? 0;
|
|
6005
|
+
return {
|
|
6006
|
+
limit: Math.min(requested, max),
|
|
6007
|
+
offset: Number.isFinite(rawOffset) ? Math.max(0, Math.floor(rawOffset)) : 0,
|
|
6008
|
+
capped: requested > max
|
|
6009
|
+
};
|
|
6010
|
+
}
|
|
6011
|
+
function withoutPagination(filter) {
|
|
6012
|
+
const { limit: _limit, offset: _offset, ...rest } = filter;
|
|
6013
|
+
return rest;
|
|
6014
|
+
}
|
|
6015
|
+
function formatMcpCollection(label, recordings, options) {
|
|
6016
|
+
if (recordings.length === 0) {
|
|
6017
|
+
return options.total === 0 ? `No ${label} found.` : `No ${label} at cursor ${options.offset}.`;
|
|
6018
|
+
}
|
|
6019
|
+
const format = options.full ? verboseRecording : compact;
|
|
6020
|
+
const separator = options.full ? `
|
|
6021
|
+
---
|
|
6022
|
+
` : `
|
|
6023
|
+
`;
|
|
6024
|
+
const end = options.offset + recordings.length;
|
|
6025
|
+
const total = Math.max(options.total, end);
|
|
6026
|
+
const lines = [
|
|
6027
|
+
`${label}: showing ${recordings.length} of ${total} (${options.offset + 1}-${end}, limit ${options.limit})`,
|
|
6028
|
+
recordings.map(format).join(separator)
|
|
6029
|
+
];
|
|
6030
|
+
const hints = [];
|
|
6031
|
+
if (options.capped)
|
|
6032
|
+
hints.push(`limit capped at ${options.limit} for MCP output`);
|
|
6033
|
+
if (end < total)
|
|
6034
|
+
hints.push(`next cursor: ${end}`);
|
|
6035
|
+
hints.push("use get_recording { id } for full details; set full=true for richer rows");
|
|
6036
|
+
lines.push(`Hints: ${hints.join("; ")}.`);
|
|
6037
|
+
return lines.join(separator);
|
|
6038
|
+
}
|
|
6039
|
+
function formatMcpPage(label, items, args, format) {
|
|
6040
|
+
const pagination = resolveMcpPagination(args, 20);
|
|
6041
|
+
const page = items.slice(pagination.offset, pagination.offset + pagination.limit);
|
|
6042
|
+
if (page.length === 0) {
|
|
6043
|
+
return items.length === 0 ? `No ${label}.` : `No ${label} at cursor ${pagination.offset}.`;
|
|
6044
|
+
}
|
|
6045
|
+
const end = pagination.offset + page.length;
|
|
6046
|
+
const hints = [];
|
|
6047
|
+
if (pagination.capped)
|
|
6048
|
+
hints.push(`limit capped at ${pagination.limit} for MCP output`);
|
|
6049
|
+
if (end < items.length)
|
|
6050
|
+
hints.push(`next cursor: ${end}`);
|
|
6051
|
+
return [
|
|
6052
|
+
`${label}: showing ${page.length} of ${items.length} (${pagination.offset + 1}-${end}, limit ${pagination.limit})`,
|
|
6053
|
+
page.map(format).join(`
|
|
6054
|
+
`),
|
|
6055
|
+
hints.length > 0 ? `Hints: ${hints.join("; ")}.` : ""
|
|
6056
|
+
].filter(Boolean).join(`
|
|
5370
6057
|
`);
|
|
5371
6058
|
}
|
|
5372
6059
|
async function saveRecordingMemento(args) {
|
|
@@ -5394,26 +6081,30 @@ function buildServer() {
|
|
|
5394
6081
|
} catch {}
|
|
5395
6082
|
}
|
|
5396
6083
|
const toolDocs = {
|
|
5397
|
-
|
|
5398
|
-
Params:
|
|
6084
|
+
recordings_status: `Show safe service status for agents.
|
|
6085
|
+
Params: none
|
|
6086
|
+
Returns: JSON with package version, MCP HTTP defaults, active transcription/enhancement models, language, data paths, key-presence booleans, and recording counts. Never returns secret values.`,
|
|
6087
|
+
transcribe_audio: `Transcribe audio file and save raw plus optional processed text.
|
|
6088
|
+
Params: audio_path (string, required): path to wav/mp3/m4a/webm | language (string): ISO code e.g. en/es/fr | transcription_prompt or prompt (string): STT vocabulary/context only | transcriber_prompt or system_prompt (string): post-transcription cleanup instructions | post_processing_mode ('off'|'auto'|'always') | transcriber_model (string) | no_enhance (bool): alias for post_processing_mode=off | tags (string[]) | agent_id (string) | project_id (string) | session_id (string)
|
|
6089
|
+
Returns: JSON recording summary with raw_text, processed_text, processing_mode, and safe metadata.`,
|
|
5399
6090
|
save_recording: `Save text as recording. Auto-enhances if needed.
|
|
5400
|
-
Params: text (string, required): text to save | enhance (bool):
|
|
6091
|
+
Params: text (string, required): text to save | enhance (bool): true forces always, false disables | transcriber_prompt or system_prompt (string): post-processing instructions | post_processing_mode ('off'|'auto'|'always') | tags (string[]) | agent_id (string) | project_id (string) | session_id (string) | metadata (object)`,
|
|
5401
6092
|
get_recording: `Get recording by ID or prefix.
|
|
5402
6093
|
Params: id (string, required): recording ID or prefix`,
|
|
5403
|
-
list_recordings: `List recordings, compact by default, most recent first.
|
|
5404
|
-
Params: limit (number, default 10) | offset (number) | processing_mode ('raw'|'enhanced') | tags (string[]) | search (string): text search | since/until (ISO date) | agent_id | project_id | session_id | full (bool):
|
|
5405
|
-
search_recordings: `Search recordings by text content.
|
|
5406
|
-
Params: query (string, required) | limit (number, default 10) | agent_id | project_id | full (bool):
|
|
6094
|
+
list_recordings: `List recordings, compact and capped by default, most recent first.
|
|
6095
|
+
Params: limit (number, default 10, max 50 compact / 10 verbose) | offset or cursor (number) | processing_mode ('raw'|'enhanced') | tags (string[]) | search (string): text search | since/until (ISO date) | agent_id | project_id | session_id | full (bool): richer metadata rows with bounded previews. Use get_recording for full text/details.`,
|
|
6096
|
+
search_recordings: `Search recordings by text content, compact and capped by default.
|
|
6097
|
+
Params: query (string, required) | limit (number, default 10, max 50 compact / 10 verbose) | offset or cursor (number) | agent_id | project_id | full (bool): richer metadata rows with bounded previews. Use get_recording for full text/details.`,
|
|
5407
6098
|
delete_recording: `Delete recording by ID.
|
|
5408
6099
|
Params: id (string, required)`,
|
|
5409
|
-
recording_stats: `Recording count, mode breakdown, duration.
|
|
5410
|
-
Params:
|
|
6100
|
+
recording_stats: `Recording count, mode breakdown, duration, and capped model breakdown.
|
|
6101
|
+
Params: limit_models (number, default 10, max 10 compact / 50 verbose) | verbose (bool): allow up to 50 model counts`,
|
|
5411
6102
|
detect_enhancement: `Check if text needs AI enhancement.
|
|
5412
6103
|
Params: text (string, required)`,
|
|
5413
6104
|
register_agent: `Register agent (idempotent). Auto-updates last_seen_at on re-register.
|
|
5414
6105
|
Params: name (string, required) | description (string) | role (string)`,
|
|
5415
|
-
list_agents: `List registered agents.
|
|
5416
|
-
Params:
|
|
6106
|
+
list_agents: `List registered agents, capped by default.
|
|
6107
|
+
Params: limit (number, default 20, max 50) | offset or cursor (number)`,
|
|
5417
6108
|
get_agent: `Get agent by ID or name.
|
|
5418
6109
|
Params: id (string, required)`,
|
|
5419
6110
|
heartbeat: `Update last_seen_at to signal agent is active.
|
|
@@ -5422,16 +6113,59 @@ Params: agent_id (string, required): agent ID or name`,
|
|
|
5422
6113
|
Params: agent_id (string, required) | project_id (string, nullable): project ID or null to clear`,
|
|
5423
6114
|
register_project: `Register project (idempotent).
|
|
5424
6115
|
Params: name (string, required) | path (string, required): absolute path | description (string)`,
|
|
5425
|
-
list_projects: `List registered projects.
|
|
5426
|
-
Params:
|
|
6116
|
+
list_projects: `List registered projects, capped by default.
|
|
6117
|
+
Params: limit (number, default 20, max 50) | offset or cursor (number)`
|
|
5427
6118
|
};
|
|
5428
6119
|
registerTool("describe_tool", "Get full param docs for any tool.", { name: exports_external.string() }, async (args) => {
|
|
5429
6120
|
const doc = toolDocs[args.name];
|
|
5430
|
-
return doc ? text(doc) : text(`Unknown tool: ${args.name}. Available: ${Object.keys(toolDocs).join(", ")}`);
|
|
6121
|
+
return doc ? text(doc) : text(`Unknown tool: ${truncateText(args.name, 80)}. Available: ${Object.keys(toolDocs).join(", ")}`);
|
|
6122
|
+
});
|
|
6123
|
+
registerTool("recordings_status", "Show safe service status for agents.", {}, async () => {
|
|
6124
|
+
try {
|
|
6125
|
+
const cfg = runtimeConfig();
|
|
6126
|
+
const stats = await getStore().getRecordingStats();
|
|
6127
|
+
return text(JSON.stringify({
|
|
6128
|
+
service: "recordings",
|
|
6129
|
+
version: VERSION,
|
|
6130
|
+
mcp: {
|
|
6131
|
+
default_http_port: DEFAULT_MCP_HTTP_PORT,
|
|
6132
|
+
endpoint: "/mcp"
|
|
6133
|
+
},
|
|
6134
|
+
config: {
|
|
6135
|
+
transcription_model: cfg.transcription_model,
|
|
6136
|
+
realtime_session_model: cfg.realtime_session_model,
|
|
6137
|
+
realtime_transcription_model: cfg.realtime_transcription_model,
|
|
6138
|
+
enhancement_model: cfg.enhancement_model,
|
|
6139
|
+
transcriber_model: resolveTranscriberModel(cfg),
|
|
6140
|
+
language: cfg.language,
|
|
6141
|
+
auto_enhance: cfg.auto_enhance,
|
|
6142
|
+
post_processing_mode: cfg.post_processing_mode,
|
|
6143
|
+
transcription_prompt_configured: Boolean(cfg.transcription_prompt?.trim()),
|
|
6144
|
+
transcriber_prompt_configured: Boolean(cfg.transcriber_prompt?.trim()),
|
|
6145
|
+
max_recording_seconds: cfg.max_recording_seconds,
|
|
6146
|
+
db_path: cfg.db_path,
|
|
6147
|
+
audio_dir: cfg.audio_dir,
|
|
6148
|
+
openai_api_key_configured: Boolean(cfg.openai_api_key),
|
|
6149
|
+
enhancement_api_key_configured: Boolean(cfg.enhancement_api_key || cfg.openai_api_key),
|
|
6150
|
+
config_warnings: cfg.config_warnings ?? []
|
|
6151
|
+
},
|
|
6152
|
+
stats
|
|
6153
|
+
}, null, 2));
|
|
6154
|
+
} catch (e) {
|
|
6155
|
+
return errorResult(e);
|
|
6156
|
+
}
|
|
5431
6157
|
});
|
|
5432
6158
|
registerTool("transcribe_audio", "Transcribe audio file. Auto-enhances if needed.", {
|
|
5433
6159
|
audio_path: exports_external.string(),
|
|
5434
6160
|
language: exports_external.string().optional(),
|
|
6161
|
+
transcription_prompt: exports_external.string().optional(),
|
|
6162
|
+
prompt: exports_external.string().optional(),
|
|
6163
|
+
transcriber_prompt: exports_external.string().optional(),
|
|
6164
|
+
system_prompt: exports_external.string().optional(),
|
|
6165
|
+
post_processing_mode: exports_external.enum(["off", "auto", "always"]).optional(),
|
|
6166
|
+
post_processing: exports_external.enum(["off", "auto", "always"]).optional(),
|
|
6167
|
+
transcriber_model: exports_external.string().optional(),
|
|
6168
|
+
enhancement_model: exports_external.string().optional(),
|
|
5435
6169
|
no_enhance: exports_external.boolean().optional(),
|
|
5436
6170
|
tags: exports_external.array(exports_external.string()).optional(),
|
|
5437
6171
|
agent_id: exports_external.string().optional(),
|
|
@@ -5439,13 +6173,13 @@ Params: none`
|
|
|
5439
6173
|
session_id: exports_external.string().optional()
|
|
5440
6174
|
}, async (args) => {
|
|
5441
6175
|
try {
|
|
5442
|
-
const cfg = { ...
|
|
5443
|
-
if (args.language)
|
|
5444
|
-
cfg.language = args.language;
|
|
5445
|
-
if (args.no_enhance)
|
|
5446
|
-
cfg.auto_enhance = false;
|
|
6176
|
+
const cfg = applyTranscriptionArgs({ ...runtimeConfig() }, args);
|
|
5447
6177
|
const transcription = await transcribeAudio(args.audio_path, cfg);
|
|
5448
6178
|
const processed = await processText(transcription.text, cfg);
|
|
6179
|
+
const metadata = buildTranscriptionMetadata(cfg, processed, {
|
|
6180
|
+
transcriptionPromptFromRequest: args.transcription_prompt !== undefined || args.prompt !== undefined,
|
|
6181
|
+
transcriberPromptFromRequest: args.transcriber_prompt !== undefined || args.system_prompt !== undefined
|
|
6182
|
+
});
|
|
5449
6183
|
const recording = await getStore().createRecording({
|
|
5450
6184
|
audio_path: args.audio_path,
|
|
5451
6185
|
raw_text: transcription.text,
|
|
@@ -5458,7 +6192,9 @@ Params: none`
|
|
|
5458
6192
|
tags: args.tags,
|
|
5459
6193
|
agent_id: args.agent_id,
|
|
5460
6194
|
project_id: args.project_id,
|
|
5461
|
-
session_id: args.session_id
|
|
6195
|
+
session_id: args.session_id,
|
|
6196
|
+
machine_id: currentMachineId(),
|
|
6197
|
+
metadata
|
|
5462
6198
|
});
|
|
5463
6199
|
if (args.agent_id) {
|
|
5464
6200
|
await saveRecordingMemento({
|
|
@@ -5474,8 +6210,7 @@ Params: none`
|
|
|
5474
6210
|
summary: `Recording ${recording.id.slice(0, 8)} for ${args.agent_id}`
|
|
5475
6211
|
});
|
|
5476
6212
|
}
|
|
5477
|
-
|
|
5478
|
-
return text(`${recording.id.slice(0, 8)} | ${processed.mode} | ${output}`);
|
|
6213
|
+
return text(recordingJson(recording));
|
|
5479
6214
|
} catch (e) {
|
|
5480
6215
|
return errorResult(e);
|
|
5481
6216
|
}
|
|
@@ -5483,6 +6218,12 @@ Params: none`
|
|
|
5483
6218
|
registerTool("save_recording", "Save text as recording. Auto-enhances if needed.", {
|
|
5484
6219
|
text: exports_external.string(),
|
|
5485
6220
|
enhance: exports_external.boolean().optional(),
|
|
6221
|
+
transcriber_prompt: exports_external.string().optional(),
|
|
6222
|
+
system_prompt: exports_external.string().optional(),
|
|
6223
|
+
post_processing_mode: exports_external.enum(["off", "auto", "always"]).optional(),
|
|
6224
|
+
post_processing: exports_external.enum(["off", "auto", "always"]).optional(),
|
|
6225
|
+
transcriber_model: exports_external.string().optional(),
|
|
6226
|
+
enhancement_model: exports_external.string().optional(),
|
|
5486
6227
|
tags: exports_external.array(exports_external.string()).optional(),
|
|
5487
6228
|
agent_id: exports_external.string().optional(),
|
|
5488
6229
|
project_id: exports_external.string().optional(),
|
|
@@ -5493,17 +6234,28 @@ Params: none`
|
|
|
5493
6234
|
metadata: exports_external.record(exports_external.unknown()).optional()
|
|
5494
6235
|
}, async (args) => {
|
|
5495
6236
|
try {
|
|
6237
|
+
const cfg = applyTranscriptionArgs(runtimeConfig(), args);
|
|
5496
6238
|
let processedText;
|
|
5497
6239
|
let mode = "raw";
|
|
5498
6240
|
let enhModel;
|
|
5499
|
-
|
|
5500
|
-
|
|
5501
|
-
|
|
5502
|
-
|
|
5503
|
-
|
|
5504
|
-
|
|
5505
|
-
|
|
6241
|
+
let processed = {
|
|
6242
|
+
text: args.text,
|
|
6243
|
+
mode: "raw",
|
|
6244
|
+
enhancement_model: null,
|
|
6245
|
+
post_processing_mode: cfg.post_processing_mode ?? "auto",
|
|
6246
|
+
enhancement_reason: null
|
|
6247
|
+
};
|
|
6248
|
+
if (cfg.post_processing_mode !== "off") {
|
|
6249
|
+
processed = await processText(args.text, cfg);
|
|
5506
6250
|
}
|
|
6251
|
+
if (processed.mode === "enhanced") {
|
|
6252
|
+
processedText = processed.text;
|
|
6253
|
+
mode = "enhanced";
|
|
6254
|
+
enhModel = processed.enhancement_model || undefined;
|
|
6255
|
+
}
|
|
6256
|
+
const processingMetadata = buildTranscriptionMetadata(cfg, processed, {
|
|
6257
|
+
transcriberPromptFromRequest: args.transcriber_prompt !== undefined || args.system_prompt !== undefined
|
|
6258
|
+
});
|
|
5507
6259
|
const recording = await getStore().createRecording({
|
|
5508
6260
|
raw_text: args.text,
|
|
5509
6261
|
processed_text: processedText,
|
|
@@ -5517,10 +6269,13 @@ Params: none`
|
|
|
5517
6269
|
goal: args.goal,
|
|
5518
6270
|
role: args.role,
|
|
5519
6271
|
task_list_id: args.task_list_id,
|
|
5520
|
-
|
|
6272
|
+
machine_id: currentMachineId(),
|
|
6273
|
+
metadata: {
|
|
6274
|
+
...args.metadata ?? {},
|
|
6275
|
+
...processingMetadata
|
|
6276
|
+
}
|
|
5521
6277
|
});
|
|
5522
|
-
|
|
5523
|
-
return text(`${recording.id.slice(0, 8)} | ${mode} | ${output}`);
|
|
6278
|
+
return text(recordingJson(recording));
|
|
5524
6279
|
} catch (e) {
|
|
5525
6280
|
return errorResult(e);
|
|
5526
6281
|
}
|
|
@@ -5529,8 +6284,8 @@ Params: none`
|
|
|
5529
6284
|
try {
|
|
5530
6285
|
const r = await getStore().getRecording(args.id);
|
|
5531
6286
|
if (!r)
|
|
5532
|
-
return text(`Not found: ${args.id}`);
|
|
5533
|
-
return text(
|
|
6287
|
+
return text(`Not found: ${truncateText(args.id, 80)}`);
|
|
6288
|
+
return text(detail(r));
|
|
5534
6289
|
} catch (e) {
|
|
5535
6290
|
return errorResult(e);
|
|
5536
6291
|
}
|
|
@@ -5538,6 +6293,7 @@ Params: none`
|
|
|
5538
6293
|
registerTool("list_recordings", "List recordings. Compact default, recent first.", {
|
|
5539
6294
|
limit: exports_external.number().optional(),
|
|
5540
6295
|
offset: exports_external.number().optional(),
|
|
6296
|
+
cursor: exports_external.number().optional(),
|
|
5541
6297
|
processing_mode: exports_external.enum(["raw", "enhanced"]).optional(),
|
|
5542
6298
|
tags: exports_external.array(exports_external.string()).optional(),
|
|
5543
6299
|
search: exports_external.string().optional(),
|
|
@@ -5549,9 +6305,10 @@ Params: none`
|
|
|
5549
6305
|
full: exports_external.boolean().optional()
|
|
5550
6306
|
}, async (args) => {
|
|
5551
6307
|
try {
|
|
6308
|
+
const pagination = resolveMcpPagination(args);
|
|
5552
6309
|
const filter = {
|
|
5553
|
-
limit:
|
|
5554
|
-
offset:
|
|
6310
|
+
limit: pagination.limit,
|
|
6311
|
+
offset: pagination.offset,
|
|
5555
6312
|
processing_mode: args.processing_mode,
|
|
5556
6313
|
tags: args.tags,
|
|
5557
6314
|
search: args.search,
|
|
@@ -5561,15 +6318,18 @@ Params: none`
|
|
|
5561
6318
|
project_id: args.project_id,
|
|
5562
6319
|
session_id: args.session_id
|
|
5563
6320
|
};
|
|
5564
|
-
const
|
|
5565
|
-
|
|
5566
|
-
|
|
5567
|
-
|
|
5568
|
-
|
|
5569
|
-
|
|
5570
|
-
|
|
5571
|
-
|
|
5572
|
-
|
|
6321
|
+
const store = getStore();
|
|
6322
|
+
const [recordings, total] = await Promise.all([
|
|
6323
|
+
store.listRecordings(filter),
|
|
6324
|
+
countStoreRecordings(store, withoutPagination(filter))
|
|
6325
|
+
]);
|
|
6326
|
+
return text(formatMcpCollection("recordings", recordings, {
|
|
6327
|
+
total,
|
|
6328
|
+
offset: pagination.offset,
|
|
6329
|
+
limit: pagination.limit,
|
|
6330
|
+
full: args.full,
|
|
6331
|
+
capped: pagination.capped
|
|
6332
|
+
}));
|
|
5573
6333
|
} catch (e) {
|
|
5574
6334
|
return errorResult(e);
|
|
5575
6335
|
}
|
|
@@ -5577,42 +6337,63 @@ Params: none`
|
|
|
5577
6337
|
registerTool("search_recordings", "Search recordings by text.", {
|
|
5578
6338
|
query: exports_external.string(),
|
|
5579
6339
|
limit: exports_external.number().optional(),
|
|
6340
|
+
offset: exports_external.number().optional(),
|
|
6341
|
+
cursor: exports_external.number().optional(),
|
|
5580
6342
|
agent_id: exports_external.string().optional(),
|
|
5581
6343
|
project_id: exports_external.string().optional(),
|
|
6344
|
+
session_id: exports_external.string().optional(),
|
|
5582
6345
|
full: exports_external.boolean().optional()
|
|
5583
6346
|
}, async (args) => {
|
|
5584
6347
|
try {
|
|
5585
|
-
const
|
|
5586
|
-
|
|
6348
|
+
const pagination = resolveMcpPagination(args);
|
|
6349
|
+
const filter = {
|
|
6350
|
+
limit: pagination.limit,
|
|
6351
|
+
offset: pagination.offset,
|
|
5587
6352
|
agent_id: args.agent_id,
|
|
5588
|
-
project_id: args.project_id
|
|
5589
|
-
|
|
5590
|
-
|
|
5591
|
-
|
|
5592
|
-
const
|
|
5593
|
-
|
|
5594
|
-
|
|
5595
|
-
|
|
5596
|
-
|
|
5597
|
-
|
|
6353
|
+
project_id: args.project_id,
|
|
6354
|
+
session_id: args.session_id
|
|
6355
|
+
};
|
|
6356
|
+
const store = getStore();
|
|
6357
|
+
const [results, total] = await Promise.all([
|
|
6358
|
+
store.searchRecordings(args.query, filter),
|
|
6359
|
+
countStoreRecordings(store, withoutPagination({ ...filter, search: args.query }))
|
|
6360
|
+
]);
|
|
6361
|
+
return text(formatMcpCollection("results", results, {
|
|
6362
|
+
total,
|
|
6363
|
+
offset: pagination.offset,
|
|
6364
|
+
limit: pagination.limit,
|
|
6365
|
+
full: args.full,
|
|
6366
|
+
capped: pagination.capped
|
|
6367
|
+
}));
|
|
5598
6368
|
} catch (e) {
|
|
5599
6369
|
return errorResult(e);
|
|
5600
6370
|
}
|
|
5601
6371
|
});
|
|
5602
6372
|
registerTool("delete_recording", "Delete recording by ID.", { id: exports_external.string() }, async (args) => {
|
|
5603
6373
|
try {
|
|
5604
|
-
return text(await getStore().deleteRecording(args.id) ? `Deleted ${args.id}` : `Not found: ${args.id}`);
|
|
6374
|
+
return text(await getStore().deleteRecording(args.id) ? `Deleted ${truncateText(args.id, 80)}` : `Not found: ${truncateText(args.id, 80)}`);
|
|
5605
6375
|
} catch (e) {
|
|
5606
6376
|
return errorResult(e);
|
|
5607
6377
|
}
|
|
5608
6378
|
});
|
|
5609
|
-
registerTool("recording_stats", "Recording stats: count, modes, duration
|
|
6379
|
+
registerTool("recording_stats", "Recording stats: count, modes, duration, and capped model breakdown.", {
|
|
6380
|
+
limit_models: exports_external.number().optional(),
|
|
6381
|
+
verbose: exports_external.boolean().optional()
|
|
6382
|
+
}, async (args) => {
|
|
5610
6383
|
try {
|
|
5611
6384
|
const s = await getStore().getRecordingStats();
|
|
5612
6385
|
let out = `Total: ${s.total} | Raw: ${s.raw} | Enhanced: ${s.enhanced} | Duration: ${(s.total_duration_ms / 1000).toFixed(1)}s`;
|
|
5613
|
-
|
|
6386
|
+
const models = Object.entries(s.by_model).sort((a, b) => b[1] - a[1]);
|
|
6387
|
+
if (models.length > 0) {
|
|
6388
|
+
const maxModels = args.verbose ? 50 : 10;
|
|
6389
|
+
const requested = Number.isFinite(args.limit_models) && args.limit_models && args.limit_models > 0 ? Math.floor(args.limit_models) : maxModels;
|
|
6390
|
+
const limit = Math.min(requested, maxModels);
|
|
5614
6391
|
out += `
|
|
5615
|
-
` +
|
|
6392
|
+
Models: ` + models.slice(0, limit).map(([m, c]) => `${truncateText(m, 80)}: ${c}`).join(", ");
|
|
6393
|
+
if (limit < models.length) {
|
|
6394
|
+
out += `
|
|
6395
|
+
Hints: ${models.length - limit} more model(s); call recording_stats with verbose=true and limit_models up to 50.`;
|
|
6396
|
+
}
|
|
5616
6397
|
}
|
|
5617
6398
|
return text(out);
|
|
5618
6399
|
} catch (e) {
|
|
@@ -5621,7 +6402,7 @@ Params: none`
|
|
|
5621
6402
|
});
|
|
5622
6403
|
registerTool("detect_enhancement", "Check if text needs AI enhancement.", { text: exports_external.string() }, async (args) => {
|
|
5623
6404
|
try {
|
|
5624
|
-
const r = needsEnhancement(args.text,
|
|
6405
|
+
const r = needsEnhancement(args.text, runtimeConfig());
|
|
5625
6406
|
return text(`${r.needs ? "Yes" : "No"}: ${r.reason}`);
|
|
5626
6407
|
} catch (e) {
|
|
5627
6408
|
return errorResult(e);
|
|
@@ -5630,18 +6411,19 @@ Params: none`
|
|
|
5630
6411
|
registerTool("register_agent", "Register agent (idempotent).", { name: exports_external.string(), description: exports_external.string().optional(), role: exports_external.string().optional() }, async (args) => {
|
|
5631
6412
|
try {
|
|
5632
6413
|
const a = await getStore().registerAgent(args.name, args.description, args.role);
|
|
5633
|
-
return text(`${a.id} | ${a.name} | ${a.role}`);
|
|
6414
|
+
return text(`${truncateText(a.id, 80)} | ${truncateText(a.name, 80)} | ${truncateText(a.role, 40)}`);
|
|
5634
6415
|
} catch (e) {
|
|
5635
6416
|
return errorResult(e);
|
|
5636
6417
|
}
|
|
5637
6418
|
});
|
|
5638
|
-
registerTool("list_agents", "List registered agents
|
|
6419
|
+
registerTool("list_agents", "List registered agents, capped by default.", {
|
|
6420
|
+
limit: exports_external.number().optional(),
|
|
6421
|
+
offset: exports_external.number().optional(),
|
|
6422
|
+
cursor: exports_external.number().optional()
|
|
6423
|
+
}, async (args) => {
|
|
5639
6424
|
try {
|
|
5640
6425
|
const agents = await getStore().listAgents();
|
|
5641
|
-
|
|
5642
|
-
return text("None.");
|
|
5643
|
-
return text(agents.map((a) => `${a.id} | ${a.name} | ${a.role}`).join(`
|
|
5644
|
-
`));
|
|
6426
|
+
return text(formatMcpPage("agents", agents, args, (agent) => `${truncateText(agent.id, 80)} | ${truncateText(agent.name, 80)} | ${truncateText(agent.role, 40)} | last_seen: ${truncateText(agent.last_seen_at, 40)}`));
|
|
5645
6427
|
} catch (e) {
|
|
5646
6428
|
return errorResult(e);
|
|
5647
6429
|
}
|
|
@@ -5650,8 +6432,8 @@ Params: none`
|
|
|
5650
6432
|
try {
|
|
5651
6433
|
const a = await getStore().getAgent(args.id);
|
|
5652
6434
|
if (!a)
|
|
5653
|
-
return text(`Not found: ${args.id}`);
|
|
5654
|
-
return text(`${a.id} | ${a.name} | ${a.role} | ${a.last_seen_at}`);
|
|
6435
|
+
return text(`Not found: ${truncateText(args.id, 80)}`);
|
|
6436
|
+
return text(`${truncateText(a.id, 80)} | ${truncateText(a.name, 80)} | ${truncateText(a.role, 40)} | ${truncateText(a.last_seen_at, 40)}`);
|
|
5655
6437
|
} catch (e) {
|
|
5656
6438
|
return errorResult(e);
|
|
5657
6439
|
}
|
|
@@ -5659,18 +6441,19 @@ Params: none`
|
|
|
5659
6441
|
registerTool("register_project", "Register project (idempotent).", { name: exports_external.string(), path: exports_external.string(), description: exports_external.string().optional() }, async (args) => {
|
|
5660
6442
|
try {
|
|
5661
6443
|
const p = await getStore().registerProject(args.name, args.path, args.description);
|
|
5662
|
-
return text(`${p.id
|
|
6444
|
+
return text(`${truncateText(p.id, 8)} | ${truncateText(p.name, 80)} | ${truncateText(p.path, 120)}`);
|
|
5663
6445
|
} catch (e) {
|
|
5664
6446
|
return errorResult(e);
|
|
5665
6447
|
}
|
|
5666
6448
|
});
|
|
5667
|
-
registerTool("list_projects", "List registered projects
|
|
6449
|
+
registerTool("list_projects", "List registered projects, capped by default.", {
|
|
6450
|
+
limit: exports_external.number().optional(),
|
|
6451
|
+
offset: exports_external.number().optional(),
|
|
6452
|
+
cursor: exports_external.number().optional()
|
|
6453
|
+
}, async (args) => {
|
|
5668
6454
|
try {
|
|
5669
6455
|
const projects = await getStore().listProjects();
|
|
5670
|
-
|
|
5671
|
-
return text("None.");
|
|
5672
|
-
return text(projects.map((p) => `${p.id.slice(0, 8)} | ${p.name} | ${p.path}`).join(`
|
|
5673
|
-
`));
|
|
6456
|
+
return text(formatMcpPage("projects", projects, args, (project) => `${truncateText(project.id, 8)} | ${truncateText(project.name, 80)} | ${truncateText(project.path, 120)}`));
|
|
5674
6457
|
} catch (e) {
|
|
5675
6458
|
return errorResult(e);
|
|
5676
6459
|
}
|
|
@@ -5679,8 +6462,8 @@ Params: none`
|
|
|
5679
6462
|
try {
|
|
5680
6463
|
const agent = await getStore().heartbeatAgent(args.agent_id);
|
|
5681
6464
|
if (!agent)
|
|
5682
|
-
return text(`Agent not found: ${args.agent_id}`);
|
|
5683
|
-
return text(`${agent.id} | ${agent.name} | last_seen: ${agent.last_seen_at}`);
|
|
6465
|
+
return text(`Agent not found: ${truncateText(args.agent_id, 80)}`);
|
|
6466
|
+
return text(`${truncateText(agent.id, 80)} | ${truncateText(agent.name, 80)} | last_seen: ${truncateText(agent.last_seen_at, 40)}`);
|
|
5684
6467
|
} catch (e) {
|
|
5685
6468
|
return errorResult(e);
|
|
5686
6469
|
}
|
|
@@ -5689,8 +6472,8 @@ Params: none`
|
|
|
5689
6472
|
try {
|
|
5690
6473
|
const agent = await getStore().setAgentFocus(args.agent_id, args.project_id ?? null);
|
|
5691
6474
|
if (!agent)
|
|
5692
|
-
return text(`Agent not found: ${args.agent_id}`);
|
|
5693
|
-
return text(args.project_id ? `Focus set: ${args.project_id}` : "Focus cleared");
|
|
6475
|
+
return text(`Agent not found: ${truncateText(args.agent_id, 80)}`);
|
|
6476
|
+
return text(args.project_id ? `Focus set: ${truncateText(args.project_id, 80)}` : "Focus cleared");
|
|
5694
6477
|
} catch (e) {
|
|
5695
6478
|
return errorResult(e);
|
|
5696
6479
|
}
|