lmnr-cli 0.1.0-alpha.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.
- package/LICENSE +75 -0
- package/README.md +409 -0
- package/dist/index.cjs +1971 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1 -0
- package/package.json +42 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1971 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
//#region rolldown:runtime
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __commonJSMin = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
13
|
+
key = keys[i];
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except) {
|
|
15
|
+
__defProp(to, key, {
|
|
16
|
+
get: ((k) => from[k]).bind(null, key),
|
|
17
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return to;
|
|
23
|
+
};
|
|
24
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
25
|
+
value: mod,
|
|
26
|
+
enumerable: true
|
|
27
|
+
}) : target, mod));
|
|
28
|
+
|
|
29
|
+
//#endregion
|
|
30
|
+
let commander = require("commander");
|
|
31
|
+
let path = require("path");
|
|
32
|
+
path = __toESM(path);
|
|
33
|
+
let pino = require("pino");
|
|
34
|
+
pino = __toESM(pino);
|
|
35
|
+
let pino_pretty = require("pino-pretty");
|
|
36
|
+
let uuid = require("uuid");
|
|
37
|
+
let chokidar = require("chokidar");
|
|
38
|
+
chokidar = __toESM(chokidar);
|
|
39
|
+
let http = require("http");
|
|
40
|
+
http = __toESM(http);
|
|
41
|
+
let events = require("events");
|
|
42
|
+
let eventsource_parser = require("eventsource-parser");
|
|
43
|
+
let child_process = require("child_process");
|
|
44
|
+
let readline = require("readline");
|
|
45
|
+
readline = __toESM(readline);
|
|
46
|
+
|
|
47
|
+
//#region package.json
|
|
48
|
+
var version$1 = "0.1.0-alpha.0";
|
|
49
|
+
|
|
50
|
+
//#endregion
|
|
51
|
+
//#region ../../node_modules/.pnpm/dotenv@17.2.3/node_modules/dotenv/package.json
|
|
52
|
+
var require_package = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
53
|
+
module.exports = {
|
|
54
|
+
"name": "dotenv",
|
|
55
|
+
"version": "17.2.3",
|
|
56
|
+
"description": "Loads environment variables from .env file",
|
|
57
|
+
"main": "lib/main.js",
|
|
58
|
+
"types": "lib/main.d.ts",
|
|
59
|
+
"exports": {
|
|
60
|
+
".": {
|
|
61
|
+
"types": "./lib/main.d.ts",
|
|
62
|
+
"require": "./lib/main.js",
|
|
63
|
+
"default": "./lib/main.js"
|
|
64
|
+
},
|
|
65
|
+
"./config": "./config.js",
|
|
66
|
+
"./config.js": "./config.js",
|
|
67
|
+
"./lib/env-options": "./lib/env-options.js",
|
|
68
|
+
"./lib/env-options.js": "./lib/env-options.js",
|
|
69
|
+
"./lib/cli-options": "./lib/cli-options.js",
|
|
70
|
+
"./lib/cli-options.js": "./lib/cli-options.js",
|
|
71
|
+
"./package.json": "./package.json"
|
|
72
|
+
},
|
|
73
|
+
"scripts": {
|
|
74
|
+
"dts-check": "tsc --project tests/types/tsconfig.json",
|
|
75
|
+
"lint": "standard",
|
|
76
|
+
"pretest": "npm run lint && npm run dts-check",
|
|
77
|
+
"test": "tap run tests/**/*.js --allow-empty-coverage --disable-coverage --timeout=60000",
|
|
78
|
+
"test:coverage": "tap run tests/**/*.js --show-full-coverage --timeout=60000 --coverage-report=text --coverage-report=lcov",
|
|
79
|
+
"prerelease": "npm test",
|
|
80
|
+
"release": "standard-version"
|
|
81
|
+
},
|
|
82
|
+
"repository": {
|
|
83
|
+
"type": "git",
|
|
84
|
+
"url": "git://github.com/motdotla/dotenv.git"
|
|
85
|
+
},
|
|
86
|
+
"homepage": "https://github.com/motdotla/dotenv#readme",
|
|
87
|
+
"funding": "https://dotenvx.com",
|
|
88
|
+
"keywords": [
|
|
89
|
+
"dotenv",
|
|
90
|
+
"env",
|
|
91
|
+
".env",
|
|
92
|
+
"environment",
|
|
93
|
+
"variables",
|
|
94
|
+
"config",
|
|
95
|
+
"settings"
|
|
96
|
+
],
|
|
97
|
+
"readmeFilename": "README.md",
|
|
98
|
+
"license": "BSD-2-Clause",
|
|
99
|
+
"devDependencies": {
|
|
100
|
+
"@types/node": "^18.11.3",
|
|
101
|
+
"decache": "^4.6.2",
|
|
102
|
+
"sinon": "^14.0.1",
|
|
103
|
+
"standard": "^17.0.0",
|
|
104
|
+
"standard-version": "^9.5.0",
|
|
105
|
+
"tap": "^19.2.0",
|
|
106
|
+
"typescript": "^4.8.4"
|
|
107
|
+
},
|
|
108
|
+
"engines": { "node": ">=12" },
|
|
109
|
+
"browser": { "fs": false }
|
|
110
|
+
};
|
|
111
|
+
}));
|
|
112
|
+
|
|
113
|
+
//#endregion
|
|
114
|
+
//#region ../../node_modules/.pnpm/dotenv@17.2.3/node_modules/dotenv/lib/main.js
|
|
115
|
+
var require_main = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
116
|
+
const fs = require("fs");
|
|
117
|
+
const path$1 = require("path");
|
|
118
|
+
const os = require("os");
|
|
119
|
+
const crypto$1 = require("crypto");
|
|
120
|
+
const version = require_package().version;
|
|
121
|
+
const TIPS = [
|
|
122
|
+
"🔐 encrypt with Dotenvx: https://dotenvx.com",
|
|
123
|
+
"🔐 prevent committing .env to code: https://dotenvx.com/precommit",
|
|
124
|
+
"🔐 prevent building .env in docker: https://dotenvx.com/prebuild",
|
|
125
|
+
"📡 add observability to secrets: https://dotenvx.com/ops",
|
|
126
|
+
"👥 sync secrets across teammates & machines: https://dotenvx.com/ops",
|
|
127
|
+
"🗂️ backup and recover secrets: https://dotenvx.com/ops",
|
|
128
|
+
"✅ audit secrets and track compliance: https://dotenvx.com/ops",
|
|
129
|
+
"🔄 add secrets lifecycle management: https://dotenvx.com/ops",
|
|
130
|
+
"🔑 add access controls to secrets: https://dotenvx.com/ops",
|
|
131
|
+
"🛠️ run anywhere with `dotenvx run -- yourcommand`",
|
|
132
|
+
"⚙️ specify custom .env file path with { path: '/custom/path/.env' }",
|
|
133
|
+
"⚙️ enable debug logging with { debug: true }",
|
|
134
|
+
"⚙️ override existing env vars with { override: true }",
|
|
135
|
+
"⚙️ suppress all logs with { quiet: true }",
|
|
136
|
+
"⚙️ write to custom object with { processEnv: myObject }",
|
|
137
|
+
"⚙️ load multiple .env files with { path: ['.env.local', '.env'] }"
|
|
138
|
+
];
|
|
139
|
+
function _getRandomTip() {
|
|
140
|
+
return TIPS[Math.floor(Math.random() * TIPS.length)];
|
|
141
|
+
}
|
|
142
|
+
function parseBoolean(value) {
|
|
143
|
+
if (typeof value === "string") return ![
|
|
144
|
+
"false",
|
|
145
|
+
"0",
|
|
146
|
+
"no",
|
|
147
|
+
"off",
|
|
148
|
+
""
|
|
149
|
+
].includes(value.toLowerCase());
|
|
150
|
+
return Boolean(value);
|
|
151
|
+
}
|
|
152
|
+
function supportsAnsi() {
|
|
153
|
+
return process.stdout.isTTY;
|
|
154
|
+
}
|
|
155
|
+
function dim(text) {
|
|
156
|
+
return supportsAnsi() ? `\x1b[2m${text}\x1b[0m` : text;
|
|
157
|
+
}
|
|
158
|
+
const LINE = /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/gm;
|
|
159
|
+
function parse(src) {
|
|
160
|
+
const obj = {};
|
|
161
|
+
let lines = src.toString();
|
|
162
|
+
lines = lines.replace(/\r\n?/gm, "\n");
|
|
163
|
+
let match;
|
|
164
|
+
while ((match = LINE.exec(lines)) != null) {
|
|
165
|
+
const key = match[1];
|
|
166
|
+
let value = match[2] || "";
|
|
167
|
+
value = value.trim();
|
|
168
|
+
const maybeQuote = value[0];
|
|
169
|
+
value = value.replace(/^(['"`])([\s\S]*)\1$/gm, "$2");
|
|
170
|
+
if (maybeQuote === "\"") {
|
|
171
|
+
value = value.replace(/\\n/g, "\n");
|
|
172
|
+
value = value.replace(/\\r/g, "\r");
|
|
173
|
+
}
|
|
174
|
+
obj[key] = value;
|
|
175
|
+
}
|
|
176
|
+
return obj;
|
|
177
|
+
}
|
|
178
|
+
function _parseVault(options) {
|
|
179
|
+
options = options || {};
|
|
180
|
+
const vaultPath = _vaultPath(options);
|
|
181
|
+
options.path = vaultPath;
|
|
182
|
+
const result = DotenvModule.configDotenv(options);
|
|
183
|
+
if (!result.parsed) {
|
|
184
|
+
const err = /* @__PURE__ */ new Error(`MISSING_DATA: Cannot parse ${vaultPath} for an unknown reason`);
|
|
185
|
+
err.code = "MISSING_DATA";
|
|
186
|
+
throw err;
|
|
187
|
+
}
|
|
188
|
+
const keys = _dotenvKey(options).split(",");
|
|
189
|
+
const length = keys.length;
|
|
190
|
+
let decrypted;
|
|
191
|
+
for (let i = 0; i < length; i++) try {
|
|
192
|
+
const attrs = _instructions(result, keys[i].trim());
|
|
193
|
+
decrypted = DotenvModule.decrypt(attrs.ciphertext, attrs.key);
|
|
194
|
+
break;
|
|
195
|
+
} catch (error) {
|
|
196
|
+
if (i + 1 >= length) throw error;
|
|
197
|
+
}
|
|
198
|
+
return DotenvModule.parse(decrypted);
|
|
199
|
+
}
|
|
200
|
+
function _warn(message) {
|
|
201
|
+
console.error(`[dotenv@${version}][WARN] ${message}`);
|
|
202
|
+
}
|
|
203
|
+
function _debug(message) {
|
|
204
|
+
console.log(`[dotenv@${version}][DEBUG] ${message}`);
|
|
205
|
+
}
|
|
206
|
+
function _log(message) {
|
|
207
|
+
console.log(`[dotenv@${version}] ${message}`);
|
|
208
|
+
}
|
|
209
|
+
function _dotenvKey(options) {
|
|
210
|
+
if (options && options.DOTENV_KEY && options.DOTENV_KEY.length > 0) return options.DOTENV_KEY;
|
|
211
|
+
if (process.env.DOTENV_KEY && process.env.DOTENV_KEY.length > 0) return process.env.DOTENV_KEY;
|
|
212
|
+
return "";
|
|
213
|
+
}
|
|
214
|
+
function _instructions(result, dotenvKey) {
|
|
215
|
+
let uri;
|
|
216
|
+
try {
|
|
217
|
+
uri = new URL(dotenvKey);
|
|
218
|
+
} catch (error) {
|
|
219
|
+
if (error.code === "ERR_INVALID_URL") {
|
|
220
|
+
const err = /* @__PURE__ */ new Error("INVALID_DOTENV_KEY: Wrong format. Must be in valid uri format like dotenv://:key_1234@dotenvx.com/vault/.env.vault?environment=development");
|
|
221
|
+
err.code = "INVALID_DOTENV_KEY";
|
|
222
|
+
throw err;
|
|
223
|
+
}
|
|
224
|
+
throw error;
|
|
225
|
+
}
|
|
226
|
+
const key = uri.password;
|
|
227
|
+
if (!key) {
|
|
228
|
+
const err = /* @__PURE__ */ new Error("INVALID_DOTENV_KEY: Missing key part");
|
|
229
|
+
err.code = "INVALID_DOTENV_KEY";
|
|
230
|
+
throw err;
|
|
231
|
+
}
|
|
232
|
+
const environment = uri.searchParams.get("environment");
|
|
233
|
+
if (!environment) {
|
|
234
|
+
const err = /* @__PURE__ */ new Error("INVALID_DOTENV_KEY: Missing environment part");
|
|
235
|
+
err.code = "INVALID_DOTENV_KEY";
|
|
236
|
+
throw err;
|
|
237
|
+
}
|
|
238
|
+
const environmentKey = `DOTENV_VAULT_${environment.toUpperCase()}`;
|
|
239
|
+
const ciphertext = result.parsed[environmentKey];
|
|
240
|
+
if (!ciphertext) {
|
|
241
|
+
const err = /* @__PURE__ */ new Error(`NOT_FOUND_DOTENV_ENVIRONMENT: Cannot locate environment ${environmentKey} in your .env.vault file.`);
|
|
242
|
+
err.code = "NOT_FOUND_DOTENV_ENVIRONMENT";
|
|
243
|
+
throw err;
|
|
244
|
+
}
|
|
245
|
+
return {
|
|
246
|
+
ciphertext,
|
|
247
|
+
key
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
function _vaultPath(options) {
|
|
251
|
+
let possibleVaultPath = null;
|
|
252
|
+
if (options && options.path && options.path.length > 0) if (Array.isArray(options.path)) {
|
|
253
|
+
for (const filepath of options.path) if (fs.existsSync(filepath)) possibleVaultPath = filepath.endsWith(".vault") ? filepath : `${filepath}.vault`;
|
|
254
|
+
} else possibleVaultPath = options.path.endsWith(".vault") ? options.path : `${options.path}.vault`;
|
|
255
|
+
else possibleVaultPath = path$1.resolve(process.cwd(), ".env.vault");
|
|
256
|
+
if (fs.existsSync(possibleVaultPath)) return possibleVaultPath;
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
function _resolveHome(envPath) {
|
|
260
|
+
return envPath[0] === "~" ? path$1.join(os.homedir(), envPath.slice(1)) : envPath;
|
|
261
|
+
}
|
|
262
|
+
function _configVault(options) {
|
|
263
|
+
const debug = parseBoolean(process.env.DOTENV_CONFIG_DEBUG || options && options.debug);
|
|
264
|
+
const quiet = parseBoolean(process.env.DOTENV_CONFIG_QUIET || options && options.quiet);
|
|
265
|
+
if (debug || !quiet) _log("Loading env from encrypted .env.vault");
|
|
266
|
+
const parsed = DotenvModule._parseVault(options);
|
|
267
|
+
let processEnv = process.env;
|
|
268
|
+
if (options && options.processEnv != null) processEnv = options.processEnv;
|
|
269
|
+
DotenvModule.populate(processEnv, parsed, options);
|
|
270
|
+
return { parsed };
|
|
271
|
+
}
|
|
272
|
+
function configDotenv(options) {
|
|
273
|
+
const dotenvPath = path$1.resolve(process.cwd(), ".env");
|
|
274
|
+
let encoding = "utf8";
|
|
275
|
+
let processEnv = process.env;
|
|
276
|
+
if (options && options.processEnv != null) processEnv = options.processEnv;
|
|
277
|
+
let debug = parseBoolean(processEnv.DOTENV_CONFIG_DEBUG || options && options.debug);
|
|
278
|
+
let quiet = parseBoolean(processEnv.DOTENV_CONFIG_QUIET || options && options.quiet);
|
|
279
|
+
if (options && options.encoding) encoding = options.encoding;
|
|
280
|
+
else if (debug) _debug("No encoding is specified. UTF-8 is used by default");
|
|
281
|
+
let optionPaths = [dotenvPath];
|
|
282
|
+
if (options && options.path) if (!Array.isArray(options.path)) optionPaths = [_resolveHome(options.path)];
|
|
283
|
+
else {
|
|
284
|
+
optionPaths = [];
|
|
285
|
+
for (const filepath of options.path) optionPaths.push(_resolveHome(filepath));
|
|
286
|
+
}
|
|
287
|
+
let lastError;
|
|
288
|
+
const parsedAll = {};
|
|
289
|
+
for (const path$2 of optionPaths) try {
|
|
290
|
+
const parsed = DotenvModule.parse(fs.readFileSync(path$2, { encoding }));
|
|
291
|
+
DotenvModule.populate(parsedAll, parsed, options);
|
|
292
|
+
} catch (e) {
|
|
293
|
+
if (debug) _debug(`Failed to load ${path$2} ${e.message}`);
|
|
294
|
+
lastError = e;
|
|
295
|
+
}
|
|
296
|
+
const populated = DotenvModule.populate(processEnv, parsedAll, options);
|
|
297
|
+
debug = parseBoolean(processEnv.DOTENV_CONFIG_DEBUG || debug);
|
|
298
|
+
quiet = parseBoolean(processEnv.DOTENV_CONFIG_QUIET || quiet);
|
|
299
|
+
if (debug || !quiet) {
|
|
300
|
+
const keysCount = Object.keys(populated).length;
|
|
301
|
+
const shortPaths = [];
|
|
302
|
+
for (const filePath of optionPaths) try {
|
|
303
|
+
const relative = path$1.relative(process.cwd(), filePath);
|
|
304
|
+
shortPaths.push(relative);
|
|
305
|
+
} catch (e) {
|
|
306
|
+
if (debug) _debug(`Failed to load ${filePath} ${e.message}`);
|
|
307
|
+
lastError = e;
|
|
308
|
+
}
|
|
309
|
+
_log(`injecting env (${keysCount}) from ${shortPaths.join(",")} ${dim(`-- tip: ${_getRandomTip()}`)}`);
|
|
310
|
+
}
|
|
311
|
+
if (lastError) return {
|
|
312
|
+
parsed: parsedAll,
|
|
313
|
+
error: lastError
|
|
314
|
+
};
|
|
315
|
+
else return { parsed: parsedAll };
|
|
316
|
+
}
|
|
317
|
+
function config(options) {
|
|
318
|
+
if (_dotenvKey(options).length === 0) return DotenvModule.configDotenv(options);
|
|
319
|
+
const vaultPath = _vaultPath(options);
|
|
320
|
+
if (!vaultPath) {
|
|
321
|
+
_warn(`You set DOTENV_KEY but you are missing a .env.vault file at ${vaultPath}. Did you forget to build it?`);
|
|
322
|
+
return DotenvModule.configDotenv(options);
|
|
323
|
+
}
|
|
324
|
+
return DotenvModule._configVault(options);
|
|
325
|
+
}
|
|
326
|
+
function decrypt(encrypted, keyStr) {
|
|
327
|
+
const key = Buffer.from(keyStr.slice(-64), "hex");
|
|
328
|
+
let ciphertext = Buffer.from(encrypted, "base64");
|
|
329
|
+
const nonce = ciphertext.subarray(0, 12);
|
|
330
|
+
const authTag = ciphertext.subarray(-16);
|
|
331
|
+
ciphertext = ciphertext.subarray(12, -16);
|
|
332
|
+
try {
|
|
333
|
+
const aesgcm = crypto$1.createDecipheriv("aes-256-gcm", key, nonce);
|
|
334
|
+
aesgcm.setAuthTag(authTag);
|
|
335
|
+
return `${aesgcm.update(ciphertext)}${aesgcm.final()}`;
|
|
336
|
+
} catch (error) {
|
|
337
|
+
const isRange = error instanceof RangeError;
|
|
338
|
+
const invalidKeyLength = error.message === "Invalid key length";
|
|
339
|
+
const decryptionFailed = error.message === "Unsupported state or unable to authenticate data";
|
|
340
|
+
if (isRange || invalidKeyLength) {
|
|
341
|
+
const err = /* @__PURE__ */ new Error("INVALID_DOTENV_KEY: It must be 64 characters long (or more)");
|
|
342
|
+
err.code = "INVALID_DOTENV_KEY";
|
|
343
|
+
throw err;
|
|
344
|
+
} else if (decryptionFailed) {
|
|
345
|
+
const err = /* @__PURE__ */ new Error("DECRYPTION_FAILED: Please check your DOTENV_KEY");
|
|
346
|
+
err.code = "DECRYPTION_FAILED";
|
|
347
|
+
throw err;
|
|
348
|
+
} else throw error;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
function populate(processEnv, parsed, options = {}) {
|
|
352
|
+
const debug = Boolean(options && options.debug);
|
|
353
|
+
const override = Boolean(options && options.override);
|
|
354
|
+
const populated = {};
|
|
355
|
+
if (typeof parsed !== "object") {
|
|
356
|
+
const err = /* @__PURE__ */ new Error("OBJECT_REQUIRED: Please check the processEnv argument being passed to populate");
|
|
357
|
+
err.code = "OBJECT_REQUIRED";
|
|
358
|
+
throw err;
|
|
359
|
+
}
|
|
360
|
+
for (const key of Object.keys(parsed)) if (Object.prototype.hasOwnProperty.call(processEnv, key)) {
|
|
361
|
+
if (override === true) {
|
|
362
|
+
processEnv[key] = parsed[key];
|
|
363
|
+
populated[key] = parsed[key];
|
|
364
|
+
}
|
|
365
|
+
if (debug) if (override === true) _debug(`"${key}" is already defined and WAS overwritten`);
|
|
366
|
+
else _debug(`"${key}" is already defined and was NOT overwritten`);
|
|
367
|
+
} else {
|
|
368
|
+
processEnv[key] = parsed[key];
|
|
369
|
+
populated[key] = parsed[key];
|
|
370
|
+
}
|
|
371
|
+
return populated;
|
|
372
|
+
}
|
|
373
|
+
const DotenvModule = {
|
|
374
|
+
configDotenv,
|
|
375
|
+
_configVault,
|
|
376
|
+
_parseVault,
|
|
377
|
+
config,
|
|
378
|
+
decrypt,
|
|
379
|
+
parse,
|
|
380
|
+
populate
|
|
381
|
+
};
|
|
382
|
+
module.exports.configDotenv = DotenvModule.configDotenv;
|
|
383
|
+
module.exports._configVault = DotenvModule._configVault;
|
|
384
|
+
module.exports._parseVault = DotenvModule._parseVault;
|
|
385
|
+
module.exports.config = DotenvModule.config;
|
|
386
|
+
module.exports.decrypt = DotenvModule.decrypt;
|
|
387
|
+
module.exports.parse = DotenvModule.parse;
|
|
388
|
+
module.exports.populate = DotenvModule.populate;
|
|
389
|
+
module.exports = DotenvModule;
|
|
390
|
+
}));
|
|
391
|
+
|
|
392
|
+
//#endregion
|
|
393
|
+
//#region ../client/dist/index.mjs
|
|
394
|
+
var import_main = require_main();
|
|
395
|
+
var version = "0.8.2";
|
|
396
|
+
function getLangVersion() {
|
|
397
|
+
if (typeof process !== "undefined" && process.versions && process.versions.node) return `node-${process.versions.node}`;
|
|
398
|
+
if (typeof navigator !== "undefined" && navigator.userAgent) return `browser-${navigator.userAgent}`;
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
401
|
+
var BaseResource = class {
|
|
402
|
+
constructor(baseHttpUrl, projectApiKey) {
|
|
403
|
+
this.baseHttpUrl = baseHttpUrl;
|
|
404
|
+
this.projectApiKey = projectApiKey;
|
|
405
|
+
}
|
|
406
|
+
headers() {
|
|
407
|
+
return {
|
|
408
|
+
Authorization: `Bearer ${this.projectApiKey}`,
|
|
409
|
+
"Content-Type": "application/json",
|
|
410
|
+
Accept: "application/json"
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
async handleError(response) {
|
|
414
|
+
const errorMsg = await response.text();
|
|
415
|
+
throw new Error(`${response.status} ${errorMsg}`);
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
var BrowserEventsResource = class extends BaseResource {
|
|
419
|
+
constructor(baseHttpUrl, projectApiKey) {
|
|
420
|
+
super(baseHttpUrl, projectApiKey);
|
|
421
|
+
}
|
|
422
|
+
async send({ sessionId, traceId, events: events$1 }) {
|
|
423
|
+
const payload = {
|
|
424
|
+
sessionId,
|
|
425
|
+
traceId,
|
|
426
|
+
events: events$1,
|
|
427
|
+
source: getLangVersion() ?? "javascript",
|
|
428
|
+
sdkVersion: version
|
|
429
|
+
};
|
|
430
|
+
const jsonString = JSON.stringify(payload);
|
|
431
|
+
const compressedStream = new Blob([jsonString], { type: "application/json" }).stream().pipeThrough(new CompressionStream("gzip"));
|
|
432
|
+
const compressedData = await new Response(compressedStream).arrayBuffer();
|
|
433
|
+
const response = await fetch(this.baseHttpUrl + "/v1/browser-sessions/events", {
|
|
434
|
+
method: "POST",
|
|
435
|
+
headers: {
|
|
436
|
+
...this.headers(),
|
|
437
|
+
"Content-Encoding": "gzip"
|
|
438
|
+
},
|
|
439
|
+
body: compressedData
|
|
440
|
+
});
|
|
441
|
+
if (!response.ok) await this.handleError(response);
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
function initializeLogger$1(options) {
|
|
445
|
+
const colorize = options?.colorize ?? true;
|
|
446
|
+
const level = options?.level ?? process.env.LMNR_LOG_LEVEL?.toLowerCase()?.trim() ?? "info";
|
|
447
|
+
return (0, pino.default)({ level }, (0, pino_pretty.PinoPretty)({
|
|
448
|
+
colorize,
|
|
449
|
+
minimumLevel: level
|
|
450
|
+
}));
|
|
451
|
+
}
|
|
452
|
+
const logger$2 = initializeLogger$1();
|
|
453
|
+
const isStringUUID = (id) => /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(id);
|
|
454
|
+
const newUUID$1 = () => {
|
|
455
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return crypto.randomUUID();
|
|
456
|
+
else return (0, uuid.v4)();
|
|
457
|
+
};
|
|
458
|
+
const otelSpanIdToUUID = (spanId) => {
|
|
459
|
+
let id = spanId.toLowerCase();
|
|
460
|
+
if (id.startsWith("0x")) id = id.slice(2);
|
|
461
|
+
if (id.length !== 16) logger$2.warn(`Span ID ${spanId} is not 16 hex chars long. This is not a valid OpenTelemetry span ID.`);
|
|
462
|
+
if (!/^[0-9a-f]+$/.test(id)) {
|
|
463
|
+
logger$2.error(`Span ID ${spanId} is not a valid hex string. Generating a random UUID instead.`);
|
|
464
|
+
return newUUID$1();
|
|
465
|
+
}
|
|
466
|
+
return id.padStart(32, "0").replace(/^([0-9a-f]{8})([0-9a-f]{4})([0-9a-f]{4})([0-9a-f]{4})([0-9a-f]{12})$/, "$1-$2-$3-$4-$5");
|
|
467
|
+
};
|
|
468
|
+
const otelTraceIdToUUID = (traceId) => {
|
|
469
|
+
let id = traceId.toLowerCase();
|
|
470
|
+
if (id.startsWith("0x")) id = id.slice(2);
|
|
471
|
+
if (id.length !== 32) logger$2.warn(`Trace ID ${traceId} is not 32 hex chars long. This is not a valid OpenTelemetry trace ID.`);
|
|
472
|
+
if (!/^[0-9a-f]+$/.test(id)) {
|
|
473
|
+
logger$2.error(`Trace ID ${traceId} is not a valid hex string. Generating a random UUID instead.`);
|
|
474
|
+
return newUUID$1();
|
|
475
|
+
}
|
|
476
|
+
return id.replace(/^([0-9a-f]{8})([0-9a-f]{4})([0-9a-f]{4})([0-9a-f]{4})([0-9a-f]{12})$/, "$1-$2-$3-$4-$5");
|
|
477
|
+
};
|
|
478
|
+
const slicePayload = (value, length) => {
|
|
479
|
+
if (value === null || value === void 0) return value;
|
|
480
|
+
const str = JSON.stringify(value);
|
|
481
|
+
if (str.length <= length) return value;
|
|
482
|
+
return str.slice(0, length) + "...";
|
|
483
|
+
};
|
|
484
|
+
const loadEnv = (options) => {
|
|
485
|
+
const nodeEnv = process.env.NODE_ENV || "development";
|
|
486
|
+
const envDir = process.cwd();
|
|
487
|
+
const envFiles = [
|
|
488
|
+
".env",
|
|
489
|
+
".env.local",
|
|
490
|
+
`.env.${nodeEnv}`,
|
|
491
|
+
`.env.${nodeEnv}.local`
|
|
492
|
+
];
|
|
493
|
+
const logLevel = process.env.LMNR_LOG_LEVEL ?? "info";
|
|
494
|
+
const verbose = ["debug", "trace"].includes(logLevel.trim().toLowerCase());
|
|
495
|
+
const quiet = options?.quiet ?? !verbose;
|
|
496
|
+
(0, import_main.config)({
|
|
497
|
+
path: options?.paths ?? envFiles.map((envFile) => path.resolve(envDir, envFile)),
|
|
498
|
+
quiet
|
|
499
|
+
});
|
|
500
|
+
};
|
|
501
|
+
const logger$1$1 = initializeLogger$1();
|
|
502
|
+
const DEFAULT_DATASET_PULL_LIMIT = 100;
|
|
503
|
+
const DEFAULT_DATASET_PUSH_BATCH_SIZE = 100;
|
|
504
|
+
var DatasetsResource = class extends BaseResource {
|
|
505
|
+
constructor(baseHttpUrl, projectApiKey) {
|
|
506
|
+
super(baseHttpUrl, projectApiKey);
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* List all datasets.
|
|
510
|
+
*
|
|
511
|
+
* @returns {Promise<Dataset[]>} Array of datasets
|
|
512
|
+
*/
|
|
513
|
+
async listDatasets() {
|
|
514
|
+
const response = await fetch(this.baseHttpUrl + "/v1/datasets", {
|
|
515
|
+
method: "GET",
|
|
516
|
+
headers: this.headers()
|
|
517
|
+
});
|
|
518
|
+
if (!response.ok) await this.handleError(response);
|
|
519
|
+
return response.json();
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Get a dataset by name.
|
|
523
|
+
*
|
|
524
|
+
* @param {string} name - Name of the dataset
|
|
525
|
+
* @returns {Promise<Dataset[]>} Array of datasets with matching name
|
|
526
|
+
*/
|
|
527
|
+
async getDatasetByName(name) {
|
|
528
|
+
const params = new URLSearchParams({ name });
|
|
529
|
+
const response = await fetch(this.baseHttpUrl + `/v1/datasets?${params.toString()}`, {
|
|
530
|
+
method: "GET",
|
|
531
|
+
headers: this.headers()
|
|
532
|
+
});
|
|
533
|
+
if (!response.ok) await this.handleError(response);
|
|
534
|
+
return response.json();
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Push datapoints to a dataset.
|
|
538
|
+
*
|
|
539
|
+
* @param {Object} options - Push options
|
|
540
|
+
* @param {Datapoint<D, T>[]} options.points - Datapoints to push
|
|
541
|
+
* @param {string} [options.name] - Name of the dataset (either name or id must be provided)
|
|
542
|
+
* @param {StringUUID} [options.id] - ID of the dataset (either name or id must be provided)
|
|
543
|
+
* @param {number} [options.batchSize] - Batch size for pushing (default: 100)
|
|
544
|
+
* @param {boolean} [options.createDataset] - Whether to create the dataset if it doesn't exist
|
|
545
|
+
* @returns {Promise<PushDatapointsResponse | undefined>}
|
|
546
|
+
*/
|
|
547
|
+
async push({ points, name, id, batchSize = DEFAULT_DATASET_PUSH_BATCH_SIZE, createDataset = false }) {
|
|
548
|
+
if (!name && !id) throw new Error("Either name or id must be provided");
|
|
549
|
+
if (name && id) throw new Error("Only one of name or id must be provided");
|
|
550
|
+
if (createDataset && !name) throw new Error("Name must be provided when creating a new dataset");
|
|
551
|
+
const identifier = name ? { name } : { datasetId: id };
|
|
552
|
+
const totalBatches = Math.ceil(points.length / batchSize);
|
|
553
|
+
let response;
|
|
554
|
+
for (let i = 0; i < points.length; i += batchSize) {
|
|
555
|
+
const batchNum = Math.floor(i / batchSize) + 1;
|
|
556
|
+
logger$1$1.debug(`Pushing batch ${batchNum} of ${totalBatches}`);
|
|
557
|
+
const batch = points.slice(i, i + batchSize);
|
|
558
|
+
const fetchResponse = await fetch(this.baseHttpUrl + "/v1/datasets/datapoints", {
|
|
559
|
+
method: "POST",
|
|
560
|
+
headers: this.headers(),
|
|
561
|
+
body: JSON.stringify({
|
|
562
|
+
...identifier,
|
|
563
|
+
datapoints: batch.map((point) => ({
|
|
564
|
+
data: point.data,
|
|
565
|
+
target: point.target ?? {},
|
|
566
|
+
metadata: point.metadata ?? {}
|
|
567
|
+
})),
|
|
568
|
+
createDataset
|
|
569
|
+
})
|
|
570
|
+
});
|
|
571
|
+
if (fetchResponse.status !== 200 && fetchResponse.status !== 201) await this.handleError(fetchResponse);
|
|
572
|
+
response = await fetchResponse.json();
|
|
573
|
+
}
|
|
574
|
+
return response;
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Pull datapoints from a dataset.
|
|
578
|
+
*
|
|
579
|
+
* @param {Object} options - Pull options
|
|
580
|
+
* @param {string} [options.name] - Name of the dataset (either name or id must be provided)
|
|
581
|
+
* @param {StringUUID} [options.id] - ID of the dataset (either name or id must be provided)
|
|
582
|
+
* @param {number} [options.limit] - Maximum number of datapoints to return (default: 100)
|
|
583
|
+
* @param {number} [options.offset] - Offset for pagination (default: 0)
|
|
584
|
+
* @returns {Promise<GetDatapointsResponse<D, T>>}
|
|
585
|
+
*/
|
|
586
|
+
async pull({ name, id, limit = DEFAULT_DATASET_PULL_LIMIT, offset = 0 }) {
|
|
587
|
+
if (!name && !id) throw new Error("Either name or id must be provided");
|
|
588
|
+
if (name && id) throw new Error("Only one of name or id must be provided");
|
|
589
|
+
const paramsObj = {
|
|
590
|
+
offset: offset.toString(),
|
|
591
|
+
limit: limit.toString()
|
|
592
|
+
};
|
|
593
|
+
if (name) paramsObj.name = name;
|
|
594
|
+
else paramsObj.datasetId = id;
|
|
595
|
+
const params = new URLSearchParams(paramsObj);
|
|
596
|
+
const response = await fetch(this.baseHttpUrl + `/v1/datasets/datapoints?${params.toString()}`, {
|
|
597
|
+
method: "GET",
|
|
598
|
+
headers: this.headers()
|
|
599
|
+
});
|
|
600
|
+
if (!response.ok) await this.handleError(response);
|
|
601
|
+
return response.json();
|
|
602
|
+
}
|
|
603
|
+
};
|
|
604
|
+
const logger$3 = initializeLogger$1();
|
|
605
|
+
const INITIAL_EVALUATION_DATAPOINT_MAX_DATA_LENGTH = 16e6;
|
|
606
|
+
var EvalsResource = class extends BaseResource {
|
|
607
|
+
constructor(baseHttpUrl, projectApiKey) {
|
|
608
|
+
super(baseHttpUrl, projectApiKey);
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* Initialize an evaluation.
|
|
612
|
+
*
|
|
613
|
+
* @param {string} name - Name of the evaluation
|
|
614
|
+
* @param {string} groupName - Group name of the evaluation
|
|
615
|
+
* @param {Record<string, any>} metadata - Optional metadata
|
|
616
|
+
* @returns {Promise<InitEvaluationResponse>} Response from the evaluation initialization
|
|
617
|
+
*/
|
|
618
|
+
async init(name, groupName, metadata) {
|
|
619
|
+
const response = await fetch(this.baseHttpUrl + "/v1/evals", {
|
|
620
|
+
method: "POST",
|
|
621
|
+
headers: this.headers(),
|
|
622
|
+
body: JSON.stringify({
|
|
623
|
+
name: name ?? null,
|
|
624
|
+
groupName: groupName ?? null,
|
|
625
|
+
metadata: metadata ?? null
|
|
626
|
+
})
|
|
627
|
+
});
|
|
628
|
+
if (!response.ok) await this.handleError(response);
|
|
629
|
+
return response.json();
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Create a new evaluation and return its ID.
|
|
633
|
+
*
|
|
634
|
+
* @param {string} [name] - Optional name of the evaluation
|
|
635
|
+
* @param {string} [groupName] - An identifier to group evaluations
|
|
636
|
+
* @param {Record<string, any>} [metadata] - Optional metadata
|
|
637
|
+
* @returns {Promise<StringUUID>} The evaluation ID
|
|
638
|
+
*/
|
|
639
|
+
async create(args) {
|
|
640
|
+
return (await this.init(args?.name, args?.groupName, args?.metadata)).id;
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Create a new evaluation and return its ID.
|
|
644
|
+
* @deprecated use `create` instead.
|
|
645
|
+
*/
|
|
646
|
+
async createEvaluation(name, groupName, metadata) {
|
|
647
|
+
return (await this.init(name, groupName, metadata)).id;
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* Create a datapoint for an evaluation.
|
|
651
|
+
*
|
|
652
|
+
* @param {Object} options - Create datapoint options
|
|
653
|
+
* @param {string} options.evalId - The evaluation ID
|
|
654
|
+
* @param {D} options.data - The input data for the executor
|
|
655
|
+
* @param {T} [options.target] - The target/expected output for evaluators
|
|
656
|
+
* @param {Record<string, any>} [options.metadata] - Optional metadata
|
|
657
|
+
* @param {number} [options.index] - Optional index of the datapoint
|
|
658
|
+
* @param {string} [options.traceId] - Optional trace ID
|
|
659
|
+
* @returns {Promise<StringUUID>} The datapoint ID
|
|
660
|
+
*/
|
|
661
|
+
async createDatapoint({ evalId, data, target, metadata, index, traceId }) {
|
|
662
|
+
const datapointId = newUUID$1();
|
|
663
|
+
const partialDatapoint = {
|
|
664
|
+
id: datapointId,
|
|
665
|
+
data,
|
|
666
|
+
target,
|
|
667
|
+
index: index ?? 0,
|
|
668
|
+
traceId: traceId ?? newUUID$1(),
|
|
669
|
+
executorSpanId: newUUID$1(),
|
|
670
|
+
metadata
|
|
671
|
+
};
|
|
672
|
+
await this.saveDatapoints({
|
|
673
|
+
evalId,
|
|
674
|
+
datapoints: [partialDatapoint]
|
|
675
|
+
});
|
|
676
|
+
return datapointId;
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Update a datapoint with evaluation results.
|
|
680
|
+
*
|
|
681
|
+
* @param {Object} options - Update datapoint options
|
|
682
|
+
* @param {string} options.evalId - The evaluation ID
|
|
683
|
+
* @param {string} options.datapointId - The datapoint ID
|
|
684
|
+
* @param {Record<string, number>} options.scores - The scores
|
|
685
|
+
* @param {O} [options.executorOutput] - The executor output
|
|
686
|
+
* @returns {Promise<void>}
|
|
687
|
+
*/
|
|
688
|
+
async updateDatapoint({ evalId, datapointId, scores, executorOutput }) {
|
|
689
|
+
const response = await fetch(this.baseHttpUrl + `/v1/evals/${evalId}/datapoints/${datapointId}`, {
|
|
690
|
+
method: "POST",
|
|
691
|
+
headers: this.headers(),
|
|
692
|
+
body: JSON.stringify({
|
|
693
|
+
executorOutput,
|
|
694
|
+
scores
|
|
695
|
+
})
|
|
696
|
+
});
|
|
697
|
+
if (!response.ok) await this.handleError(response);
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Save evaluation datapoints.
|
|
701
|
+
*
|
|
702
|
+
* @param {Object} options - Save datapoints options
|
|
703
|
+
* @param {string} options.evalId - ID of the evaluation
|
|
704
|
+
* @param {EvaluationDatapoint<D, T, O>[]} options.datapoints - Datapoint to add
|
|
705
|
+
* @param {string} [options.groupName] - Group name of the evaluation
|
|
706
|
+
* @returns {Promise<void>} Response from the datapoint addition
|
|
707
|
+
*/
|
|
708
|
+
async saveDatapoints({ evalId, datapoints, groupName }) {
|
|
709
|
+
const response = await fetch(this.baseHttpUrl + `/v1/evals/${evalId}/datapoints`, {
|
|
710
|
+
method: "POST",
|
|
711
|
+
headers: this.headers(),
|
|
712
|
+
body: JSON.stringify({
|
|
713
|
+
points: datapoints.map((d) => ({
|
|
714
|
+
...d,
|
|
715
|
+
data: slicePayload(d.data, INITIAL_EVALUATION_DATAPOINT_MAX_DATA_LENGTH),
|
|
716
|
+
target: slicePayload(d.target, INITIAL_EVALUATION_DATAPOINT_MAX_DATA_LENGTH),
|
|
717
|
+
executorOutput: slicePayload(d.executorOutput, INITIAL_EVALUATION_DATAPOINT_MAX_DATA_LENGTH)
|
|
718
|
+
})),
|
|
719
|
+
groupName: groupName ?? null
|
|
720
|
+
})
|
|
721
|
+
});
|
|
722
|
+
if (response.status === 413) return await this.retrySaveDatapoints({
|
|
723
|
+
evalId,
|
|
724
|
+
datapoints,
|
|
725
|
+
groupName
|
|
726
|
+
});
|
|
727
|
+
if (!response.ok) await this.handleError(response);
|
|
728
|
+
}
|
|
729
|
+
/**
|
|
730
|
+
* Get evaluation datapoints.
|
|
731
|
+
*
|
|
732
|
+
* @deprecated Use `client.datasets.pull()` instead.
|
|
733
|
+
* @param {Object} options - Get datapoints options
|
|
734
|
+
* @param {string} options.datasetName - Name of the dataset
|
|
735
|
+
* @param {number} options.offset - Offset at which to start the query
|
|
736
|
+
* @param {number} options.limit - Maximum number of datapoints to return
|
|
737
|
+
* @returns {Promise<GetDatapointsResponse>} Response from the datapoint retrieval
|
|
738
|
+
*/
|
|
739
|
+
async getDatapoints({ datasetName, offset, limit }) {
|
|
740
|
+
logger$3.warn("evals.getDatapoints() is deprecated. Use client.datasets.pull() instead.");
|
|
741
|
+
const params = new URLSearchParams({
|
|
742
|
+
name: datasetName,
|
|
743
|
+
offset: offset.toString(),
|
|
744
|
+
limit: limit.toString()
|
|
745
|
+
});
|
|
746
|
+
const response = await fetch(this.baseHttpUrl + `/v1/datasets/datapoints?${params.toString()}`, {
|
|
747
|
+
method: "GET",
|
|
748
|
+
headers: this.headers()
|
|
749
|
+
});
|
|
750
|
+
if (!response.ok) await this.handleError(response);
|
|
751
|
+
return await response.json();
|
|
752
|
+
}
|
|
753
|
+
async retrySaveDatapoints({ evalId, datapoints, groupName, maxRetries = 25, initialLength = INITIAL_EVALUATION_DATAPOINT_MAX_DATA_LENGTH }) {
|
|
754
|
+
let length = initialLength;
|
|
755
|
+
let lastResponse = null;
|
|
756
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
757
|
+
logger$3.debug(`Retrying save datapoints... ${i + 1} of ${maxRetries}, length: ${length}`);
|
|
758
|
+
const response = await fetch(this.baseHttpUrl + `/v1/evals/${evalId}/datapoints`, {
|
|
759
|
+
method: "POST",
|
|
760
|
+
headers: this.headers(),
|
|
761
|
+
body: JSON.stringify({
|
|
762
|
+
points: datapoints.map((d) => ({
|
|
763
|
+
...d,
|
|
764
|
+
data: slicePayload(d.data, length),
|
|
765
|
+
target: slicePayload(d.target, length),
|
|
766
|
+
executorOutput: slicePayload(d.executorOutput, length)
|
|
767
|
+
})),
|
|
768
|
+
groupName: groupName ?? null
|
|
769
|
+
})
|
|
770
|
+
});
|
|
771
|
+
lastResponse = response;
|
|
772
|
+
length = Math.floor(length / 2);
|
|
773
|
+
if (response.status !== 413) break;
|
|
774
|
+
}
|
|
775
|
+
if (lastResponse && !lastResponse.ok) await this.handleError(lastResponse);
|
|
776
|
+
}
|
|
777
|
+
};
|
|
778
|
+
var EvaluatorScoreSourceType = /* @__PURE__ */ function(EvaluatorScoreSourceType$1) {
|
|
779
|
+
EvaluatorScoreSourceType$1["Evaluator"] = "Evaluator";
|
|
780
|
+
EvaluatorScoreSourceType$1["Code"] = "Code";
|
|
781
|
+
return EvaluatorScoreSourceType$1;
|
|
782
|
+
}(EvaluatorScoreSourceType || {});
|
|
783
|
+
/**
|
|
784
|
+
* Resource for creating evaluator scores
|
|
785
|
+
*/
|
|
786
|
+
var EvaluatorsResource = class extends BaseResource {
|
|
787
|
+
constructor(baseHttpUrl, projectApiKey) {
|
|
788
|
+
super(baseHttpUrl, projectApiKey);
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* Create a score for a span or trace
|
|
792
|
+
*
|
|
793
|
+
* @param {ScoreOptions} options - Score creation options
|
|
794
|
+
* @param {string} options.name - Name of the score
|
|
795
|
+
* @param {string} [options.traceId] - The trace ID to score (will be attached to top-level span)
|
|
796
|
+
* @param {string} [options.spanId] - The span ID to score
|
|
797
|
+
* @param {Record<string, any>} [options.metadata] - Additional metadata
|
|
798
|
+
* @param {number} options.score - The score value (float)
|
|
799
|
+
* @returns {Promise<void>}
|
|
800
|
+
*
|
|
801
|
+
* @example
|
|
802
|
+
* // Score by trace ID (will attach to root span)
|
|
803
|
+
* await evaluators.score({
|
|
804
|
+
* name: "quality",
|
|
805
|
+
* traceId: "trace-id-here",
|
|
806
|
+
* score: 0.95,
|
|
807
|
+
* metadata: { model: "gpt-4" }
|
|
808
|
+
* });
|
|
809
|
+
*
|
|
810
|
+
* @example
|
|
811
|
+
* // Score by span ID
|
|
812
|
+
* await evaluators.score({
|
|
813
|
+
* name: "relevance",
|
|
814
|
+
* spanId: "span-id-here",
|
|
815
|
+
* score: 0.87
|
|
816
|
+
* });
|
|
817
|
+
*/
|
|
818
|
+
async score(options) {
|
|
819
|
+
const { name, metadata, score } = options;
|
|
820
|
+
let payload;
|
|
821
|
+
if ("traceId" in options && options.traceId) {
|
|
822
|
+
const formattedTraceId = isStringUUID(options.traceId) ? options.traceId : otelTraceIdToUUID(options.traceId);
|
|
823
|
+
payload = {
|
|
824
|
+
name,
|
|
825
|
+
metadata,
|
|
826
|
+
score,
|
|
827
|
+
source: EvaluatorScoreSourceType.Code,
|
|
828
|
+
traceId: formattedTraceId
|
|
829
|
+
};
|
|
830
|
+
} else if ("spanId" in options && options.spanId) {
|
|
831
|
+
const formattedSpanId = isStringUUID(options.spanId) ? options.spanId : otelSpanIdToUUID(options.spanId);
|
|
832
|
+
payload = {
|
|
833
|
+
name,
|
|
834
|
+
metadata,
|
|
835
|
+
score,
|
|
836
|
+
source: EvaluatorScoreSourceType.Code,
|
|
837
|
+
spanId: formattedSpanId
|
|
838
|
+
};
|
|
839
|
+
} else throw new Error("Either 'traceId' or 'spanId' must be provided.");
|
|
840
|
+
const response = await fetch(this.baseHttpUrl + "/v1/evaluators/score", {
|
|
841
|
+
method: "POST",
|
|
842
|
+
headers: this.headers(),
|
|
843
|
+
body: JSON.stringify(payload)
|
|
844
|
+
});
|
|
845
|
+
if (!response.ok) await this.handleError(response);
|
|
846
|
+
}
|
|
847
|
+
};
|
|
848
|
+
var RolloutSessionsResource = class extends BaseResource {
|
|
849
|
+
constructor(baseHttpUrl, projectApiKey) {
|
|
850
|
+
super(baseHttpUrl, projectApiKey);
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* Connects to the SSE stream for rollout debugging sessions
|
|
854
|
+
* Returns the Response object for streaming SSE events
|
|
855
|
+
*/
|
|
856
|
+
async connect({ sessionId, name, params, signal }) {
|
|
857
|
+
const response = await fetch(`${this.baseHttpUrl}/v1/rollouts/${sessionId}`, {
|
|
858
|
+
method: "POST",
|
|
859
|
+
headers: {
|
|
860
|
+
...this.headers(),
|
|
861
|
+
"Accept": "text/event-stream"
|
|
862
|
+
},
|
|
863
|
+
body: JSON.stringify({
|
|
864
|
+
name,
|
|
865
|
+
params
|
|
866
|
+
}),
|
|
867
|
+
signal
|
|
868
|
+
});
|
|
869
|
+
if (!response.ok) throw new Error(`SSE connection failed: ${response.status} ${response.statusText}`);
|
|
870
|
+
if (!response.body) throw new Error("No response body");
|
|
871
|
+
return response;
|
|
872
|
+
}
|
|
873
|
+
async delete({ sessionId }) {
|
|
874
|
+
const response = await fetch(`${this.baseHttpUrl}/v1/rollouts/${sessionId}`, {
|
|
875
|
+
method: "DELETE",
|
|
876
|
+
headers: this.headers()
|
|
877
|
+
});
|
|
878
|
+
if (!response.ok) await this.handleError(response);
|
|
879
|
+
}
|
|
880
|
+
async setStatus({ sessionId, status }) {
|
|
881
|
+
const response = await fetch(`${this.baseHttpUrl}/v1/rollouts/${sessionId}/status`, {
|
|
882
|
+
method: "PATCH",
|
|
883
|
+
headers: this.headers(),
|
|
884
|
+
body: JSON.stringify({ status })
|
|
885
|
+
});
|
|
886
|
+
if (!response.ok) await this.handleError(response);
|
|
887
|
+
}
|
|
888
|
+
async sendSpanUpdate({ sessionId, span }) {
|
|
889
|
+
const response = await fetch(`${this.baseHttpUrl}/v1/rollouts/${sessionId}/update`, {
|
|
890
|
+
method: "PATCH",
|
|
891
|
+
headers: this.headers(),
|
|
892
|
+
body: JSON.stringify({
|
|
893
|
+
type: "spanStart",
|
|
894
|
+
spanId: otelSpanIdToUUID(span.spanId),
|
|
895
|
+
traceId: otelTraceIdToUUID(span.traceId),
|
|
896
|
+
parentSpanId: span.parentSpanId ? otelSpanIdToUUID(span.parentSpanId) : void 0,
|
|
897
|
+
attributes: span.attributes,
|
|
898
|
+
startTime: span.startTime,
|
|
899
|
+
name: span.name,
|
|
900
|
+
spanType: span.spanType
|
|
901
|
+
})
|
|
902
|
+
});
|
|
903
|
+
if (!response.ok) await this.handleError(response);
|
|
904
|
+
}
|
|
905
|
+
};
|
|
906
|
+
var SqlResource = class extends BaseResource {
|
|
907
|
+
constructor(baseHttpUrl, projectApiKey) {
|
|
908
|
+
super(baseHttpUrl, projectApiKey);
|
|
909
|
+
}
|
|
910
|
+
async query(sql, parameters = {}) {
|
|
911
|
+
const response = await fetch(`${this.baseHttpUrl}/v1/sql/query`, {
|
|
912
|
+
method: "POST",
|
|
913
|
+
headers: { ...this.headers() },
|
|
914
|
+
body: JSON.stringify({
|
|
915
|
+
query: sql,
|
|
916
|
+
parameters
|
|
917
|
+
})
|
|
918
|
+
});
|
|
919
|
+
if (!response.ok) await this.handleError(response);
|
|
920
|
+
return (await response.json()).data;
|
|
921
|
+
}
|
|
922
|
+
};
|
|
923
|
+
/** Resource for tagging traces. */
|
|
924
|
+
var TagsResource = class extends BaseResource {
|
|
925
|
+
/** Resource for tagging traces. */
|
|
926
|
+
constructor(baseHttpUrl, projectApiKey) {
|
|
927
|
+
super(baseHttpUrl, projectApiKey);
|
|
928
|
+
}
|
|
929
|
+
/**
|
|
930
|
+
* Tag a trace with a list of tags. Note that the trace must be ended before
|
|
931
|
+
* tagging it. You may want to call `await Laminar.flush()` after the trace
|
|
932
|
+
* that you want to tag.
|
|
933
|
+
*
|
|
934
|
+
* @param {string | StringUUID} trace_id - The trace id to tag.
|
|
935
|
+
* @param {string[] | string} tags - The tag or list of tags to add to the trace.
|
|
936
|
+
* @returns {Promise<any>} The response from the server.
|
|
937
|
+
* @example
|
|
938
|
+
* ```javascript
|
|
939
|
+
* import { Laminar, observe, LaminarClient } from "@lmnr-ai/lmnr";
|
|
940
|
+
* Laminar.initialize();
|
|
941
|
+
* const client = new LaminarClient();
|
|
942
|
+
* let traceId: StringUUID | null = null;
|
|
943
|
+
* // Make sure this is called outside of traced context.
|
|
944
|
+
* await observe(
|
|
945
|
+
* {
|
|
946
|
+
* name: "my-trace",
|
|
947
|
+
* },
|
|
948
|
+
* async () => {
|
|
949
|
+
* traceId = await Laminar.getTraceId();
|
|
950
|
+
* await foo();
|
|
951
|
+
* },
|
|
952
|
+
* );
|
|
953
|
+
*
|
|
954
|
+
* // or make sure the trace is ended by this point.
|
|
955
|
+
* await Laminar.flush();
|
|
956
|
+
* if (traceId) {
|
|
957
|
+
* await client.tags.tag(traceId, ["tag1", "tag2"]);
|
|
958
|
+
* }
|
|
959
|
+
* ```
|
|
960
|
+
*/
|
|
961
|
+
async tag(trace_id, tags) {
|
|
962
|
+
const traceTags = Array.isArray(tags) ? tags : [tags];
|
|
963
|
+
const formattedTraceId = isStringUUID(trace_id) ? trace_id : otelTraceIdToUUID(trace_id);
|
|
964
|
+
const url = this.baseHttpUrl + "/v1/tag";
|
|
965
|
+
const payload = {
|
|
966
|
+
"traceId": formattedTraceId,
|
|
967
|
+
"names": traceTags
|
|
968
|
+
};
|
|
969
|
+
const response = await fetch(url, {
|
|
970
|
+
method: "POST",
|
|
971
|
+
headers: this.headers(),
|
|
972
|
+
body: JSON.stringify(payload)
|
|
973
|
+
});
|
|
974
|
+
if (!response.ok) await this.handleError(response);
|
|
975
|
+
return response.json();
|
|
976
|
+
}
|
|
977
|
+
};
|
|
978
|
+
var LaminarClient = class {
|
|
979
|
+
constructor({ baseUrl, projectApiKey, port } = {}) {
|
|
980
|
+
loadEnv();
|
|
981
|
+
this.projectApiKey = projectApiKey ?? process.env.LMNR_PROJECT_API_KEY;
|
|
982
|
+
const httpPort = port ?? (baseUrl?.match(/:\d{1,5}$/g) ? parseInt(baseUrl.match(/:\d{1,5}$/g)[0].slice(1)) : 443);
|
|
983
|
+
this.baseUrl = `${(baseUrl ?? process.env.LMNR_BASE_URL)?.replace(/\/$/, "").replace(/:\d{1,5}$/g, "") ?? "https://api.lmnr.ai"}:${httpPort}`;
|
|
984
|
+
this._browserEvents = new BrowserEventsResource(this.baseUrl, this.projectApiKey);
|
|
985
|
+
this._datasets = new DatasetsResource(this.baseUrl, this.projectApiKey);
|
|
986
|
+
this._evals = new EvalsResource(this.baseUrl, this.projectApiKey);
|
|
987
|
+
this._evaluators = new EvaluatorsResource(this.baseUrl, this.projectApiKey);
|
|
988
|
+
this._rolloutSessions = new RolloutSessionsResource(this.baseUrl, this.projectApiKey);
|
|
989
|
+
this._sql = new SqlResource(this.baseUrl, this.projectApiKey);
|
|
990
|
+
this._tags = new TagsResource(this.baseUrl, this.projectApiKey);
|
|
991
|
+
}
|
|
992
|
+
get browserEvents() {
|
|
993
|
+
return this._browserEvents;
|
|
994
|
+
}
|
|
995
|
+
get datasets() {
|
|
996
|
+
return this._datasets;
|
|
997
|
+
}
|
|
998
|
+
get evals() {
|
|
999
|
+
return this._evals;
|
|
1000
|
+
}
|
|
1001
|
+
get evaluators() {
|
|
1002
|
+
return this._evaluators;
|
|
1003
|
+
}
|
|
1004
|
+
get rolloutSessions() {
|
|
1005
|
+
return this._rolloutSessions;
|
|
1006
|
+
}
|
|
1007
|
+
get sql() {
|
|
1008
|
+
return this._sql;
|
|
1009
|
+
}
|
|
1010
|
+
get tags() {
|
|
1011
|
+
return this._tags;
|
|
1012
|
+
}
|
|
1013
|
+
};
|
|
1014
|
+
|
|
1015
|
+
//#endregion
|
|
1016
|
+
//#region src/cache-server.ts
|
|
1017
|
+
const DEFAULT_START_PORT = 35667;
|
|
1018
|
+
/**
|
|
1019
|
+
* Finds an available port starting from the given port number
|
|
1020
|
+
*/
|
|
1021
|
+
async function findAvailablePort(startPort) {
|
|
1022
|
+
return new Promise((resolve, reject) => {
|
|
1023
|
+
const server = http.createServer();
|
|
1024
|
+
server.listen(startPort, () => {
|
|
1025
|
+
const port = server.address().port;
|
|
1026
|
+
server.close(() => resolve(port));
|
|
1027
|
+
});
|
|
1028
|
+
server.on("error", (err) => {
|
|
1029
|
+
if (err.code === "EADDRINUSE") resolve(findAvailablePort(startPort + 1));
|
|
1030
|
+
else reject(err);
|
|
1031
|
+
});
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
/**
|
|
1035
|
+
* Parses request body as JSON
|
|
1036
|
+
*/
|
|
1037
|
+
function parseBody(req) {
|
|
1038
|
+
return new Promise((resolve, reject) => {
|
|
1039
|
+
let body = "";
|
|
1040
|
+
req.on("data", (chunk) => {
|
|
1041
|
+
body += chunk.toString();
|
|
1042
|
+
});
|
|
1043
|
+
req.on("end", () => {
|
|
1044
|
+
try {
|
|
1045
|
+
resolve(body ? JSON.parse(body) : {});
|
|
1046
|
+
} catch (err) {
|
|
1047
|
+
reject(/* @__PURE__ */ new Error(`Invalid JSON: ${err instanceof Error ? err.message : String(err)}`));
|
|
1048
|
+
}
|
|
1049
|
+
});
|
|
1050
|
+
req.on("error", reject);
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
1053
|
+
/**
|
|
1054
|
+
* Starts a local cache server for storing and retrieving cached LLM responses
|
|
1055
|
+
* during rollout debugging sessions.
|
|
1056
|
+
*
|
|
1057
|
+
* @param startPort - Optional starting port number (defaults to 35667)
|
|
1058
|
+
* @returns Server information including port, server instance, cache, and metadata setter
|
|
1059
|
+
*/
|
|
1060
|
+
async function startCacheServer(startPort = DEFAULT_START_PORT) {
|
|
1061
|
+
const cache = /* @__PURE__ */ new Map();
|
|
1062
|
+
let metadata = {
|
|
1063
|
+
pathToCount: {},
|
|
1064
|
+
overrides: void 0
|
|
1065
|
+
};
|
|
1066
|
+
const server = http.createServer((req, res) => {
|
|
1067
|
+
(async () => {
|
|
1068
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
1069
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
1070
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
1071
|
+
if (req.method === "OPTIONS") {
|
|
1072
|
+
res.writeHead(200);
|
|
1073
|
+
res.end();
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
1076
|
+
if (req.method === "GET" && req.url === "/health") {
|
|
1077
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1078
|
+
res.end(JSON.stringify({ status: "ok" }));
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
if (req.method === "POST" && req.url === "/cached") {
|
|
1082
|
+
try {
|
|
1083
|
+
const { path: path$2, index } = await parseBody(req);
|
|
1084
|
+
if (typeof path$2 !== "string" || typeof index !== "number") {
|
|
1085
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1086
|
+
res.end(JSON.stringify({ error: "Invalid request: path (string) and index (number) required" }));
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
const cacheKey = `${index}:${path$2}`;
|
|
1090
|
+
const response = {
|
|
1091
|
+
span: cache.get(cacheKey),
|
|
1092
|
+
pathToCount: metadata.pathToCount,
|
|
1093
|
+
overrides: metadata.overrides
|
|
1094
|
+
};
|
|
1095
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1096
|
+
res.end(JSON.stringify(response));
|
|
1097
|
+
} catch (err) {
|
|
1098
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1099
|
+
res.end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }));
|
|
1100
|
+
}
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1104
|
+
res.end(JSON.stringify({ error: "Not found" }));
|
|
1105
|
+
})().catch((error) => {
|
|
1106
|
+
if (!res.headersSent) {
|
|
1107
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1108
|
+
res.end(JSON.stringify({ error: error instanceof Error ? error.message : "Internal server error" }));
|
|
1109
|
+
}
|
|
1110
|
+
});
|
|
1111
|
+
});
|
|
1112
|
+
const port = await findAvailablePort(startPort);
|
|
1113
|
+
return new Promise((resolve, reject) => {
|
|
1114
|
+
server.listen(port, () => {
|
|
1115
|
+
resolve({
|
|
1116
|
+
port,
|
|
1117
|
+
server,
|
|
1118
|
+
cache,
|
|
1119
|
+
setMetadata: (newMetadata) => {
|
|
1120
|
+
metadata = newMetadata;
|
|
1121
|
+
}
|
|
1122
|
+
});
|
|
1123
|
+
});
|
|
1124
|
+
server.on("error", reject);
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
//#endregion
|
|
1129
|
+
//#region src/sse-client.ts
|
|
1130
|
+
const HEARTBEAT_INTERVAL = 5e3;
|
|
1131
|
+
const MAX_MISSED_HEARTBEATS = 3;
|
|
1132
|
+
/**
|
|
1133
|
+
* SSE client for rollout debugging sessions
|
|
1134
|
+
* Connects to the Laminar backend and listens for run events
|
|
1135
|
+
*/
|
|
1136
|
+
var SSEClient = class extends events.EventEmitter {
|
|
1137
|
+
constructor(options) {
|
|
1138
|
+
super();
|
|
1139
|
+
this.lastHeartbeat = Date.now();
|
|
1140
|
+
this.isShutdown = false;
|
|
1141
|
+
this.client = options.client;
|
|
1142
|
+
this.sessionId = options.sessionId;
|
|
1143
|
+
this.params = options.params;
|
|
1144
|
+
this.name = options.name;
|
|
1145
|
+
}
|
|
1146
|
+
/**
|
|
1147
|
+
* Connects to the SSE endpoint
|
|
1148
|
+
*/
|
|
1149
|
+
async connectAndListen() {
|
|
1150
|
+
if (this.isShutdown) return;
|
|
1151
|
+
this.abortController = new AbortController();
|
|
1152
|
+
this.lastHeartbeat = Date.now();
|
|
1153
|
+
try {
|
|
1154
|
+
const response = await this.client.rolloutSessions.connect({
|
|
1155
|
+
sessionId: this.sessionId,
|
|
1156
|
+
params: this.params,
|
|
1157
|
+
signal: this.abortController.signal,
|
|
1158
|
+
name: this.name
|
|
1159
|
+
});
|
|
1160
|
+
this.emit("connected");
|
|
1161
|
+
this.startHeartbeatCheck();
|
|
1162
|
+
await this.parseSSEStream(response.body);
|
|
1163
|
+
} catch (error) {
|
|
1164
|
+
if (error.name === "AbortError") return;
|
|
1165
|
+
this.emit("error", error);
|
|
1166
|
+
if (!this.isShutdown) this.scheduleReconnect();
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
/**
|
|
1170
|
+
* Parses SSE stream and emits events
|
|
1171
|
+
*/
|
|
1172
|
+
async parseSSEStream(body) {
|
|
1173
|
+
const reader = body.getReader();
|
|
1174
|
+
const decoder = new TextDecoder();
|
|
1175
|
+
const parser = (0, eventsource_parser.createParser)({ onEvent: (event) => {
|
|
1176
|
+
this.processSSEEvent(event);
|
|
1177
|
+
} });
|
|
1178
|
+
try {
|
|
1179
|
+
while (true) {
|
|
1180
|
+
const { done, value } = await reader.read();
|
|
1181
|
+
if (done) break;
|
|
1182
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
1183
|
+
parser.feed(chunk);
|
|
1184
|
+
}
|
|
1185
|
+
} finally {
|
|
1186
|
+
reader.releaseLock();
|
|
1187
|
+
}
|
|
1188
|
+
if (!this.isShutdown) this.scheduleReconnect();
|
|
1189
|
+
}
|
|
1190
|
+
/**
|
|
1191
|
+
* Processes a parsed SSE event
|
|
1192
|
+
*/
|
|
1193
|
+
processSSEEvent(event) {
|
|
1194
|
+
if (!event.data) return;
|
|
1195
|
+
try {
|
|
1196
|
+
if (event.event === "heartbeat") {
|
|
1197
|
+
this.lastHeartbeat = Date.now();
|
|
1198
|
+
this.emit("heartbeat");
|
|
1199
|
+
} else if (event.event === "run") {
|
|
1200
|
+
const runEvent = {
|
|
1201
|
+
event_type: "run",
|
|
1202
|
+
data: JSON.parse(event.data)
|
|
1203
|
+
};
|
|
1204
|
+
this.emit("run", runEvent);
|
|
1205
|
+
} else if (event.event === "handshake") {
|
|
1206
|
+
const handshakeEvent = {
|
|
1207
|
+
event_type: "handshake",
|
|
1208
|
+
data: JSON.parse(event.data)
|
|
1209
|
+
};
|
|
1210
|
+
this.emit("handshake", handshakeEvent);
|
|
1211
|
+
} else if (event.event === "stop") this.emit("stop");
|
|
1212
|
+
} catch (error) {
|
|
1213
|
+
this.emit("error", /* @__PURE__ */ new Error(`Failed to parse SSE event data: ${error}`));
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
/**
|
|
1217
|
+
* Starts checking for missed heartbeats
|
|
1218
|
+
*/
|
|
1219
|
+
startHeartbeatCheck() {
|
|
1220
|
+
this.stopHeartbeatCheck();
|
|
1221
|
+
this.heartbeatCheckTimer = setInterval(() => {
|
|
1222
|
+
if (Date.now() - this.lastHeartbeat > HEARTBEAT_INTERVAL * MAX_MISSED_HEARTBEATS) {
|
|
1223
|
+
this.emit("heartbeat_timeout");
|
|
1224
|
+
this.reconnect();
|
|
1225
|
+
}
|
|
1226
|
+
}, HEARTBEAT_INTERVAL);
|
|
1227
|
+
}
|
|
1228
|
+
/**
|
|
1229
|
+
* Stops heartbeat checking
|
|
1230
|
+
*/
|
|
1231
|
+
stopHeartbeatCheck() {
|
|
1232
|
+
if (this.heartbeatCheckTimer) {
|
|
1233
|
+
clearInterval(this.heartbeatCheckTimer);
|
|
1234
|
+
this.heartbeatCheckTimer = void 0;
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
/**
|
|
1238
|
+
* Schedules a reconnection attempt
|
|
1239
|
+
*/
|
|
1240
|
+
scheduleReconnect() {
|
|
1241
|
+
if (this.reconnectTimer || this.isShutdown) return;
|
|
1242
|
+
this.emit("reconnecting");
|
|
1243
|
+
this.reconnectTimer = setTimeout(() => {
|
|
1244
|
+
this.reconnectTimer = void 0;
|
|
1245
|
+
this.reconnect();
|
|
1246
|
+
}, 1e3);
|
|
1247
|
+
}
|
|
1248
|
+
/**
|
|
1249
|
+
* Reconnects to the SSE endpoint
|
|
1250
|
+
*/
|
|
1251
|
+
reconnect() {
|
|
1252
|
+
this.disconnect(true);
|
|
1253
|
+
this.connectAndListen().catch((error) => {
|
|
1254
|
+
this.emit("error", error);
|
|
1255
|
+
});
|
|
1256
|
+
}
|
|
1257
|
+
/**
|
|
1258
|
+
* Disconnects from the SSE endpoint
|
|
1259
|
+
*/
|
|
1260
|
+
disconnect(stopReconnect = true) {
|
|
1261
|
+
if (this.abortController) {
|
|
1262
|
+
this.abortController.abort();
|
|
1263
|
+
this.abortController = void 0;
|
|
1264
|
+
}
|
|
1265
|
+
this.stopHeartbeatCheck();
|
|
1266
|
+
if (stopReconnect && this.reconnectTimer) {
|
|
1267
|
+
clearTimeout(this.reconnectTimer);
|
|
1268
|
+
this.reconnectTimer = void 0;
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
/**
|
|
1272
|
+
* Updates the function metadata (params, name) and reconnects
|
|
1273
|
+
*/
|
|
1274
|
+
updateMetadata(params, name) {
|
|
1275
|
+
this.params = params;
|
|
1276
|
+
this.name = name;
|
|
1277
|
+
this.reconnect();
|
|
1278
|
+
}
|
|
1279
|
+
/**
|
|
1280
|
+
* Shuts down the SSE client gracefully
|
|
1281
|
+
*/
|
|
1282
|
+
shutdown() {
|
|
1283
|
+
this.isShutdown = true;
|
|
1284
|
+
this.disconnect(true);
|
|
1285
|
+
this.emit("shutdown");
|
|
1286
|
+
this.removeAllListeners();
|
|
1287
|
+
}
|
|
1288
|
+
};
|
|
1289
|
+
/**
|
|
1290
|
+
* Creates an SSE client (does not auto-connect)
|
|
1291
|
+
* Call client.connect() after registering event listeners
|
|
1292
|
+
*/
|
|
1293
|
+
function createSSEClient(options) {
|
|
1294
|
+
return new SSEClient(options);
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
//#endregion
|
|
1298
|
+
//#region ../types/dist/index.mjs
|
|
1299
|
+
/**
|
|
1300
|
+
* Message prefix for protocol messages in stdout
|
|
1301
|
+
*/
|
|
1302
|
+
const WORKER_MESSAGE_PREFIX = "__LMNR_WORKER__:";
|
|
1303
|
+
|
|
1304
|
+
//#endregion
|
|
1305
|
+
//#region src/utils.ts
|
|
1306
|
+
function initializeLogger(options) {
|
|
1307
|
+
const colorize = options?.colorize ?? true;
|
|
1308
|
+
const level = options?.level ?? process.env.LMNR_LOG_LEVEL?.toLowerCase()?.trim() ?? "info";
|
|
1309
|
+
return (0, pino.default)({ level }, (0, pino_pretty.PinoPretty)({
|
|
1310
|
+
colorize,
|
|
1311
|
+
minimumLevel: level
|
|
1312
|
+
}));
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
//#endregion
|
|
1316
|
+
//#region src/subprocess/executor.ts
|
|
1317
|
+
const logger$1 = initializeLogger();
|
|
1318
|
+
/**
|
|
1319
|
+
* Track and kill the currently running subprocess
|
|
1320
|
+
*/
|
|
1321
|
+
var SubprocessManager = class {
|
|
1322
|
+
constructor() {
|
|
1323
|
+
this.currentProcess = null;
|
|
1324
|
+
}
|
|
1325
|
+
/**
|
|
1326
|
+
* Execute a subprocess and track it
|
|
1327
|
+
*/
|
|
1328
|
+
async execute(options) {
|
|
1329
|
+
const { command, args, config: config$1 } = options;
|
|
1330
|
+
const child = (0, child_process.spawn)(command, args, { stdio: [
|
|
1331
|
+
"pipe",
|
|
1332
|
+
"pipe",
|
|
1333
|
+
"pipe"
|
|
1334
|
+
] });
|
|
1335
|
+
this.currentProcess = child;
|
|
1336
|
+
return new Promise((resolve, reject) => {
|
|
1337
|
+
let result = void 0;
|
|
1338
|
+
let hasError = false;
|
|
1339
|
+
readline.createInterface({
|
|
1340
|
+
input: child.stdout,
|
|
1341
|
+
crlfDelay: Infinity
|
|
1342
|
+
}).on("line", (line) => {
|
|
1343
|
+
if (line.startsWith(WORKER_MESSAGE_PREFIX)) try {
|
|
1344
|
+
const messageJson = line.substring(WORKER_MESSAGE_PREFIX.length);
|
|
1345
|
+
const message = JSON.parse(messageJson);
|
|
1346
|
+
switch (message.type) {
|
|
1347
|
+
case "log":
|
|
1348
|
+
logger$1[message.level](message.message);
|
|
1349
|
+
break;
|
|
1350
|
+
case "result":
|
|
1351
|
+
result = message.data;
|
|
1352
|
+
break;
|
|
1353
|
+
case "error":
|
|
1354
|
+
hasError = true;
|
|
1355
|
+
logger$1.error(`Worker error: ${message.error}`);
|
|
1356
|
+
if (message.stack) logger$1.error(message.stack);
|
|
1357
|
+
break;
|
|
1358
|
+
}
|
|
1359
|
+
} catch {
|
|
1360
|
+
logger$1.error(`Failed to parse worker protocol message: ${line}`);
|
|
1361
|
+
}
|
|
1362
|
+
else console.log(line);
|
|
1363
|
+
});
|
|
1364
|
+
child.stderr.on("data", (data) => {
|
|
1365
|
+
process.stderr.write(data);
|
|
1366
|
+
});
|
|
1367
|
+
child.on("exit", (code, signal) => {
|
|
1368
|
+
if (this.currentProcess?.pid === child.pid) this.currentProcess = null;
|
|
1369
|
+
if (signal) reject(/* @__PURE__ */ new Error(`Worker terminated by signal: ${signal}`));
|
|
1370
|
+
else if (code === 0) resolve(result);
|
|
1371
|
+
else {
|
|
1372
|
+
if (!hasError) logger$1.error(`Worker exited with code ${code}`);
|
|
1373
|
+
reject(/* @__PURE__ */ new Error(`Worker exited with code ${code}`));
|
|
1374
|
+
}
|
|
1375
|
+
});
|
|
1376
|
+
child.on("error", (error) => {
|
|
1377
|
+
this.currentProcess = null;
|
|
1378
|
+
reject(/* @__PURE__ */ new Error(`Failed to spawn worker: ${error.message}`));
|
|
1379
|
+
});
|
|
1380
|
+
child.stdin?.write(JSON.stringify(config$1) + "\n");
|
|
1381
|
+
child.stdin?.end();
|
|
1382
|
+
});
|
|
1383
|
+
}
|
|
1384
|
+
/**
|
|
1385
|
+
* Kill the currently running subprocess
|
|
1386
|
+
*/
|
|
1387
|
+
kill() {
|
|
1388
|
+
if (this.currentProcess) {
|
|
1389
|
+
const processToKill = this.currentProcess;
|
|
1390
|
+
this.currentProcess.kill("SIGTERM");
|
|
1391
|
+
setTimeout(() => {
|
|
1392
|
+
if (processToKill && processToKill.exitCode === null) {
|
|
1393
|
+
logger$1.warn("Child process did not terminate, using SIGKILL");
|
|
1394
|
+
processToKill.kill("SIGKILL");
|
|
1395
|
+
}
|
|
1396
|
+
}, 5e3);
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
/**
|
|
1400
|
+
* Check if a subprocess is currently running
|
|
1401
|
+
*/
|
|
1402
|
+
isRunning() {
|
|
1403
|
+
return this.currentProcess !== null;
|
|
1404
|
+
}
|
|
1405
|
+
};
|
|
1406
|
+
|
|
1407
|
+
//#endregion
|
|
1408
|
+
//#region src/worker-registry.ts
|
|
1409
|
+
/**
|
|
1410
|
+
* Default workers mapped by file extension
|
|
1411
|
+
*/
|
|
1412
|
+
const DEFAULT_WORKERS = {
|
|
1413
|
+
".ts": {
|
|
1414
|
+
command: "node",
|
|
1415
|
+
args: []
|
|
1416
|
+
},
|
|
1417
|
+
".cts": {
|
|
1418
|
+
command: "node",
|
|
1419
|
+
args: []
|
|
1420
|
+
},
|
|
1421
|
+
".mts": {
|
|
1422
|
+
command: "node",
|
|
1423
|
+
args: []
|
|
1424
|
+
},
|
|
1425
|
+
".tsx": {
|
|
1426
|
+
command: "node",
|
|
1427
|
+
args: []
|
|
1428
|
+
},
|
|
1429
|
+
".jsx": {
|
|
1430
|
+
command: "node",
|
|
1431
|
+
args: []
|
|
1432
|
+
},
|
|
1433
|
+
".js": {
|
|
1434
|
+
command: "node",
|
|
1435
|
+
args: []
|
|
1436
|
+
},
|
|
1437
|
+
".mjs": {
|
|
1438
|
+
command: "node",
|
|
1439
|
+
args: []
|
|
1440
|
+
},
|
|
1441
|
+
".cjs": {
|
|
1442
|
+
command: "node",
|
|
1443
|
+
args: []
|
|
1444
|
+
},
|
|
1445
|
+
".py": {
|
|
1446
|
+
command: "python3",
|
|
1447
|
+
args: ["-m", "lmnr.cli.worker"]
|
|
1448
|
+
}
|
|
1449
|
+
};
|
|
1450
|
+
/**
|
|
1451
|
+
* Get the worker command for a given file path or module.
|
|
1452
|
+
* Resolves the TypeScript worker dynamically from @lmnr-ai/lmnr package.
|
|
1453
|
+
*/
|
|
1454
|
+
function getWorkerCommand(filePath, options) {
|
|
1455
|
+
if (options?.pythonModule) return {
|
|
1456
|
+
command: "python3",
|
|
1457
|
+
args: ["-m", "lmnr.cli.worker"]
|
|
1458
|
+
};
|
|
1459
|
+
if (!filePath) throw new Error("Either filePath or pythonModule must be provided");
|
|
1460
|
+
const ext = path.extname(filePath);
|
|
1461
|
+
if (!DEFAULT_WORKERS[ext]) throw new Error(`Unsupported file extension: ${ext}. Supported extensions: ${Object.keys(DEFAULT_WORKERS).join(", ")}`);
|
|
1462
|
+
const worker = DEFAULT_WORKERS[ext];
|
|
1463
|
+
if ([
|
|
1464
|
+
".ts",
|
|
1465
|
+
".tsx",
|
|
1466
|
+
".js",
|
|
1467
|
+
".mjs",
|
|
1468
|
+
".cjs",
|
|
1469
|
+
".mts",
|
|
1470
|
+
".cts",
|
|
1471
|
+
".jsx"
|
|
1472
|
+
].includes(ext)) try {
|
|
1473
|
+
const workerPath = require.resolve("@lmnr-ai/lmnr/dist/cli/worker/index.cjs");
|
|
1474
|
+
return {
|
|
1475
|
+
command: worker.command,
|
|
1476
|
+
args: [workerPath]
|
|
1477
|
+
};
|
|
1478
|
+
} catch (error) {
|
|
1479
|
+
throw new Error(`Failed to resolve TypeScript/JavaScript worker from @lmnr-ai/lmnr package. Make sure @lmnr-ai/lmnr is installed. Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
1480
|
+
}
|
|
1481
|
+
return worker;
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
//#endregion
|
|
1485
|
+
//#region src/commands/dev.ts
|
|
1486
|
+
const logger = initializeLogger();
|
|
1487
|
+
const TS_JS_EXTENSIONS = [
|
|
1488
|
+
".ts",
|
|
1489
|
+
".tsx",
|
|
1490
|
+
".js",
|
|
1491
|
+
".mjs",
|
|
1492
|
+
".cjs",
|
|
1493
|
+
".jsx",
|
|
1494
|
+
".mts",
|
|
1495
|
+
".cts"
|
|
1496
|
+
];
|
|
1497
|
+
const EXTENSIONS_TO_DISCOVER_METADATA = [...TS_JS_EXTENSIONS, ".py"];
|
|
1498
|
+
/**
|
|
1499
|
+
* Discovers function metadata for TypeScript and JavaScript files by:
|
|
1500
|
+
* 1. Extracting TypeScript metadata (params with types from source - TS only)
|
|
1501
|
+
* 2. Building and loading the module with esbuild
|
|
1502
|
+
* 3. Selecting the appropriate function
|
|
1503
|
+
* 4. Matching metadata by span name
|
|
1504
|
+
*
|
|
1505
|
+
* For JavaScript files, TypeScript metadata extraction fails gracefully, but runtime
|
|
1506
|
+
* parameter extraction via regex still works (param names without types).
|
|
1507
|
+
*/
|
|
1508
|
+
const discoverTypeScriptMetadata = async (filePath, options) => {
|
|
1509
|
+
const lmnrPackage = "@lmnr-ai/lmnr";
|
|
1510
|
+
const { extractRolloutFunctions } = require(`${lmnrPackage}/dist/cli/worker/ts-parser.cjs`);
|
|
1511
|
+
const { buildFile, loadModule, selectRolloutFunction } = require(`${lmnrPackage}/dist/cli/worker/build.cjs`);
|
|
1512
|
+
let paramsMetadata;
|
|
1513
|
+
try {
|
|
1514
|
+
paramsMetadata = extractRolloutFunctions(filePath);
|
|
1515
|
+
logger.debug(`Extracted TypeScript metadata for ${paramsMetadata?.size} functions`);
|
|
1516
|
+
} catch (error) {
|
|
1517
|
+
logger.warn("Failed to extract TypeScript metadata, falling back to runtime parsing: " + (error instanceof Error ? error.message : String(error)));
|
|
1518
|
+
}
|
|
1519
|
+
loadModule({
|
|
1520
|
+
filename: filePath,
|
|
1521
|
+
moduleText: await buildFile(filePath, {
|
|
1522
|
+
externalPackages: options.externalPackages,
|
|
1523
|
+
dynamicImportsToSkip: options.dynamicImportsToSkip
|
|
1524
|
+
})
|
|
1525
|
+
});
|
|
1526
|
+
const selectedFunction = selectRolloutFunction(options.function);
|
|
1527
|
+
if (paramsMetadata) {
|
|
1528
|
+
logger.debug(`Available TS metadata keys: ${Array.from(paramsMetadata.keys()).join(", ")}`);
|
|
1529
|
+
logger.debug(`Looking for span name: ${selectedFunction.name} (runtime key: ${selectedFunction.exportName})`);
|
|
1530
|
+
let foundMetadata = null;
|
|
1531
|
+
for (const [exportName, metadata] of paramsMetadata.entries()) {
|
|
1532
|
+
logger.debug(`Checking ${exportName}: span name = ${metadata.name}, export name = ${exportName}`);
|
|
1533
|
+
if (metadata.name === selectedFunction.name) {
|
|
1534
|
+
foundMetadata = metadata;
|
|
1535
|
+
logger.debug(`Match. Export name: ${exportName}, span name: ${metadata.name}`);
|
|
1536
|
+
break;
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
if (foundMetadata) {
|
|
1540
|
+
selectedFunction.params = foundMetadata.params;
|
|
1541
|
+
logger.debug(`Using TypeScript metadata for span: ${selectedFunction.name}`);
|
|
1542
|
+
} else logger.info(`No TypeScript metadata found for span name: ${selectedFunction.name}`);
|
|
1543
|
+
}
|
|
1544
|
+
return {
|
|
1545
|
+
functionName: selectedFunction.name,
|
|
1546
|
+
params: selectedFunction.params || []
|
|
1547
|
+
};
|
|
1548
|
+
};
|
|
1549
|
+
/**
|
|
1550
|
+
* Helper to execute subprocess commands
|
|
1551
|
+
*/
|
|
1552
|
+
const execCommand = async (command, args) => new Promise((resolve, reject) => {
|
|
1553
|
+
const { spawn: spawn$1 } = require("child_process");
|
|
1554
|
+
const child = spawn$1(command, args);
|
|
1555
|
+
let stdout = "";
|
|
1556
|
+
let stderr = "";
|
|
1557
|
+
child.stdout.on("data", (data) => {
|
|
1558
|
+
stdout += data.toString();
|
|
1559
|
+
});
|
|
1560
|
+
child.stderr.on("data", (data) => {
|
|
1561
|
+
stderr += data.toString();
|
|
1562
|
+
});
|
|
1563
|
+
child.on("close", (code) => {
|
|
1564
|
+
if (code === 0) resolve({
|
|
1565
|
+
stdout,
|
|
1566
|
+
stderr
|
|
1567
|
+
});
|
|
1568
|
+
else reject(/* @__PURE__ */ new Error(`Command failed with code ${code}: ${stderr}`));
|
|
1569
|
+
});
|
|
1570
|
+
child.on("error", (error) => {
|
|
1571
|
+
reject(error);
|
|
1572
|
+
});
|
|
1573
|
+
});
|
|
1574
|
+
/**
|
|
1575
|
+
* Discovers function metadata for Python files/modules by calling the lmnr Python CLI
|
|
1576
|
+
*/
|
|
1577
|
+
const discoverPythonMetadata = async (filePathOrModule, options) => {
|
|
1578
|
+
logger.debug(`Discovering Python metadata for ${filePathOrModule}`);
|
|
1579
|
+
const args = ["discover"];
|
|
1580
|
+
if (options.pythonModule) args.push("--module", options.pythonModule);
|
|
1581
|
+
else args.push("--file", filePathOrModule);
|
|
1582
|
+
if (options.function) args.push("--function", options.function);
|
|
1583
|
+
try {
|
|
1584
|
+
const result = await execCommand("lmnr", args);
|
|
1585
|
+
const response = JSON.parse(result.stdout);
|
|
1586
|
+
return {
|
|
1587
|
+
functionName: response.name,
|
|
1588
|
+
params: response.params || []
|
|
1589
|
+
};
|
|
1590
|
+
} catch (error) {
|
|
1591
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1592
|
+
logger.error(`Failed to discover Python metadata: ${errorMessage}`);
|
|
1593
|
+
if (errorMessage.toLowerCase().includes("command not found") || errorMessage.includes("spawn lmnr ENOENT")) {
|
|
1594
|
+
logger.info(`HINT: Make sure latest version of \`lmnr\` python package is installed. \`pip install --upgrade lmnr\`, or if you are running this command from a virtual environment, make sure to activate it. For \`uv\` users, rerun the command with \`uv run\`, e.g. "uv run npx lmnr-cli dev ${filePathOrModule}"`);
|
|
1595
|
+
process.exit(1);
|
|
1596
|
+
}
|
|
1597
|
+
const defaultName = options.pythonModule ? options.pythonModule.split(".").pop() || "main" : path.basename(filePathOrModule, ".py");
|
|
1598
|
+
return {
|
|
1599
|
+
functionName: options.function || defaultName,
|
|
1600
|
+
params: []
|
|
1601
|
+
};
|
|
1602
|
+
}
|
|
1603
|
+
};
|
|
1604
|
+
/**
|
|
1605
|
+
* Generic metadata discovery dispatcher that routes to language-specific implementations
|
|
1606
|
+
*/
|
|
1607
|
+
const discoverFunctionMetadata = async (filePathOrModule, options) => {
|
|
1608
|
+
if (options.pythonModule) return await discoverPythonMetadata(filePathOrModule, options);
|
|
1609
|
+
const ext = path.extname(filePathOrModule);
|
|
1610
|
+
if (TS_JS_EXTENSIONS.includes(ext)) return await discoverTypeScriptMetadata(filePathOrModule, options);
|
|
1611
|
+
if (ext === ".py") return await discoverPythonMetadata(filePathOrModule, options);
|
|
1612
|
+
logger.warn(`No metadata discovery available for ${ext} files`);
|
|
1613
|
+
return {
|
|
1614
|
+
functionName: options.function || path.basename(filePathOrModule, ext),
|
|
1615
|
+
params: []
|
|
1616
|
+
};
|
|
1617
|
+
};
|
|
1618
|
+
function newUUID() {
|
|
1619
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return crypto.randomUUID();
|
|
1620
|
+
return (0, uuid.v4)();
|
|
1621
|
+
}
|
|
1622
|
+
function getFrontendUrl(baseUrl, frontendPort) {
|
|
1623
|
+
let url = baseUrl ?? "https://api.lmnr.ai";
|
|
1624
|
+
if (url === "https://api.lmnr.ai") url = "https://www.laminar.sh";
|
|
1625
|
+
url = url.replace(/\/$/, "");
|
|
1626
|
+
if (/localhost|127\.0\.0\.1/.test(url)) {
|
|
1627
|
+
const port = frontendPort ?? url.match(/:\d{1,5}$/g)?.[0]?.slice(1) ?? 5667;
|
|
1628
|
+
url = url.replace(/:\d{1,5}$/g, "");
|
|
1629
|
+
return `${url}:${port}`;
|
|
1630
|
+
}
|
|
1631
|
+
return url;
|
|
1632
|
+
}
|
|
1633
|
+
/**
|
|
1634
|
+
* Parses request arguments, attempting JSON parse for strings
|
|
1635
|
+
*/
|
|
1636
|
+
const tryParseArg = (arg) => {
|
|
1637
|
+
if (typeof arg === "string") try {
|
|
1638
|
+
return JSON.parse(arg);
|
|
1639
|
+
} catch {
|
|
1640
|
+
return arg;
|
|
1641
|
+
}
|
|
1642
|
+
return arg;
|
|
1643
|
+
};
|
|
1644
|
+
/**
|
|
1645
|
+
* Handles a run event from the backend
|
|
1646
|
+
*/
|
|
1647
|
+
const handleRunEvent = async (event, sessionId, filePathOrModule, client, cacheServerPort, cache, setMetadata, options, subprocessManager) => {
|
|
1648
|
+
logger.debug("Received run event");
|
|
1649
|
+
const { trace_id, path_to_count, args: rawArgs, overrides } = event.data;
|
|
1650
|
+
const parsedArgs = Array.isArray(rawArgs) ? rawArgs.map(tryParseArg) : Object.fromEntries(Object.entries(rawArgs).map(([key, value]) => [key, tryParseArg(value)]));
|
|
1651
|
+
cache.clear();
|
|
1652
|
+
setMetadata({
|
|
1653
|
+
pathToCount: {},
|
|
1654
|
+
overrides
|
|
1655
|
+
});
|
|
1656
|
+
try {
|
|
1657
|
+
if (!trace_id || trace_id.trim() === "") logger.info("No spans in cache, starting fresh");
|
|
1658
|
+
else {
|
|
1659
|
+
const paths = Object.keys(path_to_count || {});
|
|
1660
|
+
if (paths.length === 0) logger.info("No spans to cache, starting fresh");
|
|
1661
|
+
else {
|
|
1662
|
+
const query = `
|
|
1663
|
+
SELECT name, input, output, attributes, path
|
|
1664
|
+
FROM spans
|
|
1665
|
+
WHERE trace_id = {traceId:UUID}
|
|
1666
|
+
AND path IN {paths:String[]}
|
|
1667
|
+
ORDER BY start_time ASC
|
|
1668
|
+
`;
|
|
1669
|
+
logger.debug(`Querying spans from trace ${trace_id}...`);
|
|
1670
|
+
const spans = await client.sql.query(query, {
|
|
1671
|
+
traceId: trace_id,
|
|
1672
|
+
paths
|
|
1673
|
+
});
|
|
1674
|
+
logger.debug(`Received ${spans.length} spans from backend`);
|
|
1675
|
+
const spansByPath = {};
|
|
1676
|
+
for (const span of spans) {
|
|
1677
|
+
const path$2 = span.path;
|
|
1678
|
+
if (!spansByPath[path$2]) spansByPath[path$2] = [];
|
|
1679
|
+
spansByPath[path$2].push(span);
|
|
1680
|
+
}
|
|
1681
|
+
for (const [path$2, pathSpans] of Object.entries(spansByPath)) {
|
|
1682
|
+
const maxCount = path_to_count?.[path$2] || 0;
|
|
1683
|
+
const spansToCache = pathSpans.slice(0, maxCount);
|
|
1684
|
+
spansToCache.forEach((span, index) => {
|
|
1685
|
+
let parsedInput = span.input;
|
|
1686
|
+
let parsedOutput = span.output;
|
|
1687
|
+
let parsedAttributes = span.attributes;
|
|
1688
|
+
try {
|
|
1689
|
+
parsedInput = typeof span.input === "string" ? JSON.parse(span.input) : span.input;
|
|
1690
|
+
} catch {}
|
|
1691
|
+
try {
|
|
1692
|
+
parsedOutput = typeof span.output === "string" ? span.output : JSON.stringify(span.output);
|
|
1693
|
+
} catch {
|
|
1694
|
+
parsedOutput = String(span.output);
|
|
1695
|
+
}
|
|
1696
|
+
try {
|
|
1697
|
+
parsedAttributes = typeof span.attributes === "string" ? JSON.parse(span.attributes) : span.attributes;
|
|
1698
|
+
} catch {
|
|
1699
|
+
parsedAttributes = {};
|
|
1700
|
+
}
|
|
1701
|
+
const cachedSpan = {
|
|
1702
|
+
name: span.name,
|
|
1703
|
+
input: parsedInput,
|
|
1704
|
+
output: parsedOutput,
|
|
1705
|
+
attributes: parsedAttributes
|
|
1706
|
+
};
|
|
1707
|
+
const cacheKey = `${index}:${path$2}`;
|
|
1708
|
+
cache.set(cacheKey, cachedSpan);
|
|
1709
|
+
});
|
|
1710
|
+
logger.info(`Cached ${spansToCache.length} spans for path: ${path$2}`);
|
|
1711
|
+
}
|
|
1712
|
+
setMetadata({
|
|
1713
|
+
pathToCount: path_to_count || {},
|
|
1714
|
+
overrides
|
|
1715
|
+
});
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
const baseUrl = options.baseUrl ?? process.env.LMNR_BASE_URL ?? "https://api.lmnr.ai";
|
|
1719
|
+
const httpPort = options.port ?? (baseUrl.match(/:\d{1,5}$/g) ? parseInt(baseUrl.match(/:\d{1,5}$/g)[0].slice(1)) : 443);
|
|
1720
|
+
const grpcPort = options.grpcPort ?? 8443;
|
|
1721
|
+
const env = {
|
|
1722
|
+
LMNR_ROLLOUT_SESSION_ID: sessionId,
|
|
1723
|
+
LMNR_ROLLOUT_STATE_SERVER_ADDRESS: `http://localhost:${cacheServerPort}`
|
|
1724
|
+
};
|
|
1725
|
+
const workerConfig = {
|
|
1726
|
+
filePath: options.pythonModule ? void 0 : filePathOrModule,
|
|
1727
|
+
modulePath: options.pythonModule,
|
|
1728
|
+
functionName: options.function,
|
|
1729
|
+
args: parsedArgs,
|
|
1730
|
+
env,
|
|
1731
|
+
cacheServerPort,
|
|
1732
|
+
baseUrl,
|
|
1733
|
+
projectApiKey: options.projectApiKey,
|
|
1734
|
+
httpPort,
|
|
1735
|
+
grpcPort,
|
|
1736
|
+
externalPackages: options.externalPackages,
|
|
1737
|
+
dynamicImportsToSkip: options.dynamicImportsToSkip
|
|
1738
|
+
};
|
|
1739
|
+
const workerCommand = options.command ? {
|
|
1740
|
+
command: options.command,
|
|
1741
|
+
args: options.commandArgs ?? []
|
|
1742
|
+
} : getWorkerCommand(options.pythonModule ? void 0 : filePathOrModule, options);
|
|
1743
|
+
try {
|
|
1744
|
+
await client.rolloutSessions.setStatus({
|
|
1745
|
+
sessionId,
|
|
1746
|
+
status: "RUNNING"
|
|
1747
|
+
});
|
|
1748
|
+
} catch (error) {
|
|
1749
|
+
logger.error(`Error setting rollout session status: ${error instanceof Error ? error.message : error}`);
|
|
1750
|
+
}
|
|
1751
|
+
await subprocessManager.execute({
|
|
1752
|
+
command: workerCommand.command,
|
|
1753
|
+
args: workerCommand.args,
|
|
1754
|
+
config: workerConfig
|
|
1755
|
+
});
|
|
1756
|
+
try {
|
|
1757
|
+
await client.rolloutSessions.setStatus({
|
|
1758
|
+
sessionId,
|
|
1759
|
+
status: "FINISHED"
|
|
1760
|
+
});
|
|
1761
|
+
} catch (error) {
|
|
1762
|
+
logger.error(`Error setting rollout session status: ${error instanceof Error ? error.message : error}`);
|
|
1763
|
+
}
|
|
1764
|
+
} catch (error) {
|
|
1765
|
+
logger.error(`Error handling run event: ${error instanceof Error ? error.message : error}`);
|
|
1766
|
+
if (error instanceof Error && error.stack) logger.error(error.stack);
|
|
1767
|
+
try {
|
|
1768
|
+
await client.rolloutSessions.setStatus({
|
|
1769
|
+
sessionId,
|
|
1770
|
+
status: "FINISHED"
|
|
1771
|
+
});
|
|
1772
|
+
} catch (error$1) {
|
|
1773
|
+
logger.error(`Error setting rollout session status: ${error$1 instanceof Error ? error$1.message : error$1}`);
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
};
|
|
1777
|
+
/**
|
|
1778
|
+
* Main dev command handler
|
|
1779
|
+
*/
|
|
1780
|
+
async function runDev(filePath, options = {}) {
|
|
1781
|
+
const isPythonModule = !!options.pythonModule;
|
|
1782
|
+
const filePathOrModule = filePath || options.pythonModule;
|
|
1783
|
+
const sessionId = newUUID();
|
|
1784
|
+
const client = new LaminarClient({
|
|
1785
|
+
baseUrl: options.baseUrl,
|
|
1786
|
+
projectApiKey: options.projectApiKey,
|
|
1787
|
+
port: options.port
|
|
1788
|
+
});
|
|
1789
|
+
logger.debug("Starting cache server...");
|
|
1790
|
+
const { port: cacheServerPort, server: cacheServer, cache, setMetadata } = await startCacheServer();
|
|
1791
|
+
logger.debug(`Cache server started on port ${cacheServerPort}`);
|
|
1792
|
+
const subprocessManager = new SubprocessManager();
|
|
1793
|
+
let functionName = options.function;
|
|
1794
|
+
let params = [];
|
|
1795
|
+
try {
|
|
1796
|
+
if (isPythonModule || filePath && EXTENSIONS_TO_DISCOVER_METADATA.includes(path.extname(filePath))) {
|
|
1797
|
+
logger.debug("Discovering rollout functions...");
|
|
1798
|
+
const metadata = await discoverFunctionMetadata(filePathOrModule, options);
|
|
1799
|
+
functionName = metadata.functionName;
|
|
1800
|
+
params = metadata.params;
|
|
1801
|
+
logger.info(`Serving function: ${functionName}`);
|
|
1802
|
+
logger.debug(`Function parameters: ${JSON.stringify(params, null, 2)}`);
|
|
1803
|
+
} else if (filePath) {
|
|
1804
|
+
functionName = options.function || path.basename(filePath, path.extname(filePath));
|
|
1805
|
+
logger.warn(`Metadata discovery not available for ${path.extname(filePath)} files`);
|
|
1806
|
+
}
|
|
1807
|
+
} catch (error) {
|
|
1808
|
+
logger.error("Failed to discover rollout functions: " + (error instanceof Error ? error.message : String(error)));
|
|
1809
|
+
cacheServer.close();
|
|
1810
|
+
throw error;
|
|
1811
|
+
}
|
|
1812
|
+
logger.debug("Setting up file watcher...");
|
|
1813
|
+
const watcher = chokidar.default.watch(".", {
|
|
1814
|
+
ignored: (path$2) => {
|
|
1815
|
+
const ignoredDirs = [
|
|
1816
|
+
"node_modules",
|
|
1817
|
+
".git",
|
|
1818
|
+
"dist",
|
|
1819
|
+
"build",
|
|
1820
|
+
".next",
|
|
1821
|
+
"coverage",
|
|
1822
|
+
".turbo",
|
|
1823
|
+
"tmp",
|
|
1824
|
+
"temp",
|
|
1825
|
+
"venv",
|
|
1826
|
+
".venv",
|
|
1827
|
+
"virtualenv",
|
|
1828
|
+
".virtualenv",
|
|
1829
|
+
"__pycache__",
|
|
1830
|
+
".pytest_cache",
|
|
1831
|
+
".ruff_cache",
|
|
1832
|
+
".mypy_cache",
|
|
1833
|
+
".cache",
|
|
1834
|
+
".DS_Store"
|
|
1835
|
+
];
|
|
1836
|
+
if (path$2.split(/[/\\]/).some((segment) => ignoredDirs.includes(segment))) return true;
|
|
1837
|
+
if (path$2.endsWith(".log") || path$2.endsWith(".map")) return true;
|
|
1838
|
+
return false;
|
|
1839
|
+
},
|
|
1840
|
+
persistent: true,
|
|
1841
|
+
ignoreInitial: true,
|
|
1842
|
+
awaitWriteFinish: {
|
|
1843
|
+
stabilityThreshold: 100,
|
|
1844
|
+
pollInterval: 100
|
|
1845
|
+
}
|
|
1846
|
+
});
|
|
1847
|
+
logger.debug("Setting up SSE client...");
|
|
1848
|
+
let sseClient = null;
|
|
1849
|
+
try {
|
|
1850
|
+
sseClient = createSSEClient({
|
|
1851
|
+
client,
|
|
1852
|
+
sessionId,
|
|
1853
|
+
params,
|
|
1854
|
+
name: functionName ?? ""
|
|
1855
|
+
});
|
|
1856
|
+
sseClient.on("heartbeat", () => {
|
|
1857
|
+
logger.debug("Heartbeat received");
|
|
1858
|
+
});
|
|
1859
|
+
sseClient.on("run", (event) => {
|
|
1860
|
+
if (subprocessManager.isRunning()) {
|
|
1861
|
+
logger.warn("Rollout is already running, skipping new run");
|
|
1862
|
+
return;
|
|
1863
|
+
}
|
|
1864
|
+
handleRunEvent(event, sessionId, filePathOrModule, client, cacheServerPort, cache, setMetadata, options, subprocessManager).catch((error) => {
|
|
1865
|
+
logger.error("Unhandled error in run event handler: " + (error instanceof Error ? error.message : String(error)));
|
|
1866
|
+
});
|
|
1867
|
+
});
|
|
1868
|
+
sseClient.on("handshake", (event) => {
|
|
1869
|
+
const projectId = event.data.project_id;
|
|
1870
|
+
const sessionId$1 = event.data.session_id;
|
|
1871
|
+
const frontendUrl = getFrontendUrl(options.baseUrl, options.frontendPort);
|
|
1872
|
+
logger.info(`View your session at ${frontendUrl}/project/${projectId}/rollout-sessions/${sessionId$1}`);
|
|
1873
|
+
});
|
|
1874
|
+
sseClient.on("error", (error) => {
|
|
1875
|
+
logger.warn(`Error connecting to backend: ${error.message}`);
|
|
1876
|
+
});
|
|
1877
|
+
sseClient.on("reconnecting", () => {
|
|
1878
|
+
logger.info("Reconnecting to backend...");
|
|
1879
|
+
});
|
|
1880
|
+
sseClient.on("heartbeat_timeout", () => {
|
|
1881
|
+
logger.debug("Heartbeat timeout, reconnecting...");
|
|
1882
|
+
});
|
|
1883
|
+
sseClient.on("stop", () => {
|
|
1884
|
+
logger.debug("Cancelling current run...");
|
|
1885
|
+
subprocessManager.kill();
|
|
1886
|
+
logger.info("Current run cancelled");
|
|
1887
|
+
});
|
|
1888
|
+
watcher.on("change", (changedPath) => {
|
|
1889
|
+
logger.info(`File changed: ${changedPath}, reloading...`);
|
|
1890
|
+
if (subprocessManager.isRunning()) {
|
|
1891
|
+
logger.warn("Cancelling current run due to file change");
|
|
1892
|
+
subprocessManager.kill();
|
|
1893
|
+
}
|
|
1894
|
+
if (isPythonModule || filePath && EXTENSIONS_TO_DISCOVER_METADATA.includes(path.extname(filePath))) discoverFunctionMetadata(filePathOrModule, options).then((metadata) => {
|
|
1895
|
+
logger.debug(`Updated function metadata: ${metadata.functionName}`);
|
|
1896
|
+
logger.debug(`Updated parameters: ${JSON.stringify(metadata.params, null, 2)}`);
|
|
1897
|
+
if (sseClient) {
|
|
1898
|
+
sseClient.updateMetadata(metadata.params, metadata.functionName);
|
|
1899
|
+
logger.debug("Notified backend of metadata changes");
|
|
1900
|
+
}
|
|
1901
|
+
}).catch((error) => {
|
|
1902
|
+
logger.error("Failed to update function metadata: " + (error instanceof Error ? error.message : String(error)));
|
|
1903
|
+
});
|
|
1904
|
+
});
|
|
1905
|
+
const shutdown = () => {
|
|
1906
|
+
logger.debug("Shutting down...");
|
|
1907
|
+
logger.debug("Closing file watcher...");
|
|
1908
|
+
watcher.close().catch((error) => {
|
|
1909
|
+
logger.error(`Failed to close file watcher: ${error instanceof Error ? error.message : error}`);
|
|
1910
|
+
});
|
|
1911
|
+
subprocessManager.kill();
|
|
1912
|
+
logger.debug("Deleting rollout session...");
|
|
1913
|
+
client.rolloutSessions.delete({ sessionId }).then(() => {
|
|
1914
|
+
if (sseClient) sseClient.shutdown();
|
|
1915
|
+
cacheServer.close(() => {
|
|
1916
|
+
logger.debug("Cache server closed");
|
|
1917
|
+
});
|
|
1918
|
+
process.exit(0);
|
|
1919
|
+
}).catch((error) => {
|
|
1920
|
+
logger.warn(`Failed to delete rollout session: ${error instanceof Error ? error.message : error}`);
|
|
1921
|
+
process.exit(1);
|
|
1922
|
+
});
|
|
1923
|
+
};
|
|
1924
|
+
process.on("SIGINT", shutdown);
|
|
1925
|
+
process.on("SIGTERM", shutdown);
|
|
1926
|
+
process.stdin.resume();
|
|
1927
|
+
logger.debug("Connecting to backend...");
|
|
1928
|
+
await sseClient.connectAndListen();
|
|
1929
|
+
} catch (error) {
|
|
1930
|
+
logger.error("Failed to start dev command: " + (error instanceof Error ? error.message : String(error)));
|
|
1931
|
+
try {
|
|
1932
|
+
await client.rolloutSessions.delete({ sessionId });
|
|
1933
|
+
} catch {}
|
|
1934
|
+
await watcher.close();
|
|
1935
|
+
cacheServer.close(() => {
|
|
1936
|
+
process.exit(1);
|
|
1937
|
+
});
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
//#endregion
|
|
1942
|
+
//#region src/index.ts
|
|
1943
|
+
async function main() {
|
|
1944
|
+
const program = new commander.Command();
|
|
1945
|
+
program.name("lmnr-cli").description("CLI for Laminar AI rollout debugging").version(version$1, "-v, --version", "display version number");
|
|
1946
|
+
program.command("dev").description("Start a rollout debugging session").argument("[file]", "Path to file containing the agent function(s). Either `file` or `-m` must be provided.").option("-m, --python-module <module>", "Python module path (e.g., src.myfile). Either `file` or `-m` must be provided.").option("--function <name>", "Specific function to serve (if multiple rollout functions found)").option("--project-api-key <key>", "Project API key. If not provided, reads from LMNR_PROJECT_API_KEY env variable").option("--base-url <url>", "Base URL for the Laminar API. Defaults to https://api.lmnr.ai or LMNR_BASE_URL env variable").option("--port <port>", "Port for the Laminar API. Defaults to 443", (val) => parseInt(val, 10)).option("--grpc-port <port>", "Port for the Laminar gRPC backend. Defaults to 8443", (val) => parseInt(val, 10)).option("--frontend-port <port>", "Port for the Laminar frontend. Defaults to 5667", (val) => parseInt(val, 10)).option("--external-packages <packages...>", "[ADVANCED] List of packages to pass as external to esbuild. This will not link the packages directly into the dev file, but will instead require them at runtime. Read more: https://esbuild.github.io/api/#external").option("--dynamic-imports-to-skip <modules...>", "[ADVANCED] List of module names to skip when encountered as dynamic imports. These dynamic imports will resolve to an empty module to prevent build failures. This is meant to skip the imports that are not used in the rollout itself.").option("--command <command>", "[ADVANCED] Custom command to run the worker (e.g., python3, node)").option("--command-args <args...>", "[ADVANCED] Arguments for the custom command").action(async (file, options) => {
|
|
1947
|
+
if (!file && !options.pythonModule) {
|
|
1948
|
+
console.error("Error: Must provide either a file path or --python-module (-m) flag");
|
|
1949
|
+
process.exit(1);
|
|
1950
|
+
}
|
|
1951
|
+
if (file && options.pythonModule) {
|
|
1952
|
+
console.error("Error: Cannot specify both file path and --python-module (-m) flag");
|
|
1953
|
+
process.exit(1);
|
|
1954
|
+
}
|
|
1955
|
+
await runDev(file, options);
|
|
1956
|
+
}).addHelpText("after", `
|
|
1957
|
+
Examples:
|
|
1958
|
+
$ lmnr-cli dev agent.ts # TypeScript file
|
|
1959
|
+
$ lmnr-cli dev agent.py # Python file (script mode)
|
|
1960
|
+
$ lmnr-cli dev -m src.agent # Python module (module mode)
|
|
1961
|
+
$ lmnr-cli dev agent.ts --function myAgent # Specific function
|
|
1962
|
+
`);
|
|
1963
|
+
await program.parseAsync();
|
|
1964
|
+
}
|
|
1965
|
+
main().catch((err) => {
|
|
1966
|
+
console.error(err instanceof Error ? err.message : err);
|
|
1967
|
+
process.exit(1);
|
|
1968
|
+
});
|
|
1969
|
+
|
|
1970
|
+
//#endregion
|
|
1971
|
+
//# sourceMappingURL=index.cjs.map
|