@exulu/backend 4.1.0 → 4.2.1
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/bin/setup-python.cjs +18 -14
- package/dist/{chunk-RBEWHG7I.js → chunk-D4PCOOL5.js} +65 -9
- package/dist/{chunk-QMN6MVHQ.js → chunk-JIZCKSJO.js} +9 -16
- package/dist/cli/start-whisper.js +1 -1
- package/dist/{convert-exulu-tools-to-ai-sdk-tools-6RU4IZMI.js → convert-exulu-tools-to-ai-sdk-tools-AHRL4TN4.js} +1 -1
- package/dist/index.cjs +180 -45
- package/dist/index.d.cts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +94 -11
- package/dist/{python-setup-DRJ3QX5F.js → python-setup-UYSNAW5V.js} +1 -1
- package/package.json +1 -1
- package/scripts/postinstall.cjs +13 -7
package/bin/setup-python.cjs
CHANGED
|
@@ -98,20 +98,24 @@ async function main() {
|
|
|
98
98
|
console.log(`${colors.blue}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${colors.reset}`);
|
|
99
99
|
console.log('');
|
|
100
100
|
|
|
101
|
-
//
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if (
|
|
114
|
-
console.log(
|
|
101
|
+
// We deliberately do NOT skip running setup.sh just because the venv
|
|
102
|
+
// directory and a `python` binary already exist (pythonEnvironmentExists()
|
|
103
|
+
// only checks that much, not that requirements.txt is fully installed).
|
|
104
|
+
// ee/python/setup.sh is itself idempotent — it only skips *recreating* the
|
|
105
|
+
// venv, never skips `pip install -r requirements.txt` — so always invoking
|
|
106
|
+
// it is cheap when nothing is missing (a few seconds) and is the only way
|
|
107
|
+
// to guarantee a venv created before a new requirement was added (e.g.
|
|
108
|
+
// python-docx, 2026-09-11) actually gets it installed. This used to return
|
|
109
|
+
// early here whenever the venv folder was merely present, which silently
|
|
110
|
+
// left newly-added dependencies uninstalled across any deploy that reused
|
|
111
|
+
// an older venv — see the docx-manipulation skill incident on dx-algi,
|
|
112
|
+
// 2026-09-21.
|
|
113
|
+
if (pythonEnvironmentExists()) {
|
|
114
|
+
console.log(
|
|
115
|
+
force
|
|
116
|
+
? `${colors.yellow}⚠${colors.reset} Rebuilding Python environment (--force flag detected)`
|
|
117
|
+
: `${colors.blue}ℹ${colors.reset} Python environment directory already exists — verifying and installing any missing dependencies...`,
|
|
118
|
+
);
|
|
115
119
|
console.log('');
|
|
116
120
|
}
|
|
117
121
|
|
|
@@ -1716,7 +1716,7 @@ var ExuluTool = class _ExuluTool {
|
|
|
1716
1716
|
if (!agent) {
|
|
1717
1717
|
throw new Error("Agent not found.");
|
|
1718
1718
|
}
|
|
1719
|
-
const { convertExuluToolsToAiSdkTools: convertExuluToolsToAiSdkTools2 } = await import("./convert-exulu-tools-to-ai-sdk-tools-
|
|
1719
|
+
const { convertExuluToolsToAiSdkTools: convertExuluToolsToAiSdkTools2 } = await import("./convert-exulu-tools-to-ai-sdk-tools-AHRL4TN4.js");
|
|
1720
1720
|
const tools = await convertExuluToolsToAiSdkTools2(
|
|
1721
1721
|
[this],
|
|
1722
1722
|
[],
|
|
@@ -6308,7 +6308,7 @@ async function downloadKeyIntoSandbox(opts) {
|
|
|
6308
6308
|
}
|
|
6309
6309
|
async function resolvePythonVenvPath() {
|
|
6310
6310
|
try {
|
|
6311
|
-
const { getPythonVenvPath } = await import("./python-setup-
|
|
6311
|
+
const { getPythonVenvPath } = await import("./python-setup-UYSNAW5V.js");
|
|
6312
6312
|
return getPythonVenvPath();
|
|
6313
6313
|
} catch (err) {
|
|
6314
6314
|
console.warn("[SKILLS] Could not resolve the Python venv for the session sandbox; skill scripts fall back to the system python.", err);
|
|
@@ -6961,11 +6961,50 @@ import { parseOfficeAsync } from "officeparser";
|
|
|
6961
6961
|
// src/templates/tools/document-render-helpers.ts
|
|
6962
6962
|
import { execFile } from "child_process";
|
|
6963
6963
|
import { promisify as promisify3 } from "util";
|
|
6964
|
+
import { existsSync as existsSync4 } from "fs";
|
|
6964
6965
|
import { mkdtemp, readdir as readdir2, readFile, rm as rm2, writeFile as writeFile2 } from "fs/promises";
|
|
6965
6966
|
import { tmpdir } from "os";
|
|
6966
6967
|
import { join as join3 } from "path";
|
|
6967
6968
|
var execFileAsync = promisify3(execFile);
|
|
6968
6969
|
var MAX_STDOUT_BYTES = 64 * 1024 * 1024;
|
|
6970
|
+
var LEGACY_OFFICE_CONVERSION_TARGETS = {
|
|
6971
|
+
".doc": "docx",
|
|
6972
|
+
".xls": "xlsx",
|
|
6973
|
+
".ppt": "pptx",
|
|
6974
|
+
".rtf": "docx"
|
|
6975
|
+
};
|
|
6976
|
+
function isLegacyOfficeFormat(ext) {
|
|
6977
|
+
return ext in LEGACY_OFFICE_CONVERSION_TARGETS;
|
|
6978
|
+
}
|
|
6979
|
+
async function convertLegacyOfficeToModern(bytes, ext) {
|
|
6980
|
+
const target = LEGACY_OFFICE_CONVERSION_TARGETS[ext];
|
|
6981
|
+
if (!target) {
|
|
6982
|
+
throw new Error(`No modern-format conversion target registered for "${ext}"`);
|
|
6983
|
+
}
|
|
6984
|
+
const dir = await mkdtemp(join3(tmpdir(), "exulu-officeconv-"));
|
|
6985
|
+
try {
|
|
6986
|
+
const inputPath = join3(dir, `input${ext}`);
|
|
6987
|
+
await writeFile2(inputPath, bytes);
|
|
6988
|
+
try {
|
|
6989
|
+
await execFileAsync(
|
|
6990
|
+
"soffice",
|
|
6991
|
+
["--headless", "--convert-to", target, inputPath, "--outdir", dir],
|
|
6992
|
+
{ timeout: 6e4, maxBuffer: MAX_STDOUT_BYTES }
|
|
6993
|
+
);
|
|
6994
|
+
} catch (err) {
|
|
6995
|
+
throw new Error(
|
|
6996
|
+
`LibreOffice could not convert the legacy "${ext}" file to ${target}: ` + (err instanceof Error ? err.message : String(err))
|
|
6997
|
+
);
|
|
6998
|
+
}
|
|
6999
|
+
const outputPath = join3(dir, `input.${target}`);
|
|
7000
|
+
if (!existsSync4(outputPath)) {
|
|
7001
|
+
throw new Error(`LibreOffice produced no output converting "${ext}" to ${target}`);
|
|
7002
|
+
}
|
|
7003
|
+
return await readFile(outputPath);
|
|
7004
|
+
} finally {
|
|
7005
|
+
await rm2(dir, { recursive: true, force: true });
|
|
7006
|
+
}
|
|
7007
|
+
}
|
|
6969
7008
|
async function pdfToText(pdf) {
|
|
6970
7009
|
const dir = await mkdtemp(join3(tmpdir(), "exulu-parse-"));
|
|
6971
7010
|
try {
|
|
@@ -7032,6 +7071,18 @@ var OFFICE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
|
7032
7071
|
".rtf"
|
|
7033
7072
|
]);
|
|
7034
7073
|
var pagesPattern = /^(\d+)(?:-(\d+))?$/;
|
|
7074
|
+
var BINARY_DOCUMENT_EXTENSION_PATTERN = new RegExp(
|
|
7075
|
+
`([^\\s"'\`]+\\.(?:pdf|${[...OFFICE_EXTENSIONS].map((ext) => ext.slice(1)).join("|")}))\\b`,
|
|
7076
|
+
"i"
|
|
7077
|
+
);
|
|
7078
|
+
function binaryDocumentBashHint(command, stdout, stderr) {
|
|
7079
|
+
const match = command.match(BINARY_DOCUMENT_EXTENSION_PATTERN);
|
|
7080
|
+
if (!match) return void 0;
|
|
7081
|
+
const silent = !stdout.trim() && !stderr.trim();
|
|
7082
|
+
if (!silent && !looksLikeGarbledTextLayer(stdout)) return void 0;
|
|
7083
|
+
const [, file] = match;
|
|
7084
|
+
return `Note: "${file}" is a binary document \u2014 grep/cat/text tools cannot read the text inside it, so a silent or garbled result does not mean the content isn't there. Use parse_document to extract its text first, then search or read within that extracted text instead of the original file.`;
|
|
7085
|
+
}
|
|
7035
7086
|
var createParseDocumentTool = ({
|
|
7036
7087
|
sessionID,
|
|
7037
7088
|
user,
|
|
@@ -7101,7 +7152,8 @@ var createParseDocumentTool = ({
|
|
|
7101
7152
|
fullText = pageTexts.map((text, i) => ({ page: i + 1, text })).filter(({ page }) => page >= range[0] && page <= range[1]).map(({ page, text }) => `--- page ${page} ---
|
|
7102
7153
|
${text.trim()}`).join("\n");
|
|
7103
7154
|
} else {
|
|
7104
|
-
const
|
|
7155
|
+
const officeBytes = isLegacyOfficeFormat(ext) ? await convertLegacyOfficeToModern(bytes, ext) : bytes;
|
|
7156
|
+
const extracted = await parseOfficeAsync(officeBytes, {
|
|
7105
7157
|
outputErrorToConsole: false,
|
|
7106
7158
|
newlineDelimiter: "\n"
|
|
7107
7159
|
});
|
|
@@ -7156,7 +7208,7 @@ import { extname as extname3 } from "path";
|
|
|
7156
7208
|
|
|
7157
7209
|
// src/sessions/pdf-preview-cache.ts
|
|
7158
7210
|
import { exec as exec3 } from "child_process";
|
|
7159
|
-
import { existsSync as
|
|
7211
|
+
import { existsSync as existsSync5 } from "fs";
|
|
7160
7212
|
import { mkdir as mkdir2, readFile as readFile2, rename, rm as rm3, writeFile as writeFile3 } from "fs/promises";
|
|
7161
7213
|
import { extname as extname2, join as join4 } from "path";
|
|
7162
7214
|
import { promisify as promisify4 } from "util";
|
|
@@ -7181,7 +7233,7 @@ async function getPdfPreviewBytes(opts) {
|
|
|
7181
7233
|
throw new PreviewRenderError(`Invalid ETag for ${sourceKey}`);
|
|
7182
7234
|
}
|
|
7183
7235
|
const cachedPath = join4(CACHE_ROOT, `${safeEtag}.pdf`);
|
|
7184
|
-
if (
|
|
7236
|
+
if (existsSync5(cachedPath)) {
|
|
7185
7237
|
return readFile2(cachedPath);
|
|
7186
7238
|
}
|
|
7187
7239
|
const existing = inFlight.get(safeEtag);
|
|
@@ -7206,7 +7258,7 @@ async function getPdfPreviewBytes(opts) {
|
|
|
7206
7258
|
`LibreOffice conversion failed for ${sourceKey} (etag ${etag}): ${err?.stderr ?? err?.message ?? "unknown error"}`
|
|
7207
7259
|
);
|
|
7208
7260
|
}
|
|
7209
|
-
if (!
|
|
7261
|
+
if (!existsSync5(outputPath)) {
|
|
7210
7262
|
throw new PreviewRenderError(
|
|
7211
7263
|
`LibreOffice produced no output for ${sourceKey} (etag ${etag})`
|
|
7212
7264
|
);
|
|
@@ -8230,13 +8282,17 @@ var convertExuluToolsToAiSdkTools = async (currentTools, currentSkills, approved
|
|
|
8230
8282
|
const origExecute = sharedSessionSandbox.tools.bash.execute;
|
|
8231
8283
|
if (!origExecute) throw new Error("bash execute is undefined");
|
|
8232
8284
|
const result = await origExecute(args, opts);
|
|
8285
|
+
const rawStdout = typeof result?.stdout === "string" ? result.stdout : "";
|
|
8286
|
+
const rawStderr = typeof result?.stderr === "string" ? result.stderr : "";
|
|
8287
|
+
const hint = binaryDocumentBashHint(String(args?.command ?? ""), rawStdout, rawStderr);
|
|
8288
|
+
const stderrWithHint = hint ? [rawStderr, hint].filter(Boolean).join("\n\n") : rawStderr;
|
|
8233
8289
|
return {
|
|
8234
8290
|
...result,
|
|
8235
8291
|
...typeof result?.stdout === "string" && {
|
|
8236
|
-
stdout: truncateToolOutput(
|
|
8292
|
+
stdout: truncateToolOutput(rawStdout, budget.contextWindow, "bash", 0.1, toolOutputCharLimit)
|
|
8237
8293
|
},
|
|
8238
|
-
...typeof result?.stderr === "string" && {
|
|
8239
|
-
stderr: truncateToolOutput(
|
|
8294
|
+
...(typeof result?.stderr === "string" || hint) && {
|
|
8295
|
+
stderr: truncateToolOutput(stderrWithHint, budget.contextWindow, "bash stderr", 0.4, toolOutputCharLimit)
|
|
8240
8296
|
}
|
|
8241
8297
|
};
|
|
8242
8298
|
}
|
|
@@ -57,15 +57,11 @@ async function setupPythonEnvironment(options = {}) {
|
|
|
57
57
|
timeout = 6e5
|
|
58
58
|
// 10 minutes
|
|
59
59
|
} = options;
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
success: true,
|
|
66
|
-
message: "Python environment already exists",
|
|
67
|
-
alreadyExists: true
|
|
68
|
-
};
|
|
60
|
+
const alreadyExisted = isPythonEnvironmentSetup(packageRoot);
|
|
61
|
+
if (verbose) {
|
|
62
|
+
console.log(
|
|
63
|
+
alreadyExisted ? "Python environment already exists \u2014 verifying and installing any missing dependencies..." : "Setting up Python environment..."
|
|
64
|
+
);
|
|
69
65
|
}
|
|
70
66
|
const setupScriptPath = getSetupScriptPath(packageRoot);
|
|
71
67
|
if (!existsSync(setupScriptPath)) {
|
|
@@ -76,9 +72,6 @@ async function setupPythonEnvironment(options = {}) {
|
|
|
76
72
|
};
|
|
77
73
|
}
|
|
78
74
|
try {
|
|
79
|
-
if (verbose) {
|
|
80
|
-
console.log("Setting up Python environment...");
|
|
81
|
-
}
|
|
82
75
|
const { stdout, stderr } = await execAsync(`bash "${setupScriptPath}"`, {
|
|
83
76
|
cwd: packageRoot,
|
|
84
77
|
timeout,
|
|
@@ -98,8 +91,8 @@ async function setupPythonEnvironment(options = {}) {
|
|
|
98
91
|
}
|
|
99
92
|
return {
|
|
100
93
|
success: true,
|
|
101
|
-
message: "Python environment set up successfully",
|
|
102
|
-
alreadyExists:
|
|
94
|
+
message: alreadyExisted ? "Python environment verified (dependencies were already installed or have been updated)" : "Python environment set up successfully",
|
|
95
|
+
alreadyExists: alreadyExisted,
|
|
103
96
|
pythonVersion,
|
|
104
97
|
output
|
|
105
98
|
};
|
|
@@ -108,7 +101,7 @@ async function setupPythonEnvironment(options = {}) {
|
|
|
108
101
|
return {
|
|
109
102
|
success: false,
|
|
110
103
|
message: `Setup failed: ${error.message}`,
|
|
111
|
-
alreadyExists:
|
|
104
|
+
alreadyExists: alreadyExisted,
|
|
112
105
|
output: errorOutput
|
|
113
106
|
};
|
|
114
107
|
}
|
|
@@ -171,7 +164,7 @@ async function validatePythonEnvironment(packageRoot, checkPackages = true) {
|
|
|
171
164
|
};
|
|
172
165
|
}
|
|
173
166
|
if (checkPackages) {
|
|
174
|
-
const criticalPackages = ["pypdf", "transformers"];
|
|
167
|
+
const criticalPackages = ["pypdf", "transformers", "docx"];
|
|
175
168
|
const missingPackages = [];
|
|
176
169
|
for (const pkg of criticalPackages) {
|
|
177
170
|
try {
|
package/dist/index.cjs
CHANGED
|
@@ -3950,15 +3950,11 @@ async function setupPythonEnvironment(options = {}) {
|
|
|
3950
3950
|
timeout = 6e5
|
|
3951
3951
|
// 10 minutes
|
|
3952
3952
|
} = options;
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
success: true,
|
|
3959
|
-
message: "Python environment already exists",
|
|
3960
|
-
alreadyExists: true
|
|
3961
|
-
};
|
|
3953
|
+
const alreadyExisted = isPythonEnvironmentSetup(packageRoot);
|
|
3954
|
+
if (verbose) {
|
|
3955
|
+
console.log(
|
|
3956
|
+
alreadyExisted ? "Python environment already exists \u2014 verifying and installing any missing dependencies..." : "Setting up Python environment..."
|
|
3957
|
+
);
|
|
3962
3958
|
}
|
|
3963
3959
|
const setupScriptPath = getSetupScriptPath(packageRoot);
|
|
3964
3960
|
if (!(0, import_fs.existsSync)(setupScriptPath)) {
|
|
@@ -3969,9 +3965,6 @@ async function setupPythonEnvironment(options = {}) {
|
|
|
3969
3965
|
};
|
|
3970
3966
|
}
|
|
3971
3967
|
try {
|
|
3972
|
-
if (verbose) {
|
|
3973
|
-
console.log("Setting up Python environment...");
|
|
3974
|
-
}
|
|
3975
3968
|
const { stdout, stderr } = await execAsync2(`bash "${setupScriptPath}"`, {
|
|
3976
3969
|
cwd: packageRoot,
|
|
3977
3970
|
timeout,
|
|
@@ -3991,8 +3984,8 @@ async function setupPythonEnvironment(options = {}) {
|
|
|
3991
3984
|
}
|
|
3992
3985
|
return {
|
|
3993
3986
|
success: true,
|
|
3994
|
-
message: "Python environment set up successfully",
|
|
3995
|
-
alreadyExists:
|
|
3987
|
+
message: alreadyExisted ? "Python environment verified (dependencies were already installed or have been updated)" : "Python environment set up successfully",
|
|
3988
|
+
alreadyExists: alreadyExisted,
|
|
3996
3989
|
pythonVersion,
|
|
3997
3990
|
output
|
|
3998
3991
|
};
|
|
@@ -4001,7 +3994,7 @@ async function setupPythonEnvironment(options = {}) {
|
|
|
4001
3994
|
return {
|
|
4002
3995
|
success: false,
|
|
4003
3996
|
message: `Setup failed: ${error.message}`,
|
|
4004
|
-
alreadyExists:
|
|
3997
|
+
alreadyExists: alreadyExisted,
|
|
4005
3998
|
output: errorOutput
|
|
4006
3999
|
};
|
|
4007
4000
|
}
|
|
@@ -4064,7 +4057,7 @@ async function validatePythonEnvironment(packageRoot, checkPackages = true) {
|
|
|
4064
4057
|
};
|
|
4065
4058
|
}
|
|
4066
4059
|
if (checkPackages) {
|
|
4067
|
-
const criticalPackages = ["pypdf", "transformers"];
|
|
4060
|
+
const criticalPackages = ["pypdf", "transformers", "docx"];
|
|
4068
4061
|
const missingPackages = [];
|
|
4069
4062
|
for (const pkg of criticalPackages) {
|
|
4070
4063
|
try {
|
|
@@ -5013,6 +5006,38 @@ var init_session_file_read_tool = __esm({
|
|
|
5013
5006
|
});
|
|
5014
5007
|
|
|
5015
5008
|
// src/templates/tools/document-render-helpers.ts
|
|
5009
|
+
function isLegacyOfficeFormat(ext) {
|
|
5010
|
+
return ext in LEGACY_OFFICE_CONVERSION_TARGETS;
|
|
5011
|
+
}
|
|
5012
|
+
async function convertLegacyOfficeToModern(bytes, ext) {
|
|
5013
|
+
const target = LEGACY_OFFICE_CONVERSION_TARGETS[ext];
|
|
5014
|
+
if (!target) {
|
|
5015
|
+
throw new Error(`No modern-format conversion target registered for "${ext}"`);
|
|
5016
|
+
}
|
|
5017
|
+
const dir = await (0, import_promises2.mkdtemp)((0, import_node_path5.join)((0, import_node_os.tmpdir)(), "exulu-officeconv-"));
|
|
5018
|
+
try {
|
|
5019
|
+
const inputPath = (0, import_node_path5.join)(dir, `input${ext}`);
|
|
5020
|
+
await (0, import_promises2.writeFile)(inputPath, bytes);
|
|
5021
|
+
try {
|
|
5022
|
+
await execFileAsync(
|
|
5023
|
+
"soffice",
|
|
5024
|
+
["--headless", "--convert-to", target, inputPath, "--outdir", dir],
|
|
5025
|
+
{ timeout: 6e4, maxBuffer: MAX_STDOUT_BYTES }
|
|
5026
|
+
);
|
|
5027
|
+
} catch (err) {
|
|
5028
|
+
throw new Error(
|
|
5029
|
+
`LibreOffice could not convert the legacy "${ext}" file to ${target}: ` + (err instanceof Error ? err.message : String(err))
|
|
5030
|
+
);
|
|
5031
|
+
}
|
|
5032
|
+
const outputPath = (0, import_node_path5.join)(dir, `input.${target}`);
|
|
5033
|
+
if (!(0, import_node_fs6.existsSync)(outputPath)) {
|
|
5034
|
+
throw new Error(`LibreOffice produced no output converting "${ext}" to ${target}`);
|
|
5035
|
+
}
|
|
5036
|
+
return await (0, import_promises2.readFile)(outputPath);
|
|
5037
|
+
} finally {
|
|
5038
|
+
await (0, import_promises2.rm)(dir, { recursive: true, force: true });
|
|
5039
|
+
}
|
|
5040
|
+
}
|
|
5016
5041
|
async function pdfToText(pdf) {
|
|
5017
5042
|
const dir = await (0, import_promises2.mkdtemp)((0, import_node_path5.join)((0, import_node_os.tmpdir)(), "exulu-parse-"));
|
|
5018
5043
|
try {
|
|
@@ -5049,18 +5074,25 @@ async function renderPdfPageToPng(pdf, page, scaleTo) {
|
|
|
5049
5074
|
await (0, import_promises2.rm)(dir, { recursive: true, force: true });
|
|
5050
5075
|
}
|
|
5051
5076
|
}
|
|
5052
|
-
var import_node_child_process4, import_node_util3, import_promises2, import_node_os, import_node_path5, execFileAsync, MAX_STDOUT_BYTES;
|
|
5077
|
+
var import_node_child_process4, import_node_util3, import_node_fs6, import_promises2, import_node_os, import_node_path5, execFileAsync, MAX_STDOUT_BYTES, LEGACY_OFFICE_CONVERSION_TARGETS;
|
|
5053
5078
|
var init_document_render_helpers = __esm({
|
|
5054
5079
|
"src/templates/tools/document-render-helpers.ts"() {
|
|
5055
5080
|
"use strict";
|
|
5056
5081
|
init_cjs_shims();
|
|
5057
5082
|
import_node_child_process4 = require("child_process");
|
|
5058
5083
|
import_node_util3 = require("util");
|
|
5084
|
+
import_node_fs6 = require("fs");
|
|
5059
5085
|
import_promises2 = require("fs/promises");
|
|
5060
5086
|
import_node_os = require("os");
|
|
5061
5087
|
import_node_path5 = require("path");
|
|
5062
5088
|
execFileAsync = (0, import_node_util3.promisify)(import_node_child_process4.execFile);
|
|
5063
5089
|
MAX_STDOUT_BYTES = 64 * 1024 * 1024;
|
|
5090
|
+
LEGACY_OFFICE_CONVERSION_TARGETS = {
|
|
5091
|
+
".doc": "docx",
|
|
5092
|
+
".xls": "xlsx",
|
|
5093
|
+
".ppt": "pptx",
|
|
5094
|
+
".rtf": "docx"
|
|
5095
|
+
};
|
|
5064
5096
|
}
|
|
5065
5097
|
});
|
|
5066
5098
|
|
|
@@ -5077,7 +5109,15 @@ function looksLikeGarbledTextLayer(text) {
|
|
|
5077
5109
|
if (visible < 40) return false;
|
|
5078
5110
|
return control / visible > 0.01;
|
|
5079
5111
|
}
|
|
5080
|
-
|
|
5112
|
+
function binaryDocumentBashHint(command, stdout, stderr) {
|
|
5113
|
+
const match = command.match(BINARY_DOCUMENT_EXTENSION_PATTERN);
|
|
5114
|
+
if (!match) return void 0;
|
|
5115
|
+
const silent = !stdout.trim() && !stderr.trim();
|
|
5116
|
+
if (!silent && !looksLikeGarbledTextLayer(stdout)) return void 0;
|
|
5117
|
+
const [, file] = match;
|
|
5118
|
+
return `Note: "${file}" is a binary document \u2014 grep/cat/text tools cannot read the text inside it, so a silent or garbled result does not mean the content isn't there. Use parse_document to extract its text first, then search or read within that extracted text instead of the original file.`;
|
|
5119
|
+
}
|
|
5120
|
+
var import_zod8, import_node_path6, import_officeparser, DEFAULT_LIMIT2, MAX_CONTENT_CHARS2, MIN_CHARS_PER_PAGE, OFFICE_EXTENSIONS, pagesPattern, BINARY_DOCUMENT_EXTENSION_PATTERN, createParseDocumentTool;
|
|
5081
5121
|
var init_parse_document_tool = __esm({
|
|
5082
5122
|
"src/templates/tools/parse-document-tool.ts"() {
|
|
5083
5123
|
"use strict";
|
|
@@ -5105,6 +5145,10 @@ var init_parse_document_tool = __esm({
|
|
|
5105
5145
|
".rtf"
|
|
5106
5146
|
]);
|
|
5107
5147
|
pagesPattern = /^(\d+)(?:-(\d+))?$/;
|
|
5148
|
+
BINARY_DOCUMENT_EXTENSION_PATTERN = new RegExp(
|
|
5149
|
+
`([^\\s"'\`]+\\.(?:pdf|${[...OFFICE_EXTENSIONS].map((ext) => ext.slice(1)).join("|")}))\\b`,
|
|
5150
|
+
"i"
|
|
5151
|
+
);
|
|
5108
5152
|
createParseDocumentTool = ({
|
|
5109
5153
|
sessionID,
|
|
5110
5154
|
user,
|
|
@@ -5174,7 +5218,8 @@ var init_parse_document_tool = __esm({
|
|
|
5174
5218
|
fullText = pageTexts.map((text, i) => ({ page: i + 1, text })).filter(({ page }) => page >= range[0] && page <= range[1]).map(({ page, text }) => `--- page ${page} ---
|
|
5175
5219
|
${text.trim()}`).join("\n");
|
|
5176
5220
|
} else {
|
|
5177
|
-
const
|
|
5221
|
+
const officeBytes = isLegacyOfficeFormat(ext) ? await convertLegacyOfficeToModern(bytes, ext) : bytes;
|
|
5222
|
+
const extracted = await (0, import_officeparser.parseOfficeAsync)(officeBytes, {
|
|
5178
5223
|
outputErrorToConsole: false,
|
|
5179
5224
|
newlineDelimiter: "\n"
|
|
5180
5225
|
});
|
|
@@ -5236,7 +5281,7 @@ async function getPdfPreviewBytes(opts) {
|
|
|
5236
5281
|
throw new PreviewRenderError(`Invalid ETag for ${sourceKey}`);
|
|
5237
5282
|
}
|
|
5238
5283
|
const cachedPath = (0, import_node_path7.join)(CACHE_ROOT, `${safeEtag}.pdf`);
|
|
5239
|
-
if ((0,
|
|
5284
|
+
if ((0, import_node_fs7.existsSync)(cachedPath)) {
|
|
5240
5285
|
return (0, import_promises3.readFile)(cachedPath);
|
|
5241
5286
|
}
|
|
5242
5287
|
const existing = inFlight.get(safeEtag);
|
|
@@ -5261,7 +5306,7 @@ async function getPdfPreviewBytes(opts) {
|
|
|
5261
5306
|
`LibreOffice conversion failed for ${sourceKey} (etag ${etag}): ${err?.stderr ?? err?.message ?? "unknown error"}`
|
|
5262
5307
|
);
|
|
5263
5308
|
}
|
|
5264
|
-
if (!(0,
|
|
5309
|
+
if (!(0, import_node_fs7.existsSync)(outputPath)) {
|
|
5265
5310
|
throw new PreviewRenderError(
|
|
5266
5311
|
`LibreOffice produced no output for ${sourceKey} (etag ${etag})`
|
|
5267
5312
|
);
|
|
@@ -5278,13 +5323,13 @@ async function getPdfPreviewBytes(opts) {
|
|
|
5278
5323
|
inFlight.set(safeEtag, promise);
|
|
5279
5324
|
return promise;
|
|
5280
5325
|
}
|
|
5281
|
-
var import_node_child_process5,
|
|
5326
|
+
var import_node_child_process5, import_node_fs7, import_promises3, import_node_path7, import_node_util4, execAsync4, CACHE_ROOT, CACHE_IN, CACHE_OUT, inFlight, PreviewRenderError;
|
|
5282
5327
|
var init_pdf_preview_cache = __esm({
|
|
5283
5328
|
"src/sessions/pdf-preview-cache.ts"() {
|
|
5284
5329
|
"use strict";
|
|
5285
5330
|
init_cjs_shims();
|
|
5286
5331
|
import_node_child_process5 = require("child_process");
|
|
5287
|
-
|
|
5332
|
+
import_node_fs7 = require("fs");
|
|
5288
5333
|
import_promises3 = require("fs/promises");
|
|
5289
5334
|
import_node_path7 = require("path");
|
|
5290
5335
|
import_node_util4 = require("util");
|
|
@@ -6540,13 +6585,17 @@ var init_convert_exulu_tools_to_ai_sdk_tools = __esm({
|
|
|
6540
6585
|
const origExecute = sharedSessionSandbox.tools.bash.execute;
|
|
6541
6586
|
if (!origExecute) throw new Error("bash execute is undefined");
|
|
6542
6587
|
const result = await origExecute(args, opts);
|
|
6588
|
+
const rawStdout = typeof result?.stdout === "string" ? result.stdout : "";
|
|
6589
|
+
const rawStderr = typeof result?.stderr === "string" ? result.stderr : "";
|
|
6590
|
+
const hint = binaryDocumentBashHint(String(args?.command ?? ""), rawStdout, rawStderr);
|
|
6591
|
+
const stderrWithHint = hint ? [rawStderr, hint].filter(Boolean).join("\n\n") : rawStderr;
|
|
6543
6592
|
return {
|
|
6544
6593
|
...result,
|
|
6545
6594
|
...typeof result?.stdout === "string" && {
|
|
6546
|
-
stdout: truncateToolOutput(
|
|
6595
|
+
stdout: truncateToolOutput(rawStdout, budget.contextWindow, "bash", 0.1, toolOutputCharLimit)
|
|
6547
6596
|
},
|
|
6548
|
-
...typeof result?.stderr === "string" && {
|
|
6549
|
-
stderr: truncateToolOutput(
|
|
6597
|
+
...(typeof result?.stderr === "string" || hint) && {
|
|
6598
|
+
stderr: truncateToolOutput(stderrWithHint, budget.contextWindow, "bash stderr", 0.4, toolOutputCharLimit)
|
|
6550
6599
|
}
|
|
6551
6600
|
};
|
|
6552
6601
|
}
|
|
@@ -10751,6 +10800,10 @@ async function resolveEmbedder(input) {
|
|
|
10751
10800
|
};
|
|
10752
10801
|
}
|
|
10753
10802
|
|
|
10803
|
+
// types/rbac-rights-modes.ts
|
|
10804
|
+
init_cjs_shims();
|
|
10805
|
+
var VALID_RIGHTS_MODES = ["private", "users", "roles", "teams", "public"];
|
|
10806
|
+
|
|
10754
10807
|
// src/exulu/context.ts
|
|
10755
10808
|
init_statistics2();
|
|
10756
10809
|
init_statistics();
|
|
@@ -12365,7 +12418,12 @@ var transcriptionJobsSchema = {
|
|
|
12365
12418
|
// Selected per-meeting post-processing: [{ prompt_id, agent_id }].
|
|
12366
12419
|
{ name: "post_processing_prompts", type: "json" },
|
|
12367
12420
|
// Results: [{ prompt_id, agent_id, prompt_name, status, output, error, ran_at }].
|
|
12368
|
-
{ name: "post_processing_outputs", type: "json" }
|
|
12421
|
+
{ name: "post_processing_outputs", type: "json" },
|
|
12422
|
+
// Permanent local copy of the meeting's mixed MP4, downloaded from Recall
|
|
12423
|
+
// when RECALL_STORE_VIDEO_LOCALLY=true. Null when that flag is off (the
|
|
12424
|
+
// video then stays reachable only via ExuluRecall.getRecordingVideoUrl,
|
|
12425
|
+
// for as long as Recall itself retains it) or for Whisper upload jobs.
|
|
12426
|
+
{ name: "video", type: "file" }
|
|
12369
12427
|
]
|
|
12370
12428
|
};
|
|
12371
12429
|
var imageGenerationsSchema = {
|
|
@@ -14012,6 +14070,13 @@ var bullmqDecorator = async ({
|
|
|
14012
14070
|
};
|
|
14013
14071
|
|
|
14014
14072
|
// src/exulu/context.ts
|
|
14073
|
+
function sanitizeItemRightsMode(rightsMode, defaultMode) {
|
|
14074
|
+
if (rightsMode == null) return rightsMode;
|
|
14075
|
+
if (VALID_RIGHTS_MODES.includes(rightsMode)) {
|
|
14076
|
+
return rightsMode;
|
|
14077
|
+
}
|
|
14078
|
+
return defaultMode ?? "private";
|
|
14079
|
+
}
|
|
14015
14080
|
var ExuluContext2 = class {
|
|
14016
14081
|
// Must begin with a letter (a-z) or underscore (_). Subsequent characters in a name can be letters, digits (0-9), or
|
|
14017
14082
|
// underscores and be a max length of 80 characters and at least 5 characters long.
|
|
@@ -14319,6 +14384,12 @@ var ExuluContext2 = class {
|
|
|
14319
14384
|
if (upsert2 && !item.id && !item.external_id) {
|
|
14320
14385
|
throw new Error("Item id or external id is required for upsert.");
|
|
14321
14386
|
}
|
|
14387
|
+
if (item.rights_mode != null && !VALID_RIGHTS_MODES.includes(item.rights_mode)) {
|
|
14388
|
+
console.warn(
|
|
14389
|
+
`[EXULU] createItem: invalid rights_mode "${item.rights_mode}" for context "${this.id}" (item ${item.id ?? item.external_id ?? "(new)"}) \u2014 falling back to the configured default.`
|
|
14390
|
+
);
|
|
14391
|
+
}
|
|
14392
|
+
item.rights_mode = sanitizeItemRightsMode(item.rights_mode, this.configuration.defaultRightsMode);
|
|
14322
14393
|
const { db: db2 } = await postgresClient();
|
|
14323
14394
|
Object.keys(item).forEach((key) => {
|
|
14324
14395
|
if (this.fields.find((field) => field.name === key)?.type === "json") {
|
|
@@ -16315,7 +16386,7 @@ var shouldGenerateEmbeddings = ({
|
|
|
16315
16386
|
};
|
|
16316
16387
|
|
|
16317
16388
|
// src/graphql/mutations/index.ts
|
|
16318
|
-
var
|
|
16389
|
+
var VALID_RIGHTS_MODES2 = ["private", "users", "roles", "teams", "public"];
|
|
16319
16390
|
var postprocessDeletion = async ({
|
|
16320
16391
|
table,
|
|
16321
16392
|
requestedFields,
|
|
@@ -16640,9 +16711,9 @@ function createMutations(table, contexts, tools, config) {
|
|
|
16640
16711
|
input.id = db2.fn.uuid();
|
|
16641
16712
|
}
|
|
16642
16713
|
}
|
|
16643
|
-
if (table.RBAC && input.rights_mode != null && !
|
|
16714
|
+
if (table.RBAC && input.rights_mode != null && !VALID_RIGHTS_MODES2.includes(input.rights_mode)) {
|
|
16644
16715
|
throw new Error(
|
|
16645
|
-
`Invalid rights_mode "${input.rights_mode}" \u2014 expected one of: ${
|
|
16716
|
+
`Invalid rights_mode "${input.rights_mode}" \u2014 expected one of: ${VALID_RIGHTS_MODES2.join(", ")}`
|
|
16646
16717
|
);
|
|
16647
16718
|
}
|
|
16648
16719
|
const columns = await db2(tableNamePlural).columnInfo();
|
|
@@ -17222,9 +17293,9 @@ function createMutations(table, contexts, tools, config) {
|
|
|
17222
17293
|
if (!Array.isArray(ids) || ids.length === 0) {
|
|
17223
17294
|
throw new Error("ids is required and must be a non-empty array.");
|
|
17224
17295
|
}
|
|
17225
|
-
if (!
|
|
17296
|
+
if (!VALID_RIGHTS_MODES2.includes(rights_mode)) {
|
|
17226
17297
|
throw new Error(
|
|
17227
|
-
`Invalid rights_mode "${rights_mode}" \u2014 expected one of: ${
|
|
17298
|
+
`Invalid rights_mode "${rights_mode}" \u2014 expected one of: ${VALID_RIGHTS_MODES2.join(", ")}`
|
|
17228
17299
|
);
|
|
17229
17300
|
}
|
|
17230
17301
|
for (const id of ids) {
|
|
@@ -20847,6 +20918,9 @@ var buildTranscriptItemInput = ({
|
|
|
20847
20918
|
post_processing: row.post_processing_outputs ?? void 0,
|
|
20848
20919
|
// Handle for the meeting video; null for Whisper uploads.
|
|
20849
20920
|
recall_recording_id: row.recall_recording_id ?? void 0,
|
|
20921
|
+
// Permanent local copy of the video, only present when
|
|
20922
|
+
// RECALL_STORE_VIDEO_LOCALLY was on at recording time.
|
|
20923
|
+
video_s3key: row.video_s3key ?? void 0,
|
|
20850
20924
|
rights_mode: rightsMode,
|
|
20851
20925
|
created_by: row.created_by
|
|
20852
20926
|
});
|
|
@@ -21182,6 +21256,12 @@ var recordingMonthlyLimitSeconds = () => {
|
|
|
21182
21256
|
const minutes = Number(raw);
|
|
21183
21257
|
return Number.isFinite(minutes) && minutes > 0 ? minutes * 60 : null;
|
|
21184
21258
|
};
|
|
21259
|
+
var RECALL_RECORDING_RETENTION_DEFAULT_HOURS = 2160;
|
|
21260
|
+
var recallRecordingRetentionHours = () => {
|
|
21261
|
+
const raw = Number(process.env.RECALL_RECORDING_RETENTION_HOURS);
|
|
21262
|
+
return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : RECALL_RECORDING_RETENTION_DEFAULT_HOURS;
|
|
21263
|
+
};
|
|
21264
|
+
var recallStoreVideoLocally = () => process.env.RECALL_STORE_VIDEO_LOCALLY === "true";
|
|
21185
21265
|
var recallWebhookUrl = () => {
|
|
21186
21266
|
const base = recallPublicBaseUrl();
|
|
21187
21267
|
return base ? `${base}/recall/webhooks` : null;
|
|
@@ -21296,7 +21376,6 @@ var recordingDurationSeconds = (rec) => {
|
|
|
21296
21376
|
}
|
|
21297
21377
|
return null;
|
|
21298
21378
|
};
|
|
21299
|
-
var RECORDING_RETENTION_HOURS = 2160;
|
|
21300
21379
|
var buildCreateBotPayload = (input) => ({
|
|
21301
21380
|
meeting_url: input.meeting_url,
|
|
21302
21381
|
join_at: input.join_at,
|
|
@@ -21306,7 +21385,7 @@ var buildCreateBotPayload = (input) => ({
|
|
|
21306
21385
|
video_mixed_layout: "speaker_view",
|
|
21307
21386
|
participant_events: {},
|
|
21308
21387
|
meeting_metadata: {},
|
|
21309
|
-
retention: { type: "timed", hours:
|
|
21388
|
+
retention: { type: "timed", hours: recallRecordingRetentionHours() }
|
|
21310
21389
|
},
|
|
21311
21390
|
...input.notifyChat ? {
|
|
21312
21391
|
chat: {
|
|
@@ -21368,6 +21447,31 @@ var recallClient = {
|
|
|
21368
21447
|
}
|
|
21369
21448
|
};
|
|
21370
21449
|
|
|
21450
|
+
// src/exulu/recall/video-storage.ts
|
|
21451
|
+
init_cjs_shims();
|
|
21452
|
+
init_uppy();
|
|
21453
|
+
async function downloadAndStoreRecordingVideo(recording, jobId, config) {
|
|
21454
|
+
const video = recording.media_shortcuts?.video_mixed;
|
|
21455
|
+
const url = video?.data?.download_url;
|
|
21456
|
+
if (!url) return null;
|
|
21457
|
+
if (video.status?.code && video.status.code !== "done") return null;
|
|
21458
|
+
const res = await fetch(url);
|
|
21459
|
+
if (!res.ok) {
|
|
21460
|
+
throw new Error(`video download failed for job ${jobId}: HTTP ${res.status}`);
|
|
21461
|
+
}
|
|
21462
|
+
const bytes = Buffer.from(await res.arrayBuffer());
|
|
21463
|
+
return uploadFile(
|
|
21464
|
+
bytes,
|
|
21465
|
+
`recall-videos/${jobId}.mp4`,
|
|
21466
|
+
config,
|
|
21467
|
+
{ contentType: "video/mp4" },
|
|
21468
|
+
void 0,
|
|
21469
|
+
void 0,
|
|
21470
|
+
true
|
|
21471
|
+
// global: this is a system-stored artifact, not scoped to an uploading user.
|
|
21472
|
+
);
|
|
21473
|
+
}
|
|
21474
|
+
|
|
21371
21475
|
// src/exulu/recall/transcript-map.ts
|
|
21372
21476
|
init_cjs_shims();
|
|
21373
21477
|
var speakerLabel = (participant) => {
|
|
@@ -21605,12 +21709,24 @@ var recallService = {
|
|
|
21605
21709
|
const raw = await recallClient.downloadTranscript(url);
|
|
21606
21710
|
const segments = mapRecallTranscript(raw);
|
|
21607
21711
|
let duration = durationFromSegments(segments);
|
|
21712
|
+
let videoS3Key = null;
|
|
21608
21713
|
const recId = recordingId ?? job.recall_recording_id;
|
|
21609
21714
|
if (recId) {
|
|
21610
21715
|
try {
|
|
21611
21716
|
const rec = await recallClient.retrieveRecording(recId);
|
|
21612
21717
|
const recDuration = recordingDurationSeconds(rec);
|
|
21613
21718
|
if (recDuration != null) duration = recDuration;
|
|
21719
|
+
if (recallStoreVideoLocally()) {
|
|
21720
|
+
try {
|
|
21721
|
+
videoS3Key = await downloadAndStoreRecordingVideo(
|
|
21722
|
+
rec,
|
|
21723
|
+
jobId,
|
|
21724
|
+
exuluApp.get().config
|
|
21725
|
+
);
|
|
21726
|
+
} catch (err) {
|
|
21727
|
+
log4(`could not store video locally for job ${jobId}: ${err.message}`);
|
|
21728
|
+
}
|
|
21729
|
+
}
|
|
21614
21730
|
} catch (err) {
|
|
21615
21731
|
log4(`could not fetch recording duration for job ${jobId}: ${err.message}`);
|
|
21616
21732
|
}
|
|
@@ -21619,6 +21735,7 @@ var recallService = {
|
|
|
21619
21735
|
recall_transcript_id: transcriptId ?? job.recall_transcript_id ?? null,
|
|
21620
21736
|
raw_segments: JSON.stringify(segments),
|
|
21621
21737
|
duration_seconds: duration,
|
|
21738
|
+
...videoS3Key ? { video_s3key: videoS3Key } : {},
|
|
21622
21739
|
status: "awaiting_review"
|
|
21623
21740
|
});
|
|
21624
21741
|
} catch (err) {
|
|
@@ -22948,6 +23065,9 @@ type PageInfo {
|
|
|
22948
23065
|
typeDefs += `
|
|
22949
23066
|
meetingRecordingUsage: MeetingRecordingUsage
|
|
22950
23067
|
`;
|
|
23068
|
+
typeDefs += `
|
|
23069
|
+
recordingVideoUrl(job_id: ID!): String
|
|
23070
|
+
`;
|
|
22951
23071
|
typeDefs += `
|
|
22952
23072
|
workflowTriggers(workflow: ID!): [WorkflowTrigger!]!
|
|
22953
23073
|
`;
|
|
@@ -23894,6 +24014,13 @@ type LiteLLMModel {
|
|
|
23894
24014
|
if (!context.user) throw new Error("Authentication required");
|
|
23895
24015
|
return recallService.getUsage();
|
|
23896
24016
|
};
|
|
24017
|
+
resolvers.Query["recordingVideoUrl"] = async (_, args, context) => {
|
|
24018
|
+
await assertOwnsTranscriptionJob(args.job_id, context);
|
|
24019
|
+
const { db: db2 } = context;
|
|
24020
|
+
const row = await db2.from("transcription_jobs").select(["recall_recording_id"]).where({ id: args.job_id }).first();
|
|
24021
|
+
if (!row?.recall_recording_id) return null;
|
|
24022
|
+
return recallService.getRecordingVideoUrl(row.recall_recording_id);
|
|
24023
|
+
};
|
|
23897
24024
|
resolvers.Query["evals"] = async (_, args, context, info) => {
|
|
23898
24025
|
const requestedFields = getRequestedFields(info);
|
|
23899
24026
|
return {
|
|
@@ -25254,7 +25381,7 @@ async function editImage(args) {
|
|
|
25254
25381
|
|
|
25255
25382
|
// src/exulu/litellm/parse-image-models.ts
|
|
25256
25383
|
init_cjs_shims();
|
|
25257
|
-
var
|
|
25384
|
+
var import_node_fs8 = require("fs");
|
|
25258
25385
|
var stripComment3 = (line) => {
|
|
25259
25386
|
const idx = line.indexOf("#");
|
|
25260
25387
|
return idx >= 0 ? line.slice(0, idx) : line;
|
|
@@ -25277,8 +25404,8 @@ var parseInt103 = (raw) => {
|
|
|
25277
25404
|
return Number.isInteger(n) ? n : void 0;
|
|
25278
25405
|
};
|
|
25279
25406
|
var parseImageGenerationModels = (configPath) => {
|
|
25280
|
-
if (!(0,
|
|
25281
|
-
const text = (0,
|
|
25407
|
+
if (!(0, import_node_fs8.existsSync)(configPath)) return [];
|
|
25408
|
+
const text = (0, import_node_fs8.readFileSync)(configPath, "utf8");
|
|
25282
25409
|
const lines = text.split("\n");
|
|
25283
25410
|
const entries = [];
|
|
25284
25411
|
let current;
|
|
@@ -31094,7 +31221,10 @@ var transcriptionsContext = new ExuluContext2({
|
|
|
31094
31221
|
// Link back to the Recall recording so the mixed video stays reachable
|
|
31095
31222
|
// (resolve a fresh URL via ExuluRecall.getRecordingVideoUrl — it expires
|
|
31096
31223
|
// after six hours). Null for Whisper uploads.
|
|
31097
|
-
{ name: "recall_recording_id", type: "text" }
|
|
31224
|
+
{ name: "recall_recording_id", type: "text" },
|
|
31225
|
+
// Permanent local copy of the mixed video, present only when
|
|
31226
|
+
// RECALL_STORE_VIDEO_LOCALLY=true was set at recording time.
|
|
31227
|
+
{ name: "video", type: "file" }
|
|
31098
31228
|
],
|
|
31099
31229
|
sources: [],
|
|
31100
31230
|
active: true,
|
|
@@ -31527,6 +31657,11 @@ var ExuluApp = class {
|
|
|
31527
31657
|
get audit() {
|
|
31528
31658
|
return this._audit ?? getAuditLogger(this._config ?? {});
|
|
31529
31659
|
}
|
|
31660
|
+
/** The deployment's ExuluConfig, for background/webhook code paths that
|
|
31661
|
+
* have no request to read it off (e.g. the Recall webhook handler). */
|
|
31662
|
+
get config() {
|
|
31663
|
+
return this._config ?? {};
|
|
31664
|
+
}
|
|
31530
31665
|
embeddings = {
|
|
31531
31666
|
generate: {
|
|
31532
31667
|
one: async ({ context: contextId, item: itemId }) => {
|
|
@@ -32782,17 +32917,17 @@ init_cjs_shims();
|
|
|
32782
32917
|
|
|
32783
32918
|
// src/exulu/litellm/db-init.ts
|
|
32784
32919
|
init_cjs_shims();
|
|
32785
|
-
var
|
|
32920
|
+
var import_node_fs10 = require("fs");
|
|
32786
32921
|
var import_node_path11 = require("path");
|
|
32787
32922
|
var import_node_child_process6 = require("child_process");
|
|
32788
32923
|
var import_pg = require("pg");
|
|
32789
32924
|
|
|
32790
32925
|
// src/exulu/litellm/db-setup-check.ts
|
|
32791
32926
|
init_cjs_shims();
|
|
32792
|
-
var
|
|
32927
|
+
var import_node_fs9 = require("fs");
|
|
32793
32928
|
var readLiteLLMDatabaseUrl = (configPath) => {
|
|
32794
|
-
if (!(0,
|
|
32795
|
-
const text = (0,
|
|
32929
|
+
if (!(0, import_node_fs9.existsSync)(configPath)) return void 0;
|
|
32930
|
+
const text = (0, import_node_fs9.readFileSync)(configPath, "utf8");
|
|
32796
32931
|
const match = text.match(
|
|
32797
32932
|
/^\s*database_url:\s*["']?([^"'\n#]+?)["']?\s*(#.*)?$/m
|
|
32798
32933
|
);
|
|
@@ -32994,7 +33129,7 @@ var initLiteLLMDatabase = async (packageRoot) => {
|
|
|
32994
33129
|
const venvBin = (0, import_node_path11.resolve)(packageRoot, "ee/python/.venv/bin");
|
|
32995
33130
|
const prismaCli = (0, import_node_path11.resolve)(venvBin, "prisma");
|
|
32996
33131
|
const venvLibDir = (0, import_node_path11.resolve)(packageRoot, "ee/python/.venv/lib");
|
|
32997
|
-
const pythonVersionDir = (0,
|
|
33132
|
+
const pythonVersionDir = (0, import_node_fs10.existsSync)(venvLibDir) ? (0, import_node_fs10.readdirSync)(venvLibDir).find((entry) => /^python3\.\d+$/.test(entry)) : void 0;
|
|
32998
33133
|
if (!pythonVersionDir) {
|
|
32999
33134
|
warn2([
|
|
33000
33135
|
`Could not find a python3.* directory under ${venvLibDir}.`,
|
|
@@ -33009,7 +33144,7 @@ var initLiteLLMDatabase = async (packageRoot) => {
|
|
|
33009
33144
|
"site-packages/litellm/proxy"
|
|
33010
33145
|
);
|
|
33011
33146
|
const schemaPath = (0, import_node_path11.resolve)(litellmProxyDir, "schema.prisma");
|
|
33012
|
-
if (!(0,
|
|
33147
|
+
if (!(0, import_node_fs10.existsSync)(prismaCli)) {
|
|
33013
33148
|
warn2([
|
|
33014
33149
|
`Prisma CLI not found at ${prismaCli}.`,
|
|
33015
33150
|
`Run \`npm run python:setup\` to create the venv and install prisma.`,
|
|
@@ -33017,7 +33152,7 @@ var initLiteLLMDatabase = async (packageRoot) => {
|
|
|
33017
33152
|
]);
|
|
33018
33153
|
return;
|
|
33019
33154
|
}
|
|
33020
|
-
if (!(0,
|
|
33155
|
+
if (!(0, import_node_fs10.existsSync)(schemaPath)) {
|
|
33021
33156
|
warn2([
|
|
33022
33157
|
`LiteLLM Prisma schema not found at ${schemaPath}.`,
|
|
33023
33158
|
`Re-run \`npm run python:setup\`. Skipping LiteLLM database setup.`
|
package/dist/index.d.cts
CHANGED
|
@@ -1167,6 +1167,9 @@ declare class ExuluApp {
|
|
|
1167
1167
|
context(id: string): ExuluContext | undefined;
|
|
1168
1168
|
get contexts(): ExuluContext[];
|
|
1169
1169
|
get audit(): AuditLogger;
|
|
1170
|
+
/** The deployment's ExuluConfig, for background/webhook code paths that
|
|
1171
|
+
* have no request to read it off (e.g. the Recall webhook handler). */
|
|
1172
|
+
get config(): ExuluConfig;
|
|
1170
1173
|
embeddings: {
|
|
1171
1174
|
generate: {
|
|
1172
1175
|
one: ({ context: contextId, item: itemId }: {
|
package/dist/index.d.ts
CHANGED
|
@@ -1167,6 +1167,9 @@ declare class ExuluApp {
|
|
|
1167
1167
|
context(id: string): ExuluContext | undefined;
|
|
1168
1168
|
get contexts(): ExuluContext[];
|
|
1169
1169
|
get audit(): AuditLogger;
|
|
1170
|
+
/** The deployment's ExuluConfig, for background/webhook code paths that
|
|
1171
|
+
* have no request to read it off (e.g. the Recall webhook handler). */
|
|
1172
|
+
get config(): ExuluConfig;
|
|
1170
1173
|
embeddings: {
|
|
1171
1174
|
generate: {
|
|
1172
1175
|
one: ({ context: contextId, item: itemId }: {
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
isPythonEnvironmentSetup,
|
|
6
6
|
setupPythonEnvironment,
|
|
7
7
|
validatePythonEnvironment
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-JIZCKSJO.js";
|
|
9
9
|
import {
|
|
10
10
|
COMPACTION_INSUFFICIENT,
|
|
11
11
|
ContextCompactionRequiredError,
|
|
@@ -90,7 +90,7 @@ import {
|
|
|
90
90
|
verifyCredentialNonce,
|
|
91
91
|
waitForLiteLLMReady,
|
|
92
92
|
withRetry
|
|
93
|
-
} from "./chunk-
|
|
93
|
+
} from "./chunk-D4PCOOL5.js";
|
|
94
94
|
import {
|
|
95
95
|
LiteLLMAdminError,
|
|
96
96
|
findLiteLLMModel,
|
|
@@ -1299,6 +1299,9 @@ async function resolveEmbedder(input) {
|
|
|
1299
1299
|
};
|
|
1300
1300
|
}
|
|
1301
1301
|
|
|
1302
|
+
// types/rbac-rights-modes.ts
|
|
1303
|
+
var VALID_RIGHTS_MODES = ["private", "users", "roles", "teams", "public"];
|
|
1304
|
+
|
|
1302
1305
|
// src/utils/query-preprocessing.ts
|
|
1303
1306
|
import { franc } from "franc";
|
|
1304
1307
|
import natural from "natural";
|
|
@@ -2880,7 +2883,12 @@ var transcriptionJobsSchema = {
|
|
|
2880
2883
|
// Selected per-meeting post-processing: [{ prompt_id, agent_id }].
|
|
2881
2884
|
{ name: "post_processing_prompts", type: "json" },
|
|
2882
2885
|
// Results: [{ prompt_id, agent_id, prompt_name, status, output, error, ran_at }].
|
|
2883
|
-
{ name: "post_processing_outputs", type: "json" }
|
|
2886
|
+
{ name: "post_processing_outputs", type: "json" },
|
|
2887
|
+
// Permanent local copy of the meeting's mixed MP4, downloaded from Recall
|
|
2888
|
+
// when RECALL_STORE_VIDEO_LOCALLY=true. Null when that flag is off (the
|
|
2889
|
+
// video then stays reachable only via ExuluRecall.getRecordingVideoUrl,
|
|
2890
|
+
// for as long as Recall itself retains it) or for Whisper upload jobs.
|
|
2891
|
+
{ name: "video", type: "file" }
|
|
2884
2892
|
]
|
|
2885
2893
|
};
|
|
2886
2894
|
var imageGenerationsSchema = {
|
|
@@ -4500,6 +4508,13 @@ var bullmqDecorator = async ({
|
|
|
4500
4508
|
};
|
|
4501
4509
|
|
|
4502
4510
|
// src/exulu/context.ts
|
|
4511
|
+
function sanitizeItemRightsMode(rightsMode, defaultMode) {
|
|
4512
|
+
if (rightsMode == null) return rightsMode;
|
|
4513
|
+
if (VALID_RIGHTS_MODES.includes(rightsMode)) {
|
|
4514
|
+
return rightsMode;
|
|
4515
|
+
}
|
|
4516
|
+
return defaultMode ?? "private";
|
|
4517
|
+
}
|
|
4503
4518
|
var ExuluContext2 = class {
|
|
4504
4519
|
// Must begin with a letter (a-z) or underscore (_). Subsequent characters in a name can be letters, digits (0-9), or
|
|
4505
4520
|
// underscores and be a max length of 80 characters and at least 5 characters long.
|
|
@@ -4807,6 +4822,12 @@ var ExuluContext2 = class {
|
|
|
4807
4822
|
if (upsert && !item.id && !item.external_id) {
|
|
4808
4823
|
throw new Error("Item id or external id is required for upsert.");
|
|
4809
4824
|
}
|
|
4825
|
+
if (item.rights_mode != null && !VALID_RIGHTS_MODES.includes(item.rights_mode)) {
|
|
4826
|
+
console.warn(
|
|
4827
|
+
`[EXULU] createItem: invalid rights_mode "${item.rights_mode}" for context "${this.id}" (item ${item.id ?? item.external_id ?? "(new)"}) \u2014 falling back to the configured default.`
|
|
4828
|
+
);
|
|
4829
|
+
}
|
|
4830
|
+
item.rights_mode = sanitizeItemRightsMode(item.rights_mode, this.configuration.defaultRightsMode);
|
|
4810
4831
|
const { db } = await postgresClient();
|
|
4811
4832
|
Object.keys(item).forEach((key) => {
|
|
4812
4833
|
if (this.fields.find((field) => field.name === key)?.type === "json") {
|
|
@@ -6751,7 +6772,7 @@ var shouldGenerateEmbeddings = ({
|
|
|
6751
6772
|
};
|
|
6752
6773
|
|
|
6753
6774
|
// src/graphql/mutations/index.ts
|
|
6754
|
-
var
|
|
6775
|
+
var VALID_RIGHTS_MODES2 = ["private", "users", "roles", "teams", "public"];
|
|
6755
6776
|
var postprocessDeletion = async ({
|
|
6756
6777
|
table,
|
|
6757
6778
|
requestedFields,
|
|
@@ -7076,9 +7097,9 @@ function createMutations(table, contexts, tools, config) {
|
|
|
7076
7097
|
input.id = db.fn.uuid();
|
|
7077
7098
|
}
|
|
7078
7099
|
}
|
|
7079
|
-
if (table.RBAC && input.rights_mode != null && !
|
|
7100
|
+
if (table.RBAC && input.rights_mode != null && !VALID_RIGHTS_MODES2.includes(input.rights_mode)) {
|
|
7080
7101
|
throw new Error(
|
|
7081
|
-
`Invalid rights_mode "${input.rights_mode}" \u2014 expected one of: ${
|
|
7102
|
+
`Invalid rights_mode "${input.rights_mode}" \u2014 expected one of: ${VALID_RIGHTS_MODES2.join(", ")}`
|
|
7082
7103
|
);
|
|
7083
7104
|
}
|
|
7084
7105
|
const columns = await db(tableNamePlural).columnInfo();
|
|
@@ -7658,9 +7679,9 @@ function createMutations(table, contexts, tools, config) {
|
|
|
7658
7679
|
if (!Array.isArray(ids) || ids.length === 0) {
|
|
7659
7680
|
throw new Error("ids is required and must be a non-empty array.");
|
|
7660
7681
|
}
|
|
7661
|
-
if (!
|
|
7682
|
+
if (!VALID_RIGHTS_MODES2.includes(rights_mode)) {
|
|
7662
7683
|
throw new Error(
|
|
7663
|
-
`Invalid rights_mode "${rights_mode}" \u2014 expected one of: ${
|
|
7684
|
+
`Invalid rights_mode "${rights_mode}" \u2014 expected one of: ${VALID_RIGHTS_MODES2.join(", ")}`
|
|
7664
7685
|
);
|
|
7665
7686
|
}
|
|
7666
7687
|
for (const id of ids) {
|
|
@@ -11202,6 +11223,9 @@ var buildTranscriptItemInput = ({
|
|
|
11202
11223
|
post_processing: row.post_processing_outputs ?? void 0,
|
|
11203
11224
|
// Handle for the meeting video; null for Whisper uploads.
|
|
11204
11225
|
recall_recording_id: row.recall_recording_id ?? void 0,
|
|
11226
|
+
// Permanent local copy of the video, only present when
|
|
11227
|
+
// RECALL_STORE_VIDEO_LOCALLY was on at recording time.
|
|
11228
|
+
video_s3key: row.video_s3key ?? void 0,
|
|
11205
11229
|
rights_mode: rightsMode,
|
|
11206
11230
|
created_by: row.created_by
|
|
11207
11231
|
});
|
|
@@ -11529,6 +11553,12 @@ var recordingMonthlyLimitSeconds = () => {
|
|
|
11529
11553
|
const minutes = Number(raw);
|
|
11530
11554
|
return Number.isFinite(minutes) && minutes > 0 ? minutes * 60 : null;
|
|
11531
11555
|
};
|
|
11556
|
+
var RECALL_RECORDING_RETENTION_DEFAULT_HOURS = 2160;
|
|
11557
|
+
var recallRecordingRetentionHours = () => {
|
|
11558
|
+
const raw = Number(process.env.RECALL_RECORDING_RETENTION_HOURS);
|
|
11559
|
+
return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : RECALL_RECORDING_RETENTION_DEFAULT_HOURS;
|
|
11560
|
+
};
|
|
11561
|
+
var recallStoreVideoLocally = () => process.env.RECALL_STORE_VIDEO_LOCALLY === "true";
|
|
11532
11562
|
var recallWebhookUrl = () => {
|
|
11533
11563
|
const base = recallPublicBaseUrl();
|
|
11534
11564
|
return base ? `${base}/recall/webhooks` : null;
|
|
@@ -11643,7 +11673,6 @@ var recordingDurationSeconds = (rec) => {
|
|
|
11643
11673
|
}
|
|
11644
11674
|
return null;
|
|
11645
11675
|
};
|
|
11646
|
-
var RECORDING_RETENTION_HOURS = 2160;
|
|
11647
11676
|
var buildCreateBotPayload = (input) => ({
|
|
11648
11677
|
meeting_url: input.meeting_url,
|
|
11649
11678
|
join_at: input.join_at,
|
|
@@ -11653,7 +11682,7 @@ var buildCreateBotPayload = (input) => ({
|
|
|
11653
11682
|
video_mixed_layout: "speaker_view",
|
|
11654
11683
|
participant_events: {},
|
|
11655
11684
|
meeting_metadata: {},
|
|
11656
|
-
retention: { type: "timed", hours:
|
|
11685
|
+
retention: { type: "timed", hours: recallRecordingRetentionHours() }
|
|
11657
11686
|
},
|
|
11658
11687
|
...input.notifyChat ? {
|
|
11659
11688
|
chat: {
|
|
@@ -11715,6 +11744,29 @@ var recallClient = {
|
|
|
11715
11744
|
}
|
|
11716
11745
|
};
|
|
11717
11746
|
|
|
11747
|
+
// src/exulu/recall/video-storage.ts
|
|
11748
|
+
async function downloadAndStoreRecordingVideo(recording, jobId, config) {
|
|
11749
|
+
const video = recording.media_shortcuts?.video_mixed;
|
|
11750
|
+
const url = video?.data?.download_url;
|
|
11751
|
+
if (!url) return null;
|
|
11752
|
+
if (video.status?.code && video.status.code !== "done") return null;
|
|
11753
|
+
const res = await fetch(url);
|
|
11754
|
+
if (!res.ok) {
|
|
11755
|
+
throw new Error(`video download failed for job ${jobId}: HTTP ${res.status}`);
|
|
11756
|
+
}
|
|
11757
|
+
const bytes = Buffer.from(await res.arrayBuffer());
|
|
11758
|
+
return uploadFile(
|
|
11759
|
+
bytes,
|
|
11760
|
+
`recall-videos/${jobId}.mp4`,
|
|
11761
|
+
config,
|
|
11762
|
+
{ contentType: "video/mp4" },
|
|
11763
|
+
void 0,
|
|
11764
|
+
void 0,
|
|
11765
|
+
true
|
|
11766
|
+
// global: this is a system-stored artifact, not scoped to an uploading user.
|
|
11767
|
+
);
|
|
11768
|
+
}
|
|
11769
|
+
|
|
11718
11770
|
// src/exulu/recall/transcript-map.ts
|
|
11719
11771
|
var speakerLabel = (participant) => {
|
|
11720
11772
|
if (!participant) return "unknown";
|
|
@@ -11951,12 +12003,24 @@ var recallService = {
|
|
|
11951
12003
|
const raw = await recallClient.downloadTranscript(url);
|
|
11952
12004
|
const segments = mapRecallTranscript(raw);
|
|
11953
12005
|
let duration = durationFromSegments(segments);
|
|
12006
|
+
let videoS3Key = null;
|
|
11954
12007
|
const recId = recordingId ?? job.recall_recording_id;
|
|
11955
12008
|
if (recId) {
|
|
11956
12009
|
try {
|
|
11957
12010
|
const rec = await recallClient.retrieveRecording(recId);
|
|
11958
12011
|
const recDuration = recordingDurationSeconds(rec);
|
|
11959
12012
|
if (recDuration != null) duration = recDuration;
|
|
12013
|
+
if (recallStoreVideoLocally()) {
|
|
12014
|
+
try {
|
|
12015
|
+
videoS3Key = await downloadAndStoreRecordingVideo(
|
|
12016
|
+
rec,
|
|
12017
|
+
jobId,
|
|
12018
|
+
exuluApp.get().config
|
|
12019
|
+
);
|
|
12020
|
+
} catch (err) {
|
|
12021
|
+
log3(`could not store video locally for job ${jobId}: ${err.message}`);
|
|
12022
|
+
}
|
|
12023
|
+
}
|
|
11960
12024
|
} catch (err) {
|
|
11961
12025
|
log3(`could not fetch recording duration for job ${jobId}: ${err.message}`);
|
|
11962
12026
|
}
|
|
@@ -11965,6 +12029,7 @@ var recallService = {
|
|
|
11965
12029
|
recall_transcript_id: transcriptId ?? job.recall_transcript_id ?? null,
|
|
11966
12030
|
raw_segments: JSON.stringify(segments),
|
|
11967
12031
|
duration_seconds: duration,
|
|
12032
|
+
...videoS3Key ? { video_s3key: videoS3Key } : {},
|
|
11968
12033
|
status: "awaiting_review"
|
|
11969
12034
|
});
|
|
11970
12035
|
} catch (err) {
|
|
@@ -13286,6 +13351,9 @@ type PageInfo {
|
|
|
13286
13351
|
typeDefs += `
|
|
13287
13352
|
meetingRecordingUsage: MeetingRecordingUsage
|
|
13288
13353
|
`;
|
|
13354
|
+
typeDefs += `
|
|
13355
|
+
recordingVideoUrl(job_id: ID!): String
|
|
13356
|
+
`;
|
|
13289
13357
|
typeDefs += `
|
|
13290
13358
|
workflowTriggers(workflow: ID!): [WorkflowTrigger!]!
|
|
13291
13359
|
`;
|
|
@@ -14232,6 +14300,13 @@ type LiteLLMModel {
|
|
|
14232
14300
|
if (!context.user) throw new Error("Authentication required");
|
|
14233
14301
|
return recallService.getUsage();
|
|
14234
14302
|
};
|
|
14303
|
+
resolvers.Query["recordingVideoUrl"] = async (_, args, context) => {
|
|
14304
|
+
await assertOwnsTranscriptionJob(args.job_id, context);
|
|
14305
|
+
const { db } = context;
|
|
14306
|
+
const row = await db.from("transcription_jobs").select(["recall_recording_id"]).where({ id: args.job_id }).first();
|
|
14307
|
+
if (!row?.recall_recording_id) return null;
|
|
14308
|
+
return recallService.getRecordingVideoUrl(row.recall_recording_id);
|
|
14309
|
+
};
|
|
14235
14310
|
resolvers.Query["evals"] = async (_, args, context, info) => {
|
|
14236
14311
|
const requestedFields = getRequestedFields(info);
|
|
14237
14312
|
return {
|
|
@@ -21314,7 +21389,10 @@ var transcriptionsContext = new ExuluContext2({
|
|
|
21314
21389
|
// Link back to the Recall recording so the mixed video stays reachable
|
|
21315
21390
|
// (resolve a fresh URL via ExuluRecall.getRecordingVideoUrl — it expires
|
|
21316
21391
|
// after six hours). Null for Whisper uploads.
|
|
21317
|
-
{ name: "recall_recording_id", type: "text" }
|
|
21392
|
+
{ name: "recall_recording_id", type: "text" },
|
|
21393
|
+
// Permanent local copy of the mixed video, present only when
|
|
21394
|
+
// RECALL_STORE_VIDEO_LOCALLY=true was set at recording time.
|
|
21395
|
+
{ name: "video", type: "file" }
|
|
21318
21396
|
],
|
|
21319
21397
|
sources: [],
|
|
21320
21398
|
active: true,
|
|
@@ -21741,6 +21819,11 @@ var ExuluApp = class {
|
|
|
21741
21819
|
get audit() {
|
|
21742
21820
|
return this._audit ?? getAuditLogger(this._config ?? {});
|
|
21743
21821
|
}
|
|
21822
|
+
/** The deployment's ExuluConfig, for background/webhook code paths that
|
|
21823
|
+
* have no request to read it off (e.g. the Recall webhook handler). */
|
|
21824
|
+
get config() {
|
|
21825
|
+
return this._config ?? {};
|
|
21826
|
+
}
|
|
21744
21827
|
embeddings = {
|
|
21745
21828
|
generate: {
|
|
21746
21829
|
one: async ({ context: contextId, item: itemId }) => {
|
package/package.json
CHANGED
package/scripts/postinstall.cjs
CHANGED
|
@@ -156,15 +156,21 @@ async function main() {
|
|
|
156
156
|
return;
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
-
//
|
|
159
|
+
// We deliberately do NOT skip running setup.sh just because the venv
|
|
160
|
+
// directory and a `python` binary already exist (pythonEnvironmentExists()
|
|
161
|
+
// only checks that much, not that requirements.txt is fully installed).
|
|
162
|
+
// ee/python/setup.sh is itself idempotent — it only skips *recreating* the
|
|
163
|
+
// venv, never skips `pip install -r requirements.txt` — so always invoking
|
|
164
|
+
// it is cheap when nothing is missing (a few seconds) and is the only way
|
|
165
|
+
// to guarantee a venv created before a new requirement was added (e.g.
|
|
166
|
+
// python-docx, 2026-09-11) actually gets it installed. This used to return
|
|
167
|
+
// early here whenever the venv folder was merely present, which silently
|
|
168
|
+
// left newly-added dependencies uninstalled across any install that reused
|
|
169
|
+
// an older venv — see the docx-manipulation skill incident on dx-algi,
|
|
170
|
+
// 2026-09-21.
|
|
160
171
|
if (pythonEnvironmentExists()) {
|
|
161
|
-
console.log(`${colors.
|
|
172
|
+
console.log(`${colors.blue}ℹ${colors.reset} Python environment directory already exists — verifying and installing any missing dependencies...`);
|
|
162
173
|
console.log('');
|
|
163
|
-
console.log('To rebuild, run:');
|
|
164
|
-
console.log(` ${colors.green}import { setupPythonEnvironment } from '@exulu/backend';${colors.reset}`);
|
|
165
|
-
console.log(` ${colors.green}await setupPythonEnvironment({ force: true });${colors.reset}`);
|
|
166
|
-
console.log('');
|
|
167
|
-
return;
|
|
168
174
|
}
|
|
169
175
|
|
|
170
176
|
// Run setup
|