@rivus/gateway 0.18.2 → 0.19.0

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.
@@ -1,15 +1,18 @@
1
- import { S as deepFreeze, _ as loadMergedLocalEnvFile, b as validateRivusDeploymentManifest, c as parseRivusModelCliArguments, f as gatewayPackageManifestPath, g as loadRivusDaemonConfig, h as loadRivusDeploymentManifest, i as RIVUS_RELEASE_DESCRIPTOR, l as renderRivusModelCliArgumentError, m as gatewayTemplateDirectory, n as disposeRuntimeCacheEntries, r as invokeRuntimeControl, s as createRivusModelManagementWireRequest, t as createRuntimeCache, u as renderRivusModelCliHelp, v as resolveFeishuEndpointCredentials, x as validateRivusProjectSpaceDeployment, y as InvalidRivusProjectSpace } from "./runtime-cache.js";
1
+ import { C as validateRivusDeploymentManifest, S as InvalidRivusProjectSpace, T as deepFreeze, _ as gatewayTemplateDirectory, a as invokeRuntimeControl, b as loadMergedLocalEnvFile, d as renderRivusModelCliArgumentError, f as renderRivusModelCliHelp, h as gatewayPackageManifestPath, i as disposeRuntimeCacheEntries, l as createRivusModelManagementWireRequest, n as withoutRivusHomeDeployment, o as RIVUS_RELEASE_DESCRIPTOR, p as gatewayAudioTranscriberAssetDirectory, r as createRuntimeCache, t as RIVUS_HOME_DEPLOYMENT_ENV, u as parseRivusModelCliArguments, v as loadRivusDeploymentManifest, w as validateRivusProjectSpaceDeployment, x as resolveFeishuEndpointCredentials, y as loadRivusDaemonConfig } from "./rivus-home-deployment-env.js";
2
2
  import { n as narrowBackgroundSessionDefinition, t as createBackgroundSessionToolContracts } from "./background-session-authority.js";
3
3
  import { createRequire } from "node:module";
4
4
  import { createSha256Digest, createStableId } from "@rivus/platform/identity";
5
5
  import { RIVUS_MEMORY_TOOL_PLUGIN_ID, RIVUS_RUNTIME_TOOL_IDS, createRivusMemoryToolContract, isRivusRuntimeToolId } from "@rivus/runtime";
6
6
  import { homedir } from "node:os";
7
- import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
7
+ import { basename, delimiter, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
8
8
  import { fileURLToPath, pathToFileURL } from "node:url";
9
- import { Cause, Deferred, Effect, Exit, Result, Semaphore } from "effect";
10
- import { lstat, mkdir, open, readFile, readdir, realpath, rmdir, stat, unlink, writeFile } from "node:fs/promises";
11
- import { randomUUID } from "node:crypto";
9
+ import { Cause, Deferred, Effect, Exit, Option, Result, Schema, Semaphore } from "effect";
10
+ import { cp, lstat, mkdir, open, readFile, readdir, realpath, rename, rm, rmdir, stat, unlink, writeFile } from "node:fs/promises";
11
+ import { createHash, randomUUID } from "node:crypto";
12
12
  import { assertPathWithin, isPathWithin } from "@rivus/platform/filesystem";
13
+ import { setTimeout as setTimeout$1 } from "node:timers/promises";
14
+ import { readPersistenceFile, writeAtomicTextFile } from "@rivus/platform/persistence";
15
+ import { spawn } from "node:child_process";
13
16
  import { createConnection } from "node:net";
14
17
  import { constants as constants$1 } from "node:fs";
15
18
  import { unwrapExit } from "@rivus/platform/effect";
@@ -952,6 +955,561 @@ function createNodeRivusHome(options) {
952
955
  setup: initializeRivusHome
953
956
  };
954
957
  }
958
+ const RIVUS_AUDIO_CLI_USAGE = `Usage:
959
+ rivus audio install [--force] [--json]
960
+ rivus audio status
961
+ rivus audio transcribe <path>
962
+
963
+ Commands:
964
+ install Install or upgrade the managed audio transcriber in the current Home's state directory
965
+ status Print the installed and shipped transcriber releases as JSON
966
+ transcribe Print the transcript of one audio file; exits 2 when no speech is detected
967
+ and ${String(3)} when the transcriber is not installed
968
+ `;
969
+ function parseRivusAudioCliArguments(argv) {
970
+ if (argv.includes("--help") || argv.includes("-h")) return { help: true };
971
+ const [operation, ...rest] = argv;
972
+ if (operation === "install") {
973
+ const flags = new Set(rest);
974
+ const unknown = rest.find((argument) => argument !== "--force" && argument !== "--json");
975
+ if (unknown !== void 0) return { error: `Unknown audio install option: ${unknown}` };
976
+ if (flags.size !== rest.length) return { error: "audio install options may only be provided once" };
977
+ return {
978
+ force: flags.has("--force"),
979
+ json: flags.has("--json"),
980
+ operation
981
+ };
982
+ }
983
+ if (operation === "status") return rest.length === 0 ? { operation } : { error: "audio status accepts no arguments" };
984
+ if (operation === "transcribe") {
985
+ const path = rest[0];
986
+ if (rest.length !== 1 || !path || path.startsWith("-")) return { error: "audio transcribe requires exactly one audio path" };
987
+ return {
988
+ operation,
989
+ path
990
+ };
991
+ }
992
+ return { error: "audio requires one of install, status, transcribe" };
993
+ }
994
+ function renderRivusAudioCliArgumentError(error) {
995
+ return `${error}\n\n${RIVUS_AUDIO_CLI_USAGE}`;
996
+ }
997
+ function renderRivusAudioCliStatus(status) {
998
+ return `${JSON.stringify(status)}\n`;
999
+ }
1000
+ function renderRivusAudioCliInstallResult(result, json) {
1001
+ if (json) return `${JSON.stringify(result)}\n`;
1002
+ return result.status === "installed" ? `Installed audio transcriber ${result.id} in ${result.release}\n` : `Audio transcriber ${result.id} is already installed in ${result.release}\n`;
1003
+ }
1004
+ function renderRivusAudioCliNotInstalled() {
1005
+ return "audio transcriber is not installed; run: rivus audio install\n";
1006
+ }
1007
+ //#endregion
1008
+ //#region src/adapters/cli/audio/rivus-audio-install-lock.ts
1009
+ var RivusAudioInstallInProgressError = class extends Error {
1010
+ constructor() {
1011
+ super("another audio transcriber installation is in progress");
1012
+ this.name = "RivusAudioInstallInProgressError";
1013
+ }
1014
+ };
1015
+ const lockOwnerSchema = Schema.fromJsonString(Schema.Struct({
1016
+ pid: Schema.Int,
1017
+ startedAt: Schema.Number,
1018
+ token: Schema.NonEmptyString
1019
+ }));
1020
+ /**
1021
+ * Takes the installation lock at `path`: a directory holding one owner record named by the owner's token. The record
1022
+ * is written into a private draft directory that is then renamed onto `path`, which succeeds only while `path` is
1023
+ * absent or empty, so a reader never observes a partially written lock. A lock whose pid is dead, whose record is
1024
+ * unreadable, or which is older than `maxAgeMs` is reclaimed by removing that record by name and then `path` only
1025
+ * while it is empty, so a reclaim based on an outdated reading never removes another installer's live lock.
1026
+ */
1027
+ async function acquireRivusAudioInstallLock(path, options) {
1028
+ const now = options.now ?? Date.now;
1029
+ const isProcessAlive = options.isProcessAlive ?? isLiveProcess;
1030
+ const owner = {
1031
+ pid: process.pid,
1032
+ startedAt: now(),
1033
+ token: randomUUID()
1034
+ };
1035
+ const draft = `${path}.${owner.token}.draft`;
1036
+ await mkdir(draft, { mode: 448 });
1037
+ try {
1038
+ await writeFile(join(draft, owner.token), `${JSON.stringify(owner)}\n`, {
1039
+ flag: "wx",
1040
+ mode: 384
1041
+ });
1042
+ for (let attempt = 0; attempt < 3; attempt += 1) {
1043
+ if (await tryPublish(draft, path)) return { release: () => removeRecords(path, [owner.token]) };
1044
+ const records = await readLockRecords(path);
1045
+ if (records.some(({ owner: held }) => held !== void 0 && held.pid !== process.pid && isProcessAlive(held.pid) && now() - held.startedAt < options.maxAgeMs)) throw new RivusAudioInstallInProgressError();
1046
+ await removeRecords(path, records.map(({ name }) => name));
1047
+ }
1048
+ } finally {
1049
+ await rm(draft, {
1050
+ force: true,
1051
+ recursive: true
1052
+ }).catch(() => void 0);
1053
+ }
1054
+ throw new RivusAudioInstallInProgressError();
1055
+ }
1056
+ async function tryPublish(draft, path) {
1057
+ try {
1058
+ await rename(draft, path);
1059
+ return true;
1060
+ } catch (error) {
1061
+ if (hasErrorCode(error, "EEXIST") || hasErrorCode(error, "ENOTEMPTY")) return false;
1062
+ throw error;
1063
+ }
1064
+ }
1065
+ async function readLockRecords(path) {
1066
+ const names = await readdir(path).catch((error) => {
1067
+ if (hasErrorCode(error, "ENOENT")) return [];
1068
+ throw error;
1069
+ });
1070
+ return Promise.all(names.map(async (name) => {
1071
+ const text = await readPersistenceFile(join(path, name));
1072
+ return {
1073
+ name,
1074
+ owner: text === void 0 ? void 0 : Option.getOrUndefined(Schema.decodeUnknownOption(lockOwnerSchema)(text))
1075
+ };
1076
+ }));
1077
+ }
1078
+ /**
1079
+ * Removes the named owner records and then the lock directory only if nothing else is in it, so a record that another
1080
+ * installer published meanwhile keeps the lock held.
1081
+ */
1082
+ async function removeRecords(path, names) {
1083
+ await Promise.all(names.map((name) => unlink(join(path, name)).catch(ignoreErrorCodes("ENOENT"))));
1084
+ await rmdir(path).catch(ignoreErrorCodes("ENOENT", "ENOTEMPTY", "EEXIST"));
1085
+ }
1086
+ function ignoreErrorCodes(...codes) {
1087
+ return (error) => {
1088
+ if (!codes.some((code) => hasErrorCode(error, code))) throw error;
1089
+ };
1090
+ }
1091
+ function isLiveProcess(pid) {
1092
+ try {
1093
+ process.kill(pid, 0);
1094
+ return true;
1095
+ } catch (error) {
1096
+ return hasErrorCode(error, "EPERM");
1097
+ }
1098
+ }
1099
+ function hasErrorCode(error, code) {
1100
+ return error?.code === code;
1101
+ }
1102
+ //#endregion
1103
+ //#region src/adapters/cli/audio/rivus-audio-release.ts
1104
+ const RIVUS_AUDIO_MANIFEST_FILE = "manifest.json";
1105
+ const RIVUS_AUDIO_RELEASES_DIRECTORY = "releases";
1106
+ const manifestSchema = Schema.fromJsonString(Schema.Struct({
1107
+ id: Schema.NonEmptyString,
1108
+ smoke: Schema.Struct({
1109
+ expect: Schema.NonEmptyString,
1110
+ input: Schema.NonEmptyString
1111
+ })
1112
+ }));
1113
+ const currentSchema = Schema.fromJsonString(Schema.Struct({
1114
+ digest: Schema.optional(Schema.NonEmptyString),
1115
+ id: Schema.NonEmptyString,
1116
+ release: Schema.NonEmptyString
1117
+ }));
1118
+ function rivusAudioCurrentPath(installRoot) {
1119
+ return join(installRoot, "current.json");
1120
+ }
1121
+ async function readRivusAudioReleaseManifest(directory) {
1122
+ const path = join(directory, RIVUS_AUDIO_MANIFEST_FILE);
1123
+ const manifest = Option.getOrUndefined(Schema.decodeUnknownOption(manifestSchema)(await readFile(path, "utf8")));
1124
+ if (manifest === void 0 || !isStrictlyWithin(directory, resolve(directory, manifest.smoke.input))) throw new Error(`audio transcriber manifest is invalid: ${path}`);
1125
+ return manifest;
1126
+ }
1127
+ /**
1128
+ * Whether an entry of the asset directory, at any depth, belongs in a release. Dependencies come from the shrinkwrap
1129
+ * and colocated tests stay out of the published package, so a release never contains either and a workspace checkout
1130
+ * produces the same release and digest as the published package.
1131
+ */
1132
+ function isRivusAudioReleaseAsset(name) {
1133
+ return name !== "node_modules" && !name.endsWith(".test.ts");
1134
+ }
1135
+ /**
1136
+ * Digests the asset files that an installation copies, so an asset change reinstalls the transcriber even when the
1137
+ * manifest id stays the same.
1138
+ */
1139
+ async function readRivusAudioAssetDigest(directory) {
1140
+ const hash = createHash("sha256");
1141
+ for (const file of (await listAssetFiles(directory, "")).sort()) {
1142
+ const content = await readFile(join(directory, file));
1143
+ hash.update(`${file}\0${String(content.length)}\0`).update(content);
1144
+ }
1145
+ return hash.digest("hex");
1146
+ }
1147
+ /**
1148
+ * Reads the release that current.json selects. A missing or malformed record, a release outside `releases/`, or a
1149
+ * missing release directory all mean "not installed", which a reinstall repairs.
1150
+ */
1151
+ async function readRivusAudioInstalledRelease(installRoot) {
1152
+ const text = await readPersistenceFile(rivusAudioCurrentPath(installRoot));
1153
+ const current = text === void 0 ? void 0 : Option.getOrUndefined(Schema.decodeUnknownOption(currentSchema)(text));
1154
+ if (current === void 0) return;
1155
+ const directory = resolve(installRoot, current.release);
1156
+ if (!isStrictlyWithin(join(installRoot, "releases"), directory)) return;
1157
+ return await stat(directory).then((info) => info.isDirectory(), () => false) ? {
1158
+ ...current.digest === void 0 ? {} : { digest: current.digest },
1159
+ directory,
1160
+ id: current.id,
1161
+ release: current.release
1162
+ } : void 0;
1163
+ }
1164
+ async function readRivusAudioStatus(installRoot, assetDirectory) {
1165
+ const expected = await readRivusAudioReleaseManifest(assetDirectory);
1166
+ const expectedDigest = await readRivusAudioAssetDigest(assetDirectory);
1167
+ const installed = await readRivusAudioInstalledRelease(installRoot);
1168
+ return installed === void 0 ? {
1169
+ installed: false,
1170
+ expectedId: expected.id,
1171
+ expectedDigest,
1172
+ upToDate: false
1173
+ } : {
1174
+ installed: true,
1175
+ id: installed.id,
1176
+ ...installed.digest === void 0 ? {} : { digest: installed.digest },
1177
+ release: installed.directory,
1178
+ expectedId: expected.id,
1179
+ expectedDigest,
1180
+ upToDate: installed.id === expected.id && installed.digest === expectedDigest
1181
+ };
1182
+ }
1183
+ /** Lists the release asset files by relative path with "/" separators. */
1184
+ async function listAssetFiles(root, prefix) {
1185
+ const entries = await readdir(join(root, prefix), { withFileTypes: true });
1186
+ return (await Promise.all(entries.filter((entry) => isRivusAudioReleaseAsset(entry.name)).map(async (entry) => {
1187
+ const path = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
1188
+ return entry.isDirectory() ? listAssetFiles(root, path) : [path];
1189
+ }))).flat();
1190
+ }
1191
+ function isStrictlyWithin(root, candidate) {
1192
+ return candidate !== resolve(root) && isPathWithin(root, candidate);
1193
+ }
1194
+ //#endregion
1195
+ //#region src/adapters/cli/audio/rivus-audio-installer.ts
1196
+ const INSTALL_DEADLINE_MS = 15e5;
1197
+ const NPM_TIMEOUT_MS = 3e5;
1198
+ const FETCH_TIMEOUT_MS = 12e5;
1199
+ const SMOKE_TIMEOUT_MS = 3e5;
1200
+ const STALE_LOCK_MS = 2 * INSTALL_DEADLINE_MS;
1201
+ const LOCK_POLL_MS = 2e3;
1202
+ const STAGING_PREFIX = "staging-";
1203
+ var RivusAudioInstallStepError = class extends Error {
1204
+ step;
1205
+ constructor(step, reason, output = "") {
1206
+ const detail = output.trim().slice(-4e3);
1207
+ super(`audio transcriber installation failed at ${step}: ${reason}${detail ? `\n${detail}` : ""}`);
1208
+ this.name = "RivusAudioInstallStepError";
1209
+ this.step = step;
1210
+ }
1211
+ };
1212
+ /**
1213
+ * Builds a release in a staging directory beside `releases/`, smoke-tests it, then publishes it by renaming the
1214
+ * directory and atomically replacing current.json. A failure before current.json changes leaves the previous release
1215
+ * selected and removes the staging directory. While another installer holds the lock, this one waits within its
1216
+ * deadline and then rechecks current.json, so it does not rebuild a release that installer just published.
1217
+ */
1218
+ async function installRivusAudioTranscriber(options) {
1219
+ const now = options.now ?? Date.now;
1220
+ const deadlineMs = options.deadlineMs ?? INSTALL_DEADLINE_MS;
1221
+ const deadline = {
1222
+ at: now() + deadlineMs,
1223
+ ms: deadlineMs,
1224
+ now
1225
+ };
1226
+ const root = resolve(options.installRoot);
1227
+ await mkdir(root, { recursive: true });
1228
+ const lock = await acquireLockWhenFree(root, options, deadline);
1229
+ try {
1230
+ await removeInterruptedStaging(root);
1231
+ const manifest = await readRivusAudioReleaseManifest(options.assetDirectory);
1232
+ const digest = await readRivusAudioAssetDigest(options.assetDirectory);
1233
+ const previous = await readRivusAudioInstalledRelease(root);
1234
+ if (!options.force && previous?.id === manifest.id && previous.digest === digest) return {
1235
+ status: "already-installed",
1236
+ id: previous.id,
1237
+ release: previous.directory
1238
+ };
1239
+ const release = await buildRelease(root, manifest, digest, options, deadline);
1240
+ await pruneReleases(root, [release, previous?.directory]);
1241
+ return {
1242
+ status: "installed",
1243
+ id: manifest.id,
1244
+ release
1245
+ };
1246
+ } finally {
1247
+ await lock.release().catch(() => void 0);
1248
+ }
1249
+ }
1250
+ async function acquireLockWhenFree(root, options, deadline) {
1251
+ const sleep = options.sleep ?? ((ms) => sleepUnlessAborted(ms, options.signal));
1252
+ for (;;) {
1253
+ try {
1254
+ return await acquireRivusAudioInstallLock(join(root, "install.lock"), {
1255
+ maxAgeMs: STALE_LOCK_MS,
1256
+ ...options.isProcessAlive ? { isProcessAlive: options.isProcessAlive } : {}
1257
+ });
1258
+ } catch (error) {
1259
+ if (!(error instanceof RivusAudioInstallInProgressError)) throw error;
1260
+ }
1261
+ const remainingMs = deadline.at - deadline.now();
1262
+ if (remainingMs <= 0) throw new Error("another audio transcriber installation did not finish in time");
1263
+ await sleep(Math.min(LOCK_POLL_MS, remainingMs));
1264
+ if (options.signal?.aborted) throw new Error("audio transcriber installation was interrupted while waiting for another installation");
1265
+ }
1266
+ }
1267
+ function sleepUnlessAborted(ms, signal) {
1268
+ return setTimeout$1(ms, void 0, signal ? { signal } : {}).catch(() => void 0);
1269
+ }
1270
+ async function buildRelease(root, manifest, digest, options, deadline) {
1271
+ const staging = join(root, `${STAGING_PREFIX}${randomUUID()}`);
1272
+ const release = join(root, RIVUS_AUDIO_RELEASES_DIRECTORY, `${manifest.id.replaceAll(/[^\w.-]/g, "_")}-${digest.slice(0, 12)}-${randomUUID().slice(0, 8)}`);
1273
+ const env = withNodeOnPath(options.env, options.nodeExecutable);
1274
+ let renamed = false;
1275
+ try {
1276
+ await cp(options.assetDirectory, staging, {
1277
+ filter: (source) => isRivusAudioReleaseAsset(basename(source)),
1278
+ recursive: true
1279
+ }).catch((error) => {
1280
+ throw new RivusAudioInstallStepError("copy assets", errorMessage(error));
1281
+ });
1282
+ await runStep(options, deadline, "npm ci", {
1283
+ args: [
1284
+ "ci",
1285
+ "--ignore-scripts",
1286
+ "--omit=dev",
1287
+ "--no-audit",
1288
+ "--no-fund"
1289
+ ],
1290
+ command: "npm",
1291
+ cwd: staging,
1292
+ env,
1293
+ timeoutMs: NPM_TIMEOUT_MS
1294
+ });
1295
+ await runStep(options, deadline, "model download", {
1296
+ args: [
1297
+ "fetch-model.mjs",
1298
+ RIVUS_AUDIO_MANIFEST_FILE,
1299
+ "model"
1300
+ ],
1301
+ command: options.nodeExecutable,
1302
+ cwd: staging,
1303
+ env,
1304
+ timeoutMs: FETCH_TIMEOUT_MS
1305
+ });
1306
+ const { RIVUS_AUDIO_LANGUAGE: _language, ...smokeEnv } = env;
1307
+ const smoke = await runStep(options, deadline, "smoke test", {
1308
+ args: ["transcribe.mjs", resolve(staging, manifest.smoke.input)],
1309
+ command: options.nodeExecutable,
1310
+ cwd: staging,
1311
+ env: smokeEnv,
1312
+ timeoutMs: SMOKE_TIMEOUT_MS
1313
+ });
1314
+ if (smoke.stderrTail !== "") throw new RivusAudioInstallStepError("smoke test", "the transcriber wrote to stderr", smoke.stderrTail);
1315
+ if (!smoke.stdout.includes(manifest.smoke.expect)) throw new RivusAudioInstallStepError("smoke test", `the transcript does not contain ${JSON.stringify(manifest.smoke.expect)}`, smoke.stdout);
1316
+ await mkdir(dirname(release), { recursive: true });
1317
+ await rename(staging, release);
1318
+ renamed = true;
1319
+ await writeAtomicTextFile(rivusAudioCurrentPath(root), `${JSON.stringify({
1320
+ id: manifest.id,
1321
+ digest,
1322
+ release: relative(root, release)
1323
+ })}\n`, { durable: true });
1324
+ return release;
1325
+ } catch (error) {
1326
+ if ((renamed ? await readRivusAudioInstalledRelease(root).then((installed) => installed?.directory, () => void 0) : void 0) !== release) await rm(renamed ? release : staging, {
1327
+ force: true,
1328
+ recursive: true
1329
+ }).catch(() => void 0);
1330
+ throw error instanceof RivusAudioInstallStepError ? error : new RivusAudioInstallStepError("publish", errorMessage(error));
1331
+ }
1332
+ }
1333
+ async function runStep(options, deadline, step, request) {
1334
+ if (options.signal?.aborted) throw new RivusAudioInstallStepError(step, "interrupted");
1335
+ const deadlineReason = `the installation did not finish within ${seconds(deadline.ms)} s`;
1336
+ const remainingMs = deadline.at - deadline.now();
1337
+ if (remainingMs <= 0) throw new RivusAudioInstallStepError(step, deadlineReason);
1338
+ const timeoutMs = Math.min(request.timeoutMs, remainingMs);
1339
+ const result = await options.runProcess({
1340
+ ...request,
1341
+ timeoutMs,
1342
+ ...options.signal ? { signal: options.signal } : {}
1343
+ }).catch((error) => {
1344
+ throw new RivusAudioInstallStepError(step, `could not start ${request.command}: ${errorMessage(error)}`);
1345
+ });
1346
+ if (result.outcome === "timed-out") throw new RivusAudioInstallStepError(step, timeoutMs < request.timeoutMs ? deadlineReason : `timed out after ${seconds(request.timeoutMs)} s`, result.stderrTail);
1347
+ if (result.outcome === "interrupted") throw new RivusAudioInstallStepError(step, "interrupted", result.stderrTail);
1348
+ if (result.exitCode !== 0) throw new RivusAudioInstallStepError(step, `exited with code ${String(result.exitCode)}`, result.stderrTail);
1349
+ return result;
1350
+ }
1351
+ async function removeInterruptedStaging(root) {
1352
+ const entries = await readdir(root);
1353
+ await Promise.all(entries.filter((name) => name.startsWith(STAGING_PREFIX)).map((name) => join(root, name)).map((path) => rm(path, {
1354
+ force: true,
1355
+ maxRetries: 5,
1356
+ recursive: true
1357
+ }).catch((error) => {
1358
+ throw new RivusAudioInstallStepError("staging cleanup", `could not remove the staging directory of an interrupted installation, ${path}: ${errorMessage(error)}`);
1359
+ })));
1360
+ }
1361
+ /** Keeps the new and the previous release, so a transcription that started before the switch can still finish. */
1362
+ async function pruneReleases(root, keep) {
1363
+ const directory = join(root, RIVUS_AUDIO_RELEASES_DIRECTORY);
1364
+ const kept = new Set(keep.filter((path) => path !== void 0));
1365
+ const entries = await readdir(directory).catch(() => []);
1366
+ await Promise.all(entries.map((name) => join(directory, name)).filter((path) => !kept.has(path)).map((path) => rm(path, {
1367
+ force: true,
1368
+ recursive: true
1369
+ }).catch(() => void 0)));
1370
+ }
1371
+ /** npm may be missing from a service PATH, but it is installed beside the running Node executable. */
1372
+ function withNodeOnPath(env, nodeExecutable) {
1373
+ const nodeDirectory = dirname(nodeExecutable);
1374
+ return {
1375
+ ...env,
1376
+ PATH: env.PATH ? `${nodeDirectory}${delimiter}${env.PATH}` : nodeDirectory
1377
+ };
1378
+ }
1379
+ function seconds(ms) {
1380
+ return String(ms / 1e3);
1381
+ }
1382
+ function errorMessage(error) {
1383
+ return error instanceof Error ? error.message : String(error);
1384
+ }
1385
+ //#endregion
1386
+ //#region src/adapters/cli/audio/rivus-audio-process.ts
1387
+ const OUTPUT_LIMIT = 65536;
1388
+ const KILL_GRACE_MS = 2e3;
1389
+ /**
1390
+ * Runs one installation step. This CLI's own timeout or interruption signals only the step process, so a step must not
1391
+ * start processes of its own: npm ci runs with --ignore-scripts and the asset scripts are plain Node.
1392
+ */
1393
+ function runRivusAudioProcess(request) {
1394
+ return new Promise((resolve, reject) => {
1395
+ let stdout = "";
1396
+ let stderrTail = "";
1397
+ const child = spawnInCallerGroup(request, (text) => {
1398
+ stdout = stdout.length >= OUTPUT_LIMIT ? stdout : (stdout + text).slice(0, OUTPUT_LIMIT);
1399
+ }, (text) => {
1400
+ stderrTail = (stderrTail + text).slice(-65536);
1401
+ });
1402
+ let outcome = "exited";
1403
+ let exitCode = null;
1404
+ let exited = false;
1405
+ let escalation;
1406
+ const stop = (reason) => {
1407
+ if (exited || outcome !== "exited") return;
1408
+ outcome = reason;
1409
+ child.kill("SIGTERM");
1410
+ escalation = setTimeout(() => child.kill("SIGKILL"), KILL_GRACE_MS);
1411
+ };
1412
+ const onAbort = () => stop("interrupted");
1413
+ const timeout = setTimeout(() => stop("timed-out"), request.timeoutMs);
1414
+ const finish = () => {
1415
+ clearTimeout(timeout);
1416
+ clearTimeout(escalation);
1417
+ request.signal?.removeEventListener("abort", onAbort);
1418
+ };
1419
+ request.signal?.addEventListener("abort", onAbort, { once: true });
1420
+ if (request.signal?.aborted) onAbort();
1421
+ child.once("exit", (code) => {
1422
+ exited = true;
1423
+ exitCode = code;
1424
+ });
1425
+ child.once("error", (error) => {
1426
+ finish();
1427
+ reject(error);
1428
+ });
1429
+ child.once("close", () => {
1430
+ finish();
1431
+ resolve({
1432
+ exitCode,
1433
+ outcome,
1434
+ stderrTail,
1435
+ stdout
1436
+ });
1437
+ });
1438
+ });
1439
+ }
1440
+ /** Streams the transcriber's stdout and stderr to the caller and resolves with its exit code. */
1441
+ function runRivusAudioPassthroughProcess(request) {
1442
+ return new Promise((resolve, reject) => {
1443
+ const child = spawnInCallerGroup(request, (text) => request.stdout.write(text), (text) => request.stderr.write(text));
1444
+ const onAbort = () => {
1445
+ child.kill("SIGTERM");
1446
+ };
1447
+ request.signal?.addEventListener("abort", onAbort, { once: true });
1448
+ child.once("error", (error) => {
1449
+ request.signal?.removeEventListener("abort", onAbort);
1450
+ reject(error);
1451
+ });
1452
+ child.once("close", (code) => {
1453
+ request.signal?.removeEventListener("abort", onAbort);
1454
+ resolve(code);
1455
+ });
1456
+ });
1457
+ }
1458
+ /**
1459
+ * Starts a child in the caller's process group, because Pi stops a timed-out or aborted bash command by killing that
1460
+ * group and the child must stop with it.
1461
+ */
1462
+ function spawnInCallerGroup(request, onStdout, onStderr) {
1463
+ const child = spawn(request.command, [...request.args], {
1464
+ cwd: request.cwd,
1465
+ env: { ...request.env },
1466
+ stdio: [
1467
+ "ignore",
1468
+ "pipe",
1469
+ "pipe"
1470
+ ]
1471
+ });
1472
+ child.stdout.setEncoding("utf8").on("data", onStdout);
1473
+ child.stderr.setEncoding("utf8").on("data", onStderr);
1474
+ return child;
1475
+ }
1476
+ //#endregion
1477
+ //#region src/adapters/cli/audio/rivus-audio-cli.ts
1478
+ /** Runs one parsed `rivus audio` command and returns its exit code; installation failures are thrown. */
1479
+ async function runRivusAudioCliCommand(command, context) {
1480
+ if (command.operation === "install") {
1481
+ const result = await installRivusAudioTranscriber({
1482
+ assetDirectory: context.assetDirectory,
1483
+ env: context.env,
1484
+ force: command.force,
1485
+ installRoot: context.installRoot,
1486
+ nodeExecutable: context.nodeExecutable,
1487
+ runProcess: context.runProcess,
1488
+ ...context.signal ? { signal: context.signal } : {}
1489
+ });
1490
+ context.stdout.write(renderRivusAudioCliInstallResult(result, command.json));
1491
+ return 0;
1492
+ }
1493
+ if (command.operation === "status") {
1494
+ context.stdout.write(renderRivusAudioCliStatus(await readRivusAudioStatus(context.installRoot, context.assetDirectory)));
1495
+ return 0;
1496
+ }
1497
+ const release = await readRivusAudioInstalledRelease(context.installRoot);
1498
+ if (release === void 0) {
1499
+ context.stderr.write(renderRivusAudioCliNotInstalled());
1500
+ return 3;
1501
+ }
1502
+ const exitCode = await runRivusAudioPassthroughProcess({
1503
+ args: [join(release.directory, "transcribe.mjs"), resolve(context.cwd, command.path)],
1504
+ command: context.nodeExecutable,
1505
+ cwd: release.directory,
1506
+ env: context.env,
1507
+ ...context.signal ? { signal: context.signal } : {},
1508
+ stderr: context.stderr,
1509
+ stdout: context.stdout
1510
+ });
1511
+ return exitCode === 0 || exitCode === 1 || exitCode === 2 ? exitCode : 1;
1512
+ }
955
1513
  //#endregion
956
1514
  //#region src/adapters/cli/command/rivus-cli-protocol.ts
957
1515
  const RIVUS_CLI_USAGE = `Usage:
@@ -962,6 +1520,7 @@ const RIVUS_CLI_USAGE = `Usage:
962
1520
  rivus init [directory]
963
1521
  rivus doctor [directory] [--env-file <path>]
964
1522
  rivus model <status|set|rollback> ...
1523
+ rivus audio <install|status|transcribe> ...
965
1524
  rivus --bootstrap <module> [--manifest <rivus.config.json>] [options]
966
1525
 
967
1526
  Commands:
@@ -973,6 +1532,7 @@ Commands:
973
1532
  init Create a standalone local Rivus project without overwriting files
974
1533
  doctor Check Rivus Home by default, or an explicit standalone project directory
975
1534
  model Query or change the managed default model through the current Home
1535
+ audio Install, inspect, or run the managed audio transcriber of the current Home
976
1536
 
977
1537
  Run rivus --help for the complete daemon option list.
978
1538
  `;
@@ -1945,6 +2505,7 @@ Project commands:
1945
2505
  rivus init [directory]
1946
2506
  rivus doctor [directory] [--env-file <path>]
1947
2507
  rivus model <status|set|rollback> ...
2508
+ rivus audio <install|status|transcribe> ...
1948
2509
 
1949
2510
  Options:
1950
2511
  --bootstrap <module> Module exporting createRivusDaemonProcess(context)
@@ -3948,7 +4509,10 @@ function runRivusDaemonCli(options) {
3948
4509
  return 0;
3949
4510
  }
3950
4511
  const env = yield* Effect.tryPromise({
3951
- try: () => loadCliEnv(parsed.envFilePath, options.env),
4512
+ try: async () => {
4513
+ const loaded = await loadCliEnv(parsed.envFilePath, options.env);
4514
+ return options.env["RIVUS_HOME_DEPLOYMENT_DIR"] === void 0 ? withoutRivusHomeDeployment(loaded) : loaded;
4515
+ },
3952
4516
  catch: (error) => error
3953
4517
  });
3954
4518
  const bootstrapSpecifier = parsed.bootstrap ?? env.RIVUS_BOOTSTRAP_MODULE?.trim();
@@ -4159,11 +4723,15 @@ function runRivusCli(options) {
4159
4723
  if (command === "init") return runInitCommand(options);
4160
4724
  if (command === "doctor") return runDoctorCommand(options);
4161
4725
  if (command === "model") return runModelCommand(options);
4726
+ if (command === "audio") return runAudioCommand(options);
4162
4727
  if (command && !command.startsWith("-")) return Effect.sync(() => {
4163
4728
  options.stderr.write(renderRivusCliUnknownCommand(command));
4164
4729
  return 1;
4165
4730
  });
4166
- return runRivusDaemonCli(options);
4731
+ return runRivusDaemonCli({
4732
+ ...options,
4733
+ env: withoutRivusHomeDeployment(options.env)
4734
+ });
4167
4735
  }
4168
4736
  function runModelCommand(options) {
4169
4737
  const parsed = parseRivusModelCliArguments(options.argv.slice(1));
@@ -4223,6 +4791,38 @@ function modelTransportFailure(error) {
4223
4791
  status: "failed"
4224
4792
  };
4225
4793
  }
4794
+ function runAudioCommand(options) {
4795
+ const parsed = parseRivusAudioCliArguments(options.argv.slice(1));
4796
+ if ("help" in parsed) return Effect.sync(() => {
4797
+ options.stdout.write(RIVUS_AUDIO_CLI_USAGE);
4798
+ return 0;
4799
+ });
4800
+ if ("error" in parsed) return Effect.sync(() => {
4801
+ options.stderr.write(renderRivusAudioCliArgumentError(parsed.error));
4802
+ return 1;
4803
+ });
4804
+ return resolveHomeDirectoryEffect(options).pipe(Effect.flatMap((directory) => options.homeApi.load(directory)), Effect.flatMap((home) => Effect.tryPromise({
4805
+ try: () => runRivusAudioCliCommand(parsed, {
4806
+ assetDirectory: gatewayAudioTranscriberAssetDirectory,
4807
+ cwd: options.cwd,
4808
+ env: options.env,
4809
+ installRoot: join(home.stateDirectory, "audio-transcriber"),
4810
+ nodeExecutable: options.nodeExecutable,
4811
+ runProcess: runRivusAudioProcess,
4812
+ signal: abortOnInterruption(options.signalSource),
4813
+ stderr: options.stderr,
4814
+ stdout: options.stdout
4815
+ }),
4816
+ catch: (error) => error
4817
+ })), Effect.catch((error) => writeError(options, error)));
4818
+ }
4819
+ /** Lets an interrupted install stop its child processes and remove its staging directory before the CLI exits. */
4820
+ function abortOnInterruption(signalSource) {
4821
+ const controller = new AbortController();
4822
+ signalSource.on("SIGINT", () => controller.abort());
4823
+ signalSource.on("SIGTERM", () => controller.abort());
4824
+ return controller.signal;
4825
+ }
4226
4826
  function runSetupCommand(options) {
4227
4827
  return withOptionalDirectoryArgument(options, "setup", (argument) => resolveHomeDirectoryEffect(options, argument).pipe(Effect.flatMap((directory) => options.homeApi.setup(directory).pipe(Effect.tap(() => Effect.sync(() => options.stdout.write(renderRivusSetupSuccess(directory)))), Effect.as(0))), Effect.catch((error) => writeError(options, error))));
4228
4828
  }
@@ -4245,7 +4845,8 @@ function runHomeDaemonCommand(options, command, daemonArgs) {
4245
4845
  env: {
4246
4846
  ...options.env,
4247
4847
  RIVUS_DEPLOYMENT_STATE_DIR: home.stateDirectory,
4248
- RIVUS_HOME: home.directory
4848
+ RIVUS_HOME: home.directory,
4849
+ [RIVUS_HOME_DEPLOYMENT_ENV]: home.directory
4249
4850
  },
4250
4851
  pluginPackageManifestPath: options.pluginPackageManifestPath ?? gatewayPackageManifestPath
4251
4852
  })), Effect.catch((error) => writeError(options, error)));