lmnr-cli 0.1.9 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +410 -1247
- package/dist/index.cjs.map +1 -1
- package/package.json +7 -19
package/dist/index.cjs
CHANGED
|
@@ -6,7 +6,7 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
|
6
6
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
7
|
var __getProtoOf = Object.getPrototypeOf;
|
|
8
8
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
-
var __commonJSMin = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
|
9
|
+
var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
|
|
10
10
|
var __copyProps = (to, from, except, desc) => {
|
|
11
11
|
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
12
12
|
key = keys[i];
|
|
@@ -25,11 +25,12 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
25
25
|
let commander = require("commander");
|
|
26
26
|
let fs = require("fs");
|
|
27
27
|
let path = require("path");
|
|
28
|
+
let path$2 = __toESM(path, 1);
|
|
28
29
|
path = __toESM(path);
|
|
29
30
|
let pino = require("pino");
|
|
31
|
+
let pino$3 = __toESM(pino, 1);
|
|
30
32
|
pino = __toESM(pino);
|
|
31
33
|
let pino_pretty = require("pino-pretty");
|
|
32
|
-
let uuid = require("uuid");
|
|
33
34
|
let csv_parser = require("csv-parser");
|
|
34
35
|
csv_parser = __toESM(csv_parser);
|
|
35
36
|
let export_to_csv = require("export-to-csv");
|
|
@@ -37,104 +38,27 @@ let fs_promises = require("fs/promises");
|
|
|
37
38
|
fs_promises = __toESM(fs_promises);
|
|
38
39
|
let cli_table3 = require("cli-table3");
|
|
39
40
|
cli_table3 = __toESM(cli_table3);
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
let http = require("http");
|
|
43
|
-
http = __toESM(http);
|
|
44
|
-
let events = require("events");
|
|
45
|
-
let eventsource_parser = require("eventsource-parser");
|
|
46
|
-
let child_process = require("child_process");
|
|
47
|
-
let readline = require("readline");
|
|
48
|
-
readline = __toESM(readline);
|
|
49
|
-
//#region package.json
|
|
50
|
-
var version$1 = "0.1.9";
|
|
41
|
+
//#region ../types/dist/index.mjs
|
|
42
|
+
const errorMessage = (error) => error instanceof Error ? error.message : String(error);
|
|
51
43
|
//#endregion
|
|
52
|
-
//#region
|
|
53
|
-
var
|
|
54
|
-
module.exports = {
|
|
55
|
-
"name": "dotenv",
|
|
56
|
-
"version": "17.2.3",
|
|
57
|
-
"description": "Loads environment variables from .env file",
|
|
58
|
-
"main": "lib/main.js",
|
|
59
|
-
"types": "lib/main.d.ts",
|
|
60
|
-
"exports": {
|
|
61
|
-
".": {
|
|
62
|
-
"types": "./lib/main.d.ts",
|
|
63
|
-
"require": "./lib/main.js",
|
|
64
|
-
"default": "./lib/main.js"
|
|
65
|
-
},
|
|
66
|
-
"./config": "./config.js",
|
|
67
|
-
"./config.js": "./config.js",
|
|
68
|
-
"./lib/env-options": "./lib/env-options.js",
|
|
69
|
-
"./lib/env-options.js": "./lib/env-options.js",
|
|
70
|
-
"./lib/cli-options": "./lib/cli-options.js",
|
|
71
|
-
"./lib/cli-options.js": "./lib/cli-options.js",
|
|
72
|
-
"./package.json": "./package.json"
|
|
73
|
-
},
|
|
74
|
-
"scripts": {
|
|
75
|
-
"dts-check": "tsc --project tests/types/tsconfig.json",
|
|
76
|
-
"lint": "standard",
|
|
77
|
-
"pretest": "npm run lint && npm run dts-check",
|
|
78
|
-
"test": "tap run tests/**/*.js --allow-empty-coverage --disable-coverage --timeout=60000",
|
|
79
|
-
"test:coverage": "tap run tests/**/*.js --show-full-coverage --timeout=60000 --coverage-report=text --coverage-report=lcov",
|
|
80
|
-
"prerelease": "npm test",
|
|
81
|
-
"release": "standard-version"
|
|
82
|
-
},
|
|
83
|
-
"repository": {
|
|
84
|
-
"type": "git",
|
|
85
|
-
"url": "git://github.com/motdotla/dotenv.git"
|
|
86
|
-
},
|
|
87
|
-
"homepage": "https://github.com/motdotla/dotenv#readme",
|
|
88
|
-
"funding": "https://dotenvx.com",
|
|
89
|
-
"keywords": [
|
|
90
|
-
"dotenv",
|
|
91
|
-
"env",
|
|
92
|
-
".env",
|
|
93
|
-
"environment",
|
|
94
|
-
"variables",
|
|
95
|
-
"config",
|
|
96
|
-
"settings"
|
|
97
|
-
],
|
|
98
|
-
"readmeFilename": "README.md",
|
|
99
|
-
"license": "BSD-2-Clause",
|
|
100
|
-
"devDependencies": {
|
|
101
|
-
"@types/node": "^18.11.3",
|
|
102
|
-
"decache": "^4.6.2",
|
|
103
|
-
"sinon": "^14.0.1",
|
|
104
|
-
"standard": "^17.0.0",
|
|
105
|
-
"standard-version": "^9.5.0",
|
|
106
|
-
"tap": "^19.2.0",
|
|
107
|
-
"typescript": "^4.8.4"
|
|
108
|
-
},
|
|
109
|
-
"engines": { "node": ">=12" },
|
|
110
|
-
"browser": { "fs": false }
|
|
111
|
-
};
|
|
112
|
-
}));
|
|
44
|
+
//#region package.json
|
|
45
|
+
var version$1 = "0.1.10";
|
|
113
46
|
//#endregion
|
|
114
|
-
//#region
|
|
115
|
-
var
|
|
47
|
+
//#region ../../node_modules/.pnpm/dotenv@17.4.2/node_modules/dotenv/lib/main.js
|
|
48
|
+
var require_main = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
116
49
|
const fs$1 = require("fs");
|
|
117
50
|
const path$1 = require("path");
|
|
118
51
|
const os = require("os");
|
|
119
52
|
const crypto$1 = require("crypto");
|
|
120
|
-
const version = require_package().version;
|
|
121
53
|
const TIPS = [
|
|
122
|
-
"
|
|
123
|
-
"
|
|
124
|
-
"
|
|
125
|
-
"
|
|
126
|
-
"
|
|
127
|
-
"
|
|
128
|
-
"
|
|
129
|
-
"
|
|
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'] }"
|
|
54
|
+
"◈ encrypted .env [www.dotenvx.com]",
|
|
55
|
+
"◈ secrets for agents [www.dotenvx.com]",
|
|
56
|
+
"⌁ auth for agents [www.vestauth.com]",
|
|
57
|
+
"⌘ custom filepath { path: '/custom/path/.env' }",
|
|
58
|
+
"⌘ enable debugging { debug: true }",
|
|
59
|
+
"⌘ override existing { override: true }",
|
|
60
|
+
"⌘ suppress logs { quiet: true }",
|
|
61
|
+
"⌘ multiple files { path: ['.env.local', '.env'] }"
|
|
138
62
|
];
|
|
139
63
|
function _getRandomTip() {
|
|
140
64
|
return TIPS[Math.floor(Math.random() * TIPS.length)];
|
|
@@ -198,13 +122,13 @@ var import_main = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
198
122
|
return DotenvModule.parse(decrypted);
|
|
199
123
|
}
|
|
200
124
|
function _warn(message) {
|
|
201
|
-
console.error(
|
|
125
|
+
console.error(`⚠ ${message}`);
|
|
202
126
|
}
|
|
203
127
|
function _debug(message) {
|
|
204
|
-
console.log(
|
|
128
|
+
console.log(`┆ ${message}`);
|
|
205
129
|
}
|
|
206
130
|
function _log(message) {
|
|
207
|
-
console.log(
|
|
131
|
+
console.log(`◇ ${message}`);
|
|
208
132
|
}
|
|
209
133
|
function _dotenvKey(options) {
|
|
210
134
|
if (options && options.DOTENV_KEY && options.DOTENV_KEY.length > 0) return options.DOTENV_KEY;
|
|
@@ -262,7 +186,7 @@ var import_main = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
262
186
|
function _configVault(options) {
|
|
263
187
|
const debug = parseBoolean(process.env.DOTENV_CONFIG_DEBUG || options && options.debug);
|
|
264
188
|
const quiet = parseBoolean(process.env.DOTENV_CONFIG_QUIET || options && options.quiet);
|
|
265
|
-
if (debug || !quiet) _log("
|
|
189
|
+
if (debug || !quiet) _log("loading env from encrypted .env.vault");
|
|
266
190
|
const parsed = DotenvModule._parseVault(options);
|
|
267
191
|
let processEnv = process.env;
|
|
268
192
|
if (options && options.processEnv != null) processEnv = options.processEnv;
|
|
@@ -277,7 +201,7 @@ var import_main = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
277
201
|
let debug = parseBoolean(processEnv.DOTENV_CONFIG_DEBUG || options && options.debug);
|
|
278
202
|
let quiet = parseBoolean(processEnv.DOTENV_CONFIG_QUIET || options && options.quiet);
|
|
279
203
|
if (options && options.encoding) encoding = options.encoding;
|
|
280
|
-
else if (debug) _debug("
|
|
204
|
+
else if (debug) _debug("no encoding is specified (UTF-8 is used by default)");
|
|
281
205
|
let optionPaths = [dotenvPath];
|
|
282
206
|
if (options && options.path) if (!Array.isArray(options.path)) optionPaths = [_resolveHome(options.path)];
|
|
283
207
|
else {
|
|
@@ -286,11 +210,11 @@ var import_main = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
286
210
|
}
|
|
287
211
|
let lastError;
|
|
288
212
|
const parsedAll = {};
|
|
289
|
-
for (const path$
|
|
290
|
-
const parsed = DotenvModule.parse(fs$1.readFileSync(path$
|
|
213
|
+
for (const path$3 of optionPaths) try {
|
|
214
|
+
const parsed = DotenvModule.parse(fs$1.readFileSync(path$3, { encoding }));
|
|
291
215
|
DotenvModule.populate(parsedAll, parsed, options);
|
|
292
216
|
} catch (e) {
|
|
293
|
-
if (debug) _debug(`
|
|
217
|
+
if (debug) _debug(`failed to load ${path$3} ${e.message}`);
|
|
294
218
|
lastError = e;
|
|
295
219
|
}
|
|
296
220
|
const populated = DotenvModule.populate(processEnv, parsedAll, options);
|
|
@@ -303,10 +227,10 @@ var import_main = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
303
227
|
const relative = path$1.relative(process.cwd(), filePath);
|
|
304
228
|
shortPaths.push(relative);
|
|
305
229
|
} catch (e) {
|
|
306
|
-
if (debug) _debug(`
|
|
230
|
+
if (debug) _debug(`failed to load ${filePath} ${e.message}`);
|
|
307
231
|
lastError = e;
|
|
308
232
|
}
|
|
309
|
-
_log(`
|
|
233
|
+
_log(`injected env (${keysCount}) from ${shortPaths.join(",")} ${dim(`// tip: ${_getRandomTip()}`)}`);
|
|
310
234
|
}
|
|
311
235
|
if (lastError) return {
|
|
312
236
|
parsed: parsedAll,
|
|
@@ -318,7 +242,7 @@ var import_main = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
318
242
|
if (_dotenvKey(options).length === 0) return DotenvModule.configDotenv(options);
|
|
319
243
|
const vaultPath = _vaultPath(options);
|
|
320
244
|
if (!vaultPath) {
|
|
321
|
-
_warn(`
|
|
245
|
+
_warn(`you set DOTENV_KEY but you are missing a .env.vault file at ${vaultPath}`);
|
|
322
246
|
return DotenvModule.configDotenv(options);
|
|
323
247
|
}
|
|
324
248
|
return DotenvModule._configVault(options);
|
|
@@ -387,8 +311,44 @@ var import_main = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
387
311
|
module.exports.parse = DotenvModule.parse;
|
|
388
312
|
module.exports.populate = DotenvModule.populate;
|
|
389
313
|
module.exports = DotenvModule;
|
|
390
|
-
}))
|
|
391
|
-
|
|
314
|
+
}));
|
|
315
|
+
//#endregion
|
|
316
|
+
//#region ../../node_modules/.pnpm/uuid@14.0.0/node_modules/uuid/dist-node/stringify.js
|
|
317
|
+
const byteToHex = [];
|
|
318
|
+
for (let i = 0; i < 256; ++i) byteToHex.push((i + 256).toString(16).slice(1));
|
|
319
|
+
function unsafeStringify(arr, offset = 0) {
|
|
320
|
+
return (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + "-" + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + "-" + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + "-" + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + "-" + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase();
|
|
321
|
+
}
|
|
322
|
+
//#endregion
|
|
323
|
+
//#region ../../node_modules/.pnpm/uuid@14.0.0/node_modules/uuid/dist-node/rng.js
|
|
324
|
+
const rnds8 = new Uint8Array(16);
|
|
325
|
+
function rng() {
|
|
326
|
+
return crypto.getRandomValues(rnds8);
|
|
327
|
+
}
|
|
328
|
+
//#endregion
|
|
329
|
+
//#region ../../node_modules/.pnpm/uuid@14.0.0/node_modules/uuid/dist-node/v4.js
|
|
330
|
+
function v4(options, buf, offset) {
|
|
331
|
+
if (!buf && !options && crypto.randomUUID) return crypto.randomUUID();
|
|
332
|
+
return _v4(options, buf, offset);
|
|
333
|
+
}
|
|
334
|
+
function _v4(options, buf, offset) {
|
|
335
|
+
options = options || {};
|
|
336
|
+
const rnds = options.random ?? options.rng?.() ?? rng();
|
|
337
|
+
if (rnds.length < 16) throw new Error("Random bytes length must be >= 16");
|
|
338
|
+
rnds[6] = rnds[6] & 15 | 64;
|
|
339
|
+
rnds[8] = rnds[8] & 63 | 128;
|
|
340
|
+
if (buf) {
|
|
341
|
+
offset = offset || 0;
|
|
342
|
+
if (offset < 0 || offset + 16 > buf.length) throw new RangeError(`UUID byte range ${offset}:${offset + 15} is out of buffer bounds`);
|
|
343
|
+
for (let i = 0; i < 16; ++i) buf[offset + i] = rnds[i];
|
|
344
|
+
return buf;
|
|
345
|
+
}
|
|
346
|
+
return unsafeStringify(rnds);
|
|
347
|
+
}
|
|
348
|
+
//#endregion
|
|
349
|
+
//#region ../client/dist/index.mjs
|
|
350
|
+
var import_main = require_main();
|
|
351
|
+
var version = "0.8.27";
|
|
392
352
|
function getLangVersion() {
|
|
393
353
|
if (typeof process !== "undefined" && process.versions && process.versions.node) return `node-${process.versions.node}`;
|
|
394
354
|
if (typeof navigator !== "undefined" && navigator.userAgent) return `browser-${navigator.userAgent}`;
|
|
@@ -440,34 +400,34 @@ var BrowserEventsResource = class extends BaseResource {
|
|
|
440
400
|
function initializeLogger$1(options) {
|
|
441
401
|
const colorize = options?.colorize ?? true;
|
|
442
402
|
const level = options?.level ?? process.env.LMNR_LOG_LEVEL?.toLowerCase()?.trim() ?? "info";
|
|
443
|
-
return (0, pino.default)({ level }, (0, pino_pretty.PinoPretty)({
|
|
403
|
+
return (0, pino$3.default)({ level }, (0, pino_pretty.PinoPretty)({
|
|
444
404
|
colorize,
|
|
445
405
|
minimumLevel: level
|
|
446
406
|
}));
|
|
447
407
|
}
|
|
448
|
-
const logger$
|
|
408
|
+
const logger$4$1 = initializeLogger$1();
|
|
449
409
|
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);
|
|
450
|
-
const newUUID
|
|
410
|
+
const newUUID = () => {
|
|
451
411
|
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return crypto.randomUUID();
|
|
452
|
-
else return
|
|
412
|
+
else return v4();
|
|
453
413
|
};
|
|
454
414
|
const otelSpanIdToUUID = (spanId) => {
|
|
455
415
|
let id = spanId.toLowerCase();
|
|
456
416
|
if (id.startsWith("0x")) id = id.slice(2);
|
|
457
|
-
if (id.length !== 16) logger$
|
|
417
|
+
if (id.length !== 16) logger$4$1.warn(`Span ID ${spanId} is not 16 hex chars long. This is not a valid OpenTelemetry span ID.`);
|
|
458
418
|
if (!/^[0-9a-f]+$/.test(id)) {
|
|
459
|
-
logger$
|
|
460
|
-
return newUUID
|
|
419
|
+
logger$4$1.error(`Span ID ${spanId} is not a valid hex string. Generating a random UUID instead.`);
|
|
420
|
+
return newUUID();
|
|
461
421
|
}
|
|
462
422
|
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");
|
|
463
423
|
};
|
|
464
424
|
const otelTraceIdToUUID = (traceId) => {
|
|
465
425
|
let id = traceId.toLowerCase();
|
|
466
426
|
if (id.startsWith("0x")) id = id.slice(2);
|
|
467
|
-
if (id.length !== 32) logger$
|
|
427
|
+
if (id.length !== 32) logger$4$1.warn(`Trace ID ${traceId} is not 32 hex chars long. This is not a valid OpenTelemetry trace ID.`);
|
|
468
428
|
if (!/^[0-9a-f]+$/.test(id)) {
|
|
469
|
-
logger$
|
|
470
|
-
return newUUID
|
|
429
|
+
logger$4$1.error(`Trace ID ${traceId} is not a valid hex string. Generating a random UUID instead.`);
|
|
430
|
+
return newUUID();
|
|
471
431
|
}
|
|
472
432
|
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");
|
|
473
433
|
};
|
|
@@ -490,11 +450,11 @@ const loadEnv = (options) => {
|
|
|
490
450
|
const verbose = ["debug", "trace"].includes(logLevel.trim().toLowerCase());
|
|
491
451
|
const quiet = options?.quiet ?? !verbose;
|
|
492
452
|
(0, import_main.config)({
|
|
493
|
-
path: options?.paths ?? envFiles.map((envFile) => path.resolve(envDir, envFile)),
|
|
453
|
+
path: options?.paths ?? envFiles.map((envFile) => path$2.resolve(envDir, envFile)),
|
|
494
454
|
quiet
|
|
495
455
|
});
|
|
496
456
|
};
|
|
497
|
-
const logger$
|
|
457
|
+
const logger$3$1 = initializeLogger$1();
|
|
498
458
|
const DEFAULT_DATASET_PULL_LIMIT = 100;
|
|
499
459
|
const DEFAULT_DATASET_PUSH_BATCH_SIZE$1 = 100;
|
|
500
460
|
var DatasetsResource = class extends BaseResource {
|
|
@@ -549,7 +509,7 @@ var DatasetsResource = class extends BaseResource {
|
|
|
549
509
|
let response;
|
|
550
510
|
for (let i = 0; i < points.length; i += batchSize) {
|
|
551
511
|
const batchNum = Math.floor(i / batchSize) + 1;
|
|
552
|
-
logger$
|
|
512
|
+
logger$3$1.debug(`Pushing batch ${batchNum} of ${totalBatches}`);
|
|
553
513
|
const batch = points.slice(i, i + batchSize);
|
|
554
514
|
const fetchResponse = await fetch(this.baseHttpUrl + "/v1/datasets/datapoints", {
|
|
555
515
|
method: "POST",
|
|
@@ -597,7 +557,7 @@ var DatasetsResource = class extends BaseResource {
|
|
|
597
557
|
return response.json();
|
|
598
558
|
}
|
|
599
559
|
};
|
|
600
|
-
const logger$
|
|
560
|
+
const logger$2$1 = initializeLogger$1();
|
|
601
561
|
const INITIAL_EVALUATION_DATAPOINT_MAX_DATA_LENGTH = 16e6;
|
|
602
562
|
var EvalsResource = class extends BaseResource {
|
|
603
563
|
constructor(baseHttpUrl, projectApiKey) {
|
|
@@ -655,14 +615,14 @@ var EvalsResource = class extends BaseResource {
|
|
|
655
615
|
* @returns {Promise<StringUUID>} The datapoint ID
|
|
656
616
|
*/
|
|
657
617
|
async createDatapoint({ evalId, data, target, metadata, index, traceId }) {
|
|
658
|
-
const datapointId = newUUID
|
|
618
|
+
const datapointId = newUUID();
|
|
659
619
|
const partialDatapoint = {
|
|
660
620
|
id: datapointId,
|
|
661
621
|
data,
|
|
662
622
|
target,
|
|
663
623
|
index: index ?? 0,
|
|
664
|
-
traceId: traceId ?? newUUID
|
|
665
|
-
executorSpanId: newUUID
|
|
624
|
+
traceId: traceId ?? newUUID(),
|
|
625
|
+
executorSpanId: newUUID(),
|
|
666
626
|
metadata
|
|
667
627
|
};
|
|
668
628
|
await this.saveDatapoints({
|
|
@@ -733,7 +693,7 @@ var EvalsResource = class extends BaseResource {
|
|
|
733
693
|
* @returns {Promise<GetDatapointsResponse>} Response from the datapoint retrieval
|
|
734
694
|
*/
|
|
735
695
|
async getDatapoints({ datasetName, offset, limit }) {
|
|
736
|
-
logger$
|
|
696
|
+
logger$2$1.warn("evals.getDatapoints() is deprecated. Use client.datasets.pull() instead.");
|
|
737
697
|
const params = new URLSearchParams({
|
|
738
698
|
name: datasetName,
|
|
739
699
|
offset: offset.toString(),
|
|
@@ -750,7 +710,7 @@ var EvalsResource = class extends BaseResource {
|
|
|
750
710
|
let length = initialLength;
|
|
751
711
|
let lastResponse = null;
|
|
752
712
|
for (let i = 0; i < maxRetries; i++) {
|
|
753
|
-
logger$
|
|
713
|
+
logger$2$1.debug(`Retrying save datapoints... ${i + 1} of ${maxRetries}, length: ${length}`);
|
|
754
714
|
const response = await fetch(this.baseHttpUrl + `/v1/evals/${evalId}/datapoints`, {
|
|
755
715
|
method: "POST",
|
|
756
716
|
headers: this.headers(),
|
|
@@ -771,11 +731,6 @@ var EvalsResource = class extends BaseResource {
|
|
|
771
731
|
if (lastResponse && !lastResponse.ok) await this.handleError(lastResponse);
|
|
772
732
|
}
|
|
773
733
|
};
|
|
774
|
-
var EvaluatorScoreSourceType = /* @__PURE__ */ function(EvaluatorScoreSourceType) {
|
|
775
|
-
EvaluatorScoreSourceType["Evaluator"] = "Evaluator";
|
|
776
|
-
EvaluatorScoreSourceType["Code"] = "Code";
|
|
777
|
-
return EvaluatorScoreSourceType;
|
|
778
|
-
}(EvaluatorScoreSourceType || {});
|
|
779
734
|
/**
|
|
780
735
|
* Resource for creating evaluator scores
|
|
781
736
|
*/
|
|
@@ -814,25 +769,21 @@ var EvaluatorsResource = class extends BaseResource {
|
|
|
814
769
|
async score(options) {
|
|
815
770
|
const { name, metadata, score } = options;
|
|
816
771
|
let payload;
|
|
817
|
-
if ("traceId" in options && options.traceId) {
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
source: EvaluatorScoreSourceType.Code,
|
|
833
|
-
spanId: formattedSpanId
|
|
834
|
-
};
|
|
835
|
-
} else throw new Error("Either 'traceId' or 'spanId' must be provided.");
|
|
772
|
+
if ("traceId" in options && options.traceId) payload = {
|
|
773
|
+
name,
|
|
774
|
+
metadata,
|
|
775
|
+
score,
|
|
776
|
+
source: "Code",
|
|
777
|
+
traceId: isStringUUID(options.traceId) ? options.traceId : otelTraceIdToUUID(options.traceId)
|
|
778
|
+
};
|
|
779
|
+
else if ("spanId" in options && options.spanId) payload = {
|
|
780
|
+
name,
|
|
781
|
+
metadata,
|
|
782
|
+
score,
|
|
783
|
+
source: "Code",
|
|
784
|
+
spanId: isStringUUID(options.spanId) ? options.spanId : otelSpanIdToUUID(options.spanId)
|
|
785
|
+
};
|
|
786
|
+
else throw new Error("Either 'traceId' or 'spanId' must be provided.");
|
|
836
787
|
const response = await fetch(this.baseHttpUrl + "/v1/evaluators/score", {
|
|
837
788
|
method: "POST",
|
|
838
789
|
headers: this.headers(),
|
|
@@ -841,60 +792,51 @@ var EvaluatorsResource = class extends BaseResource {
|
|
|
841
792
|
if (!response.ok) await this.handleError(response);
|
|
842
793
|
}
|
|
843
794
|
};
|
|
795
|
+
const logger$1$1 = initializeLogger$1();
|
|
844
796
|
var RolloutSessionsResource = class extends BaseResource {
|
|
845
797
|
constructor(baseHttpUrl, projectApiKey) {
|
|
846
798
|
super(baseHttpUrl, projectApiKey);
|
|
847
799
|
}
|
|
848
800
|
/**
|
|
849
|
-
*
|
|
850
|
-
*
|
|
801
|
+
* Idempotently register (upsert) a debug session on the backend, keyed on the
|
|
802
|
+
* SDK-supplied session id. The backend stores the row so the session is
|
|
803
|
+
* visible in the UI; a null/omitted name never clobbers a name set elsewhere.
|
|
804
|
+
*
|
|
805
|
+
* Returns the backend-resolved `projectId` (derived from the API key) so the
|
|
806
|
+
* caller can build the debugger URL; null if the body can't be parsed.
|
|
851
807
|
*/
|
|
852
|
-
async
|
|
808
|
+
async register({ sessionId, name }) {
|
|
853
809
|
const response = await fetch(`${this.baseHttpUrl}/v1/rollouts/${sessionId}`, {
|
|
854
810
|
method: "POST",
|
|
855
|
-
headers:
|
|
856
|
-
|
|
857
|
-
"Accept": "text/event-stream"
|
|
858
|
-
},
|
|
859
|
-
body: JSON.stringify({
|
|
860
|
-
name,
|
|
861
|
-
params
|
|
862
|
-
}),
|
|
863
|
-
signal
|
|
864
|
-
});
|
|
865
|
-
if (!response.ok) throw new Error(`SSE connection failed: ${response.status} ${response.statusText}`);
|
|
866
|
-
if (!response.body) throw new Error("No response body");
|
|
867
|
-
return response;
|
|
868
|
-
}
|
|
869
|
-
async delete({ sessionId }) {
|
|
870
|
-
const response = await fetch(`${this.baseHttpUrl}/v1/rollouts/${sessionId}`, {
|
|
871
|
-
method: "DELETE",
|
|
872
|
-
headers: this.headers()
|
|
811
|
+
headers: this.headers(),
|
|
812
|
+
body: JSON.stringify({ name })
|
|
873
813
|
});
|
|
874
814
|
if (!response.ok) await this.handleError(response);
|
|
815
|
+
try {
|
|
816
|
+
return (await response.json()).projectId ?? null;
|
|
817
|
+
} catch (e) {
|
|
818
|
+
logger$1$1.warn(`Failed to parse rollout register response: ${errorMessage(e)}`);
|
|
819
|
+
return null;
|
|
820
|
+
}
|
|
875
821
|
}
|
|
876
|
-
|
|
877
|
-
|
|
822
|
+
/**
|
|
823
|
+
* Rename an existing debug session. Update-only: the backend returns 404 (and
|
|
824
|
+
* this throws) when the session id is unknown for the project, so a mistyped
|
|
825
|
+
* id surfaces as an error rather than silently creating a session. Creation
|
|
826
|
+
* stays the SDK's job via {@link register}.
|
|
827
|
+
*/
|
|
828
|
+
async setName({ sessionId, name }) {
|
|
829
|
+
const response = await fetch(`${this.baseHttpUrl}/v1/rollouts/${sessionId}/name`, {
|
|
878
830
|
method: "PATCH",
|
|
879
831
|
headers: this.headers(),
|
|
880
|
-
body: JSON.stringify({
|
|
832
|
+
body: JSON.stringify({ name })
|
|
881
833
|
});
|
|
882
834
|
if (!response.ok) await this.handleError(response);
|
|
883
835
|
}
|
|
884
|
-
async
|
|
885
|
-
const response = await fetch(`${this.baseHttpUrl}/v1/rollouts/${sessionId}
|
|
886
|
-
method: "
|
|
887
|
-
headers: this.headers()
|
|
888
|
-
body: JSON.stringify({
|
|
889
|
-
type: "spanStart",
|
|
890
|
-
spanId: otelSpanIdToUUID(span.spanId),
|
|
891
|
-
traceId: otelTraceIdToUUID(span.traceId),
|
|
892
|
-
parentSpanId: span.parentSpanId ? otelSpanIdToUUID(span.parentSpanId) : void 0,
|
|
893
|
-
attributes: span.attributes,
|
|
894
|
-
startTime: span.startTime,
|
|
895
|
-
name: span.name,
|
|
896
|
-
spanType: span.spanType
|
|
897
|
-
})
|
|
836
|
+
async delete({ sessionId }) {
|
|
837
|
+
const response = await fetch(`${this.baseHttpUrl}/v1/rollouts/${sessionId}`, {
|
|
838
|
+
method: "DELETE",
|
|
839
|
+
headers: this.headers()
|
|
898
840
|
});
|
|
899
841
|
if (!response.ok) await this.handleError(response);
|
|
900
842
|
}
|
|
@@ -971,12 +913,91 @@ var TagsResource = class extends BaseResource {
|
|
|
971
913
|
return response.json();
|
|
972
914
|
}
|
|
973
915
|
};
|
|
916
|
+
/** Resource for post-factum operations on existing traces. */
|
|
917
|
+
const logger$5 = initializeLogger$1();
|
|
918
|
+
var TracesResource = class extends BaseResource {
|
|
919
|
+
/** Resource for post-factum operations on existing traces. */
|
|
920
|
+
constructor(baseHttpUrl, projectApiKey) {
|
|
921
|
+
super(baseHttpUrl, projectApiKey);
|
|
922
|
+
}
|
|
923
|
+
/**
|
|
924
|
+
* Push a metadata patch to an existing trace.
|
|
925
|
+
*
|
|
926
|
+
* The patch is shallow-merged server-side into the trace's existing metadata
|
|
927
|
+
* (`existing || patch`, last-write-wins per top-level key). Useful for
|
|
928
|
+
* attaching post-factum signals — quality scores, human edits, triage labels —
|
|
929
|
+
* to a trace that has already finished. The patch does NOT extend `endTime`
|
|
930
|
+
* or change tokens / cost / top span / tags / span names. `numSpans` is
|
|
931
|
+
* incremented by 1 (paid by the virtual span that carried the patch through
|
|
932
|
+
* the ingestion queue) so the new ClickHouse row beats the prior version on
|
|
933
|
+
* `ReplacingMergeTree(numSpans)`. No row is added to the `spans` table.
|
|
934
|
+
*
|
|
935
|
+
* Compared to `Laminar.setTraceMetadata` (which sets metadata on the
|
|
936
|
+
* currently in-flight trace via OpenTelemetry attributes), this method
|
|
937
|
+
* operates on a finished trace by trace id, so it must be called after the
|
|
938
|
+
* trace has been flushed.
|
|
939
|
+
*
|
|
940
|
+
* A 404 response (the trace was not found in the project — typically because
|
|
941
|
+
* it has not been flushed yet) is logged as a warning and the call returns
|
|
942
|
+
* without throwing, since the 404 may be expected when pushing too soon
|
|
943
|
+
* after the trace run. Pass `failOnNotFound: true` to throw instead (e.g.
|
|
944
|
+
* CLI callers that must report the failure). Any other non-OK status throws.
|
|
945
|
+
*
|
|
946
|
+
* @param traceId - The trace id to push metadata to. Accepts a UUID string
|
|
947
|
+
* or a 32-char OTel hex trace id.
|
|
948
|
+
* @param metadata - The metadata patch. Top-level keys are merged into the
|
|
949
|
+
* trace's existing metadata. Must be non-empty (the server rejects empty
|
|
950
|
+
* patches with 400).
|
|
951
|
+
* @param options - `failOnNotFound`: throw on 404 instead of warn-and-return.
|
|
952
|
+
* @example
|
|
953
|
+
* ```typescript
|
|
954
|
+
* import { Laminar, observe, LaminarClient } from "@lmnr-ai/lmnr";
|
|
955
|
+
* Laminar.initialize();
|
|
956
|
+
* const client = new LaminarClient();
|
|
957
|
+
*
|
|
958
|
+
* let traceId: string | null = null;
|
|
959
|
+
* await observe({ name: "generate" }, async () => {
|
|
960
|
+
* traceId = await Laminar.getTraceId();
|
|
961
|
+
* });
|
|
962
|
+
* await Laminar.flush();
|
|
963
|
+
*
|
|
964
|
+
* if (traceId) {
|
|
965
|
+
* await client.traces.pushMetadata(traceId, {
|
|
966
|
+
* score: 0.85,
|
|
967
|
+
* reviewer: "alice",
|
|
968
|
+
* needsReview: false,
|
|
969
|
+
* });
|
|
970
|
+
* }
|
|
971
|
+
* ```
|
|
972
|
+
*/
|
|
973
|
+
async pushMetadata(traceId, metadata, options) {
|
|
974
|
+
if (!metadata || Object.keys(metadata).length === 0) throw new Error("metadata must be a non-empty object");
|
|
975
|
+
const formattedTraceId = isStringUUID(traceId) ? traceId : otelTraceIdToUUID(traceId);
|
|
976
|
+
const url = this.baseHttpUrl + "/v1/traces/metadata";
|
|
977
|
+
const response = await fetch(url, {
|
|
978
|
+
method: "POST",
|
|
979
|
+
headers: this.headers(),
|
|
980
|
+
body: JSON.stringify({
|
|
981
|
+
traceId: formattedTraceId,
|
|
982
|
+
metadata
|
|
983
|
+
})
|
|
984
|
+
});
|
|
985
|
+
if (response.status === 404) {
|
|
986
|
+
const message = `Trace ${formattedTraceId} not found. The trace may not have been flushed yet — call await Laminar.flush() and retry.`;
|
|
987
|
+
if (options?.failOnNotFound) throw new Error(message);
|
|
988
|
+
logger$5.warn(message);
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
if (!response.ok) await this.handleError(response);
|
|
992
|
+
}
|
|
993
|
+
};
|
|
974
994
|
var LaminarClient = class {
|
|
975
995
|
constructor({ baseUrl, projectApiKey, port } = {}) {
|
|
976
996
|
loadEnv();
|
|
977
997
|
this.projectApiKey = projectApiKey ?? process.env.LMNR_PROJECT_API_KEY;
|
|
978
998
|
const httpPort = port ?? (baseUrl?.match(/:\d{1,5}$/g) ? parseInt(baseUrl.match(/:\d{1,5}$/g)[0].slice(1)) : 443);
|
|
979
|
-
|
|
999
|
+
const baseUrlNoPort = (baseUrl ?? process.env.LMNR_BASE_URL)?.replace(/\/$/, "").replace(/:\d{1,5}$/g, "");
|
|
1000
|
+
this.baseUrl = `${baseUrlNoPort ?? "https://api.lmnr.ai"}:${httpPort}`;
|
|
980
1001
|
this._browserEvents = new BrowserEventsResource(this.baseUrl, this.projectApiKey);
|
|
981
1002
|
this._datasets = new DatasetsResource(this.baseUrl, this.projectApiKey);
|
|
982
1003
|
this._evals = new EvalsResource(this.baseUrl, this.projectApiKey);
|
|
@@ -984,6 +1005,7 @@ var LaminarClient = class {
|
|
|
984
1005
|
this._rolloutSessions = new RolloutSessionsResource(this.baseUrl, this.projectApiKey);
|
|
985
1006
|
this._sql = new SqlResource(this.baseUrl, this.projectApiKey);
|
|
986
1007
|
this._tags = new TagsResource(this.baseUrl, this.projectApiKey);
|
|
1008
|
+
this._traces = new TracesResource(this.baseUrl, this.projectApiKey);
|
|
987
1009
|
}
|
|
988
1010
|
get browserEvents() {
|
|
989
1011
|
return this._browserEvents;
|
|
@@ -1006,6 +1028,9 @@ var LaminarClient = class {
|
|
|
1006
1028
|
get tags() {
|
|
1007
1029
|
return this._tags;
|
|
1008
1030
|
}
|
|
1031
|
+
get traces() {
|
|
1032
|
+
return this._traces;
|
|
1033
|
+
}
|
|
1009
1034
|
};
|
|
1010
1035
|
//#endregion
|
|
1011
1036
|
//#region src/utils/logger.ts
|
|
@@ -1020,7 +1045,7 @@ function initializeLogger(options) {
|
|
|
1020
1045
|
}
|
|
1021
1046
|
//#endregion
|
|
1022
1047
|
//#region src/utils/file.ts
|
|
1023
|
-
const logger$
|
|
1048
|
+
const logger$4 = initializeLogger();
|
|
1024
1049
|
/**
|
|
1025
1050
|
* Check if a file has a supported extension.
|
|
1026
1051
|
*/
|
|
@@ -1041,7 +1066,7 @@ const collectFiles = async (paths, recursive = false) => {
|
|
|
1041
1066
|
for (const filepath of paths) try {
|
|
1042
1067
|
const stats = await fs_promises.stat(filepath);
|
|
1043
1068
|
if (stats.isFile()) if (isSupportedFile(filepath)) collectedFiles.push(filepath);
|
|
1044
|
-
else logger$
|
|
1069
|
+
else logger$4.warn(`Skipping unsupported file type: ${filepath}`);
|
|
1045
1070
|
else if (stats.isDirectory()) {
|
|
1046
1071
|
const entries = await fs_promises.readdir(filepath);
|
|
1047
1072
|
for (const entry of entries) {
|
|
@@ -1055,7 +1080,7 @@ const collectFiles = async (paths, recursive = false) => {
|
|
|
1055
1080
|
}
|
|
1056
1081
|
}
|
|
1057
1082
|
} catch (error) {
|
|
1058
|
-
logger$
|
|
1083
|
+
logger$4.warn(`Path does not exist or is not accessible: ${filepath}. Error: ${errorMessage(error)}`);
|
|
1059
1084
|
}
|
|
1060
1085
|
return collectedFiles;
|
|
1061
1086
|
};
|
|
@@ -1077,7 +1102,7 @@ const tryParseJson = (content) => {
|
|
|
1077
1102
|
try {
|
|
1078
1103
|
return JSON.parse(content);
|
|
1079
1104
|
} catch (error) {
|
|
1080
|
-
logger$
|
|
1105
|
+
logger$4.debug(`Error parsing JSON: ${errorMessage(error)}`);
|
|
1081
1106
|
return content;
|
|
1082
1107
|
}
|
|
1083
1108
|
};
|
|
@@ -1118,17 +1143,17 @@ async function readFile(filepath) {
|
|
|
1118
1143
|
const loadFromPaths = async (paths, recursive = false) => {
|
|
1119
1144
|
const files = await collectFiles(paths, recursive);
|
|
1120
1145
|
if (files.length === 0) {
|
|
1121
|
-
logger$
|
|
1146
|
+
logger$4.warn("No supported files found in the specified paths");
|
|
1122
1147
|
return [];
|
|
1123
1148
|
}
|
|
1124
|
-
logger$
|
|
1149
|
+
logger$4.info(`Found ${files.length} file(s) to read`);
|
|
1125
1150
|
const result = [];
|
|
1126
1151
|
for (const file of files) try {
|
|
1127
1152
|
const data = await readFile(file);
|
|
1128
1153
|
result.push(...data);
|
|
1129
|
-
logger$
|
|
1154
|
+
logger$4.info(`Read ${data.length} record(s) from ${file}`);
|
|
1130
1155
|
} catch (error) {
|
|
1131
|
-
logger$
|
|
1156
|
+
logger$4.error(`Error reading file ${file}: ${errorMessage(error)}`);
|
|
1132
1157
|
throw error;
|
|
1133
1158
|
}
|
|
1134
1159
|
return result;
|
|
@@ -1168,7 +1193,7 @@ const writeToFile = async (filepath, data, format) => {
|
|
|
1168
1193
|
const dir = path.dirname(filepath);
|
|
1169
1194
|
await fs_promises.mkdir(dir, { recursive: true });
|
|
1170
1195
|
const ext = format ?? path.extname(filepath).slice(1);
|
|
1171
|
-
if (format && format !== path.extname(filepath).slice(1)) logger$
|
|
1196
|
+
if (format && format !== path.extname(filepath).slice(1)) logger$4.warn(`Output format ${format} does not match file extension ${path.extname(filepath).slice(1)}`);
|
|
1172
1197
|
if (ext === "json") await writeJsonFile(filepath, data);
|
|
1173
1198
|
else if (ext === "csv") await writeCsvFile(filepath, data);
|
|
1174
1199
|
else if (ext === "jsonl") await writeJsonlFile(filepath, data);
|
|
@@ -1193,7 +1218,7 @@ const printToConsole = (data, format = "json") => {
|
|
|
1193
1218
|
if (format === "json") console.log(JSON.stringify(data, null, 2));
|
|
1194
1219
|
else if (format === "csv") {
|
|
1195
1220
|
if (data.length === 0) {
|
|
1196
|
-
logger$
|
|
1221
|
+
logger$4.error("No data to print");
|
|
1197
1222
|
return;
|
|
1198
1223
|
}
|
|
1199
1224
|
console.log(formatCsv(data));
|
|
@@ -1214,7 +1239,7 @@ function outputJson(data) {
|
|
|
1214
1239
|
* Use this in --json mode so agents can parse the failure.
|
|
1215
1240
|
*/
|
|
1216
1241
|
function outputJsonError(error, exitCode = 1) {
|
|
1217
|
-
console.log(JSON.stringify({ error:
|
|
1242
|
+
console.log(JSON.stringify({ error: errorMessage(error) }));
|
|
1218
1243
|
process.exit(exitCode);
|
|
1219
1244
|
}
|
|
1220
1245
|
//#endregion
|
|
@@ -1280,7 +1305,7 @@ function renderTable(head, rows) {
|
|
|
1280
1305
|
}
|
|
1281
1306
|
//#endregion
|
|
1282
1307
|
//#region src/commands/dataset/index.ts
|
|
1283
|
-
const logger$
|
|
1308
|
+
const logger$3 = initializeLogger();
|
|
1284
1309
|
const DEFAULT_DATASET_PULL_BATCH_SIZE = 100;
|
|
1285
1310
|
const DEFAULT_DATASET_PUSH_BATCH_SIZE = 100;
|
|
1286
1311
|
/**
|
|
@@ -1341,7 +1366,7 @@ const handleDatasetsList = async (options) => {
|
|
|
1341
1366
|
console.log(`\nTotal: ${datasets.length} dataset(s)\n`);
|
|
1342
1367
|
} catch (error) {
|
|
1343
1368
|
if (options.json) outputJsonError(error);
|
|
1344
|
-
logger$
|
|
1369
|
+
logger$3.error(`Failed to list datasets: ${errorMessage(error)}`);
|
|
1345
1370
|
process.exit(1);
|
|
1346
1371
|
}
|
|
1347
1372
|
};
|
|
@@ -1351,12 +1376,12 @@ const handleDatasetsList = async (options) => {
|
|
|
1351
1376
|
const handleDatasetsPush = async (paths, options) => {
|
|
1352
1377
|
if (!options.name && !options.id) {
|
|
1353
1378
|
if (options.json) outputJsonError("Either name or id must be provided");
|
|
1354
|
-
logger$
|
|
1379
|
+
logger$3.error("Either name or id must be provided");
|
|
1355
1380
|
process.exit(1);
|
|
1356
1381
|
}
|
|
1357
1382
|
if (options.name && options.id) {
|
|
1358
1383
|
if (options.json) outputJsonError("Only one of name or id must be provided");
|
|
1359
|
-
logger$
|
|
1384
|
+
logger$3.error("Only one of name or id must be provided");
|
|
1360
1385
|
process.exit(1);
|
|
1361
1386
|
}
|
|
1362
1387
|
const client = new LaminarClient({
|
|
@@ -1368,7 +1393,7 @@ const handleDatasetsPush = async (paths, options) => {
|
|
|
1368
1393
|
const data = await loadFromPaths(paths, options.recursive);
|
|
1369
1394
|
if (data.length === 0) {
|
|
1370
1395
|
if (options.json) outputJsonError("No data to push");
|
|
1371
|
-
logger$
|
|
1396
|
+
logger$3.error("No data to push. Skipping");
|
|
1372
1397
|
process.exit(1);
|
|
1373
1398
|
}
|
|
1374
1399
|
const identifier = options.name ? { name: options.name } : { id: options.id };
|
|
@@ -1384,10 +1409,10 @@ const handleDatasetsPush = async (paths, options) => {
|
|
|
1384
1409
|
});
|
|
1385
1410
|
return;
|
|
1386
1411
|
}
|
|
1387
|
-
logger$
|
|
1412
|
+
logger$3.info(`Pushed ${data.length} data points to dataset ${options.name || options.id}`);
|
|
1388
1413
|
} catch (error) {
|
|
1389
1414
|
if (options.json) outputJsonError(error);
|
|
1390
|
-
logger$
|
|
1415
|
+
logger$3.error(`Failed to push dataset: ${errorMessage(error)}`);
|
|
1391
1416
|
process.exit(1);
|
|
1392
1417
|
}
|
|
1393
1418
|
};
|
|
@@ -1397,12 +1422,12 @@ const handleDatasetsPush = async (paths, options) => {
|
|
|
1397
1422
|
const handleDatasetsPull = async (outputPath, options) => {
|
|
1398
1423
|
if (!options.name && !options.id) {
|
|
1399
1424
|
if (options.json) outputJsonError("Either name or id must be provided");
|
|
1400
|
-
logger$
|
|
1425
|
+
logger$3.error("Either name or id must be provided");
|
|
1401
1426
|
process.exit(1);
|
|
1402
1427
|
}
|
|
1403
1428
|
if (options.name && options.id) {
|
|
1404
1429
|
if (options.json) outputJsonError("Only one of name or id must be provided");
|
|
1405
|
-
logger$
|
|
1430
|
+
logger$3.error("Only one of name or id must be provided");
|
|
1406
1431
|
process.exit(1);
|
|
1407
1432
|
}
|
|
1408
1433
|
const client = new LaminarClient({
|
|
@@ -1419,12 +1444,12 @@ const handleDatasetsPull = async (outputPath, options) => {
|
|
|
1419
1444
|
path: outputPath,
|
|
1420
1445
|
count: result.length
|
|
1421
1446
|
});
|
|
1422
|
-
else logger$
|
|
1447
|
+
else logger$3.info(`Successfully pulled ${result.length} data points to ${outputPath}`);
|
|
1423
1448
|
} else if (options.json) outputJson(result);
|
|
1424
1449
|
else printToConsole(result, options.outputFormat ?? "json");
|
|
1425
1450
|
} catch (error) {
|
|
1426
1451
|
if (options.json) outputJsonError(error);
|
|
1427
|
-
logger$
|
|
1452
|
+
logger$3.error(`Failed to pull dataset: ${errorMessage(error)}`);
|
|
1428
1453
|
process.exit(1);
|
|
1429
1454
|
}
|
|
1430
1455
|
};
|
|
@@ -1441,23 +1466,23 @@ const handleDatasetsCreate = async (name, paths, options) => {
|
|
|
1441
1466
|
const data = await loadFromPaths(paths, options.recursive);
|
|
1442
1467
|
if (data.length === 0) {
|
|
1443
1468
|
if (options.json) outputJsonError("No data to push");
|
|
1444
|
-
logger$
|
|
1469
|
+
logger$3.error("No data to push. Skipping");
|
|
1445
1470
|
process.exit(1);
|
|
1446
1471
|
}
|
|
1447
|
-
logger$
|
|
1472
|
+
logger$3.info(`Pushing ${data.length} data points to dataset '${name}'...`);
|
|
1448
1473
|
await client.datasets.push({
|
|
1449
1474
|
points: data,
|
|
1450
1475
|
name,
|
|
1451
1476
|
batchSize: options.batchSize ?? DEFAULT_DATASET_PUSH_BATCH_SIZE,
|
|
1452
1477
|
createDataset: true
|
|
1453
1478
|
});
|
|
1454
|
-
logger$
|
|
1479
|
+
logger$3.info(`Successfully pushed ${data.length} data points to dataset '${name}'`);
|
|
1455
1480
|
} catch (error) {
|
|
1456
1481
|
if (options.json) outputJsonError(error);
|
|
1457
|
-
logger$
|
|
1482
|
+
logger$3.error(`Failed to create dataset: ${errorMessage(error)}`);
|
|
1458
1483
|
process.exit(1);
|
|
1459
1484
|
}
|
|
1460
|
-
logger$
|
|
1485
|
+
logger$3.info(`Pulling data from dataset '${name}'...`);
|
|
1461
1486
|
try {
|
|
1462
1487
|
const result = await pullAllData(client, { name }, options.batchSize ?? DEFAULT_DATASET_PULL_BATCH_SIZE, 0, void 0);
|
|
1463
1488
|
await writeToFile(options.outputFile, result, options.outputFormat);
|
|
@@ -1466,1058 +1491,129 @@ const handleDatasetsCreate = async (name, paths, options) => {
|
|
|
1466
1491
|
path: options.outputFile,
|
|
1467
1492
|
count: result.length
|
|
1468
1493
|
});
|
|
1469
|
-
else logger$
|
|
1494
|
+
else logger$3.info(`Successfully created dataset '${name}' and saved ${result.length} datapoints to ${options.outputFile}`);
|
|
1470
1495
|
} catch (error) {
|
|
1471
1496
|
if (options.json) outputJsonError(error);
|
|
1472
|
-
logger$
|
|
1497
|
+
logger$3.error("Failed to pull dataset after creation: " + errorMessage(error));
|
|
1473
1498
|
process.exit(1);
|
|
1474
1499
|
}
|
|
1475
1500
|
};
|
|
1476
1501
|
//#endregion
|
|
1477
|
-
//#region src/
|
|
1478
|
-
const
|
|
1502
|
+
//#region src/utils/trace-note.ts
|
|
1503
|
+
const NOTE_METADATA_KEY = "rollout.note";
|
|
1479
1504
|
/**
|
|
1480
|
-
*
|
|
1505
|
+
* Normalize a user-supplied trace id (UUID or 32-char OTel hex, optionally
|
|
1506
|
+
* 0x-prefixed) to the dashed UUID form used by the SQL endpoint. Throws on
|
|
1507
|
+
* anything else so a typo fails loudly instead of querying nothing.
|
|
1481
1508
|
*/
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
server.close(() => resolve(port));
|
|
1488
|
-
});
|
|
1489
|
-
server.on("error", (err) => {
|
|
1490
|
-
if (err.code === "EADDRINUSE") resolve(findAvailablePort(startPort + 1));
|
|
1491
|
-
else reject(err);
|
|
1492
|
-
});
|
|
1493
|
-
});
|
|
1494
|
-
}
|
|
1495
|
-
/**
|
|
1496
|
-
* Parses request body as JSON
|
|
1497
|
-
*/
|
|
1498
|
-
function parseBody(req) {
|
|
1499
|
-
return new Promise((resolve, reject) => {
|
|
1500
|
-
let body = "";
|
|
1501
|
-
req.on("data", (chunk) => {
|
|
1502
|
-
body += chunk.toString();
|
|
1503
|
-
});
|
|
1504
|
-
req.on("end", () => {
|
|
1505
|
-
try {
|
|
1506
|
-
resolve(body ? JSON.parse(body) : {});
|
|
1507
|
-
} catch (err) {
|
|
1508
|
-
reject(/* @__PURE__ */ new Error(`Invalid JSON: ${err instanceof Error ? err.message : String(err)}`));
|
|
1509
|
-
}
|
|
1510
|
-
});
|
|
1511
|
-
req.on("error", reject);
|
|
1512
|
-
});
|
|
1513
|
-
}
|
|
1514
|
-
/**
|
|
1515
|
-
* Starts a local cache server for storing and retrieving cached LLM responses
|
|
1516
|
-
* during rollout debugging sessions.
|
|
1517
|
-
*
|
|
1518
|
-
* @param startPort - Optional starting port number (defaults to 35667)
|
|
1519
|
-
* @returns Server information including port, server instance, cache, and metadata setter
|
|
1520
|
-
*/
|
|
1521
|
-
async function startCacheServer(startPort = DEFAULT_START_PORT) {
|
|
1522
|
-
const cache = /* @__PURE__ */ new Map();
|
|
1523
|
-
let metadata = {
|
|
1524
|
-
pathToCount: {},
|
|
1525
|
-
overrides: void 0
|
|
1526
|
-
};
|
|
1527
|
-
const server = http.createServer((req, res) => {
|
|
1528
|
-
(async () => {
|
|
1529
|
-
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
1530
|
-
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
1531
|
-
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
1532
|
-
if (req.method === "OPTIONS") {
|
|
1533
|
-
res.writeHead(200);
|
|
1534
|
-
res.end();
|
|
1535
|
-
return;
|
|
1536
|
-
}
|
|
1537
|
-
if (req.method === "GET" && req.url === "/health") {
|
|
1538
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1539
|
-
res.end(JSON.stringify({ status: "ok" }));
|
|
1540
|
-
return;
|
|
1541
|
-
}
|
|
1542
|
-
if (req.method === "POST" && req.url === "/cached") {
|
|
1543
|
-
try {
|
|
1544
|
-
const { path, index } = await parseBody(req);
|
|
1545
|
-
if (typeof path !== "string" || typeof index !== "number") {
|
|
1546
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1547
|
-
res.end(JSON.stringify({ error: "Invalid request: path (string) and index (number) required" }));
|
|
1548
|
-
return;
|
|
1549
|
-
}
|
|
1550
|
-
const cacheKey = `${index}:${path}`;
|
|
1551
|
-
const response = {
|
|
1552
|
-
span: cache.get(cacheKey),
|
|
1553
|
-
pathToCount: metadata.pathToCount,
|
|
1554
|
-
overrides: metadata.overrides
|
|
1555
|
-
};
|
|
1556
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1557
|
-
res.end(JSON.stringify(response));
|
|
1558
|
-
} catch (err) {
|
|
1559
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1560
|
-
res.end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }));
|
|
1561
|
-
}
|
|
1562
|
-
return;
|
|
1563
|
-
}
|
|
1564
|
-
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1565
|
-
res.end(JSON.stringify({ error: "Not found" }));
|
|
1566
|
-
})().catch((error) => {
|
|
1567
|
-
if (!res.headersSent) {
|
|
1568
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1569
|
-
res.end(JSON.stringify({ error: error instanceof Error ? error.message : "Internal server error" }));
|
|
1570
|
-
}
|
|
1571
|
-
});
|
|
1572
|
-
});
|
|
1573
|
-
const port = await findAvailablePort(startPort);
|
|
1574
|
-
return new Promise((resolve, reject) => {
|
|
1575
|
-
server.listen(port, () => {
|
|
1576
|
-
resolve({
|
|
1577
|
-
port,
|
|
1578
|
-
server,
|
|
1579
|
-
cache,
|
|
1580
|
-
setMetadata: (newMetadata) => {
|
|
1581
|
-
metadata = newMetadata;
|
|
1582
|
-
}
|
|
1583
|
-
});
|
|
1584
|
-
});
|
|
1585
|
-
server.on("error", reject);
|
|
1586
|
-
});
|
|
1587
|
-
}
|
|
1588
|
-
//#endregion
|
|
1589
|
-
//#region src/sse-client.ts
|
|
1590
|
-
const HEARTBEAT_INTERVAL = 5e3;
|
|
1591
|
-
const MAX_MISSED_HEARTBEATS = 3;
|
|
1592
|
-
/**
|
|
1593
|
-
* SSE client for rollout debugging sessions
|
|
1594
|
-
* Connects to the Laminar backend and listens for run events
|
|
1595
|
-
*/
|
|
1596
|
-
var SSEClient = class extends events.EventEmitter {
|
|
1597
|
-
constructor(options) {
|
|
1598
|
-
super();
|
|
1599
|
-
this.lastHeartbeat = Date.now();
|
|
1600
|
-
this.isShutdown = false;
|
|
1601
|
-
this.client = options.client;
|
|
1602
|
-
this.sessionId = options.sessionId;
|
|
1603
|
-
this.params = options.params;
|
|
1604
|
-
this.name = options.name;
|
|
1605
|
-
}
|
|
1606
|
-
/**
|
|
1607
|
-
* Connects to the SSE endpoint
|
|
1608
|
-
*/
|
|
1609
|
-
async connectAndListen() {
|
|
1610
|
-
if (this.isShutdown) return;
|
|
1611
|
-
this.abortController = new AbortController();
|
|
1612
|
-
this.lastHeartbeat = Date.now();
|
|
1613
|
-
try {
|
|
1614
|
-
const response = await this.client.rolloutSessions.connect({
|
|
1615
|
-
sessionId: this.sessionId,
|
|
1616
|
-
params: this.params,
|
|
1617
|
-
signal: this.abortController.signal,
|
|
1618
|
-
name: this.name
|
|
1619
|
-
});
|
|
1620
|
-
this.emit("connected");
|
|
1621
|
-
this.startHeartbeatCheck();
|
|
1622
|
-
await this.parseSSEStream(response.body);
|
|
1623
|
-
} catch (error) {
|
|
1624
|
-
if (error.name === "AbortError") return;
|
|
1625
|
-
this.emit("error", error);
|
|
1626
|
-
if (!this.isShutdown) this.scheduleReconnect();
|
|
1627
|
-
}
|
|
1628
|
-
}
|
|
1629
|
-
/**
|
|
1630
|
-
* Parses SSE stream and emits events
|
|
1631
|
-
*/
|
|
1632
|
-
async parseSSEStream(body) {
|
|
1633
|
-
const reader = body.getReader();
|
|
1634
|
-
const decoder = new TextDecoder();
|
|
1635
|
-
const parser = (0, eventsource_parser.createParser)({ onEvent: (event) => {
|
|
1636
|
-
this.processSSEEvent(event);
|
|
1637
|
-
} });
|
|
1638
|
-
try {
|
|
1639
|
-
while (true) {
|
|
1640
|
-
const { done, value } = await reader.read();
|
|
1641
|
-
if (done) break;
|
|
1642
|
-
const chunk = decoder.decode(value, { stream: true });
|
|
1643
|
-
parser.feed(chunk);
|
|
1644
|
-
}
|
|
1645
|
-
} finally {
|
|
1646
|
-
reader.releaseLock();
|
|
1647
|
-
}
|
|
1648
|
-
if (!this.isShutdown) this.scheduleReconnect();
|
|
1649
|
-
}
|
|
1650
|
-
/**
|
|
1651
|
-
* Processes a parsed SSE event
|
|
1652
|
-
*/
|
|
1653
|
-
processSSEEvent(event) {
|
|
1654
|
-
if (!event.data) return;
|
|
1655
|
-
try {
|
|
1656
|
-
if (event.event === "heartbeat") {
|
|
1657
|
-
this.lastHeartbeat = Date.now();
|
|
1658
|
-
this.emit("heartbeat");
|
|
1659
|
-
} else if (event.event === "run") {
|
|
1660
|
-
const runEvent = {
|
|
1661
|
-
event_type: "run",
|
|
1662
|
-
data: JSON.parse(event.data)
|
|
1663
|
-
};
|
|
1664
|
-
this.emit("run", runEvent);
|
|
1665
|
-
} else if (event.event === "handshake") {
|
|
1666
|
-
const handshakeEvent = {
|
|
1667
|
-
event_type: "handshake",
|
|
1668
|
-
data: JSON.parse(event.data)
|
|
1669
|
-
};
|
|
1670
|
-
this.emit("handshake", handshakeEvent);
|
|
1671
|
-
} else if (event.event === "stop") this.emit("stop");
|
|
1672
|
-
} catch (error) {
|
|
1673
|
-
this.emit("error", /* @__PURE__ */ new Error(`Failed to parse SSE event data: ${error}`));
|
|
1674
|
-
}
|
|
1675
|
-
}
|
|
1676
|
-
/**
|
|
1677
|
-
* Starts checking for missed heartbeats
|
|
1678
|
-
*/
|
|
1679
|
-
startHeartbeatCheck() {
|
|
1680
|
-
this.stopHeartbeatCheck();
|
|
1681
|
-
this.heartbeatCheckTimer = setInterval(() => {
|
|
1682
|
-
if (Date.now() - this.lastHeartbeat > HEARTBEAT_INTERVAL * MAX_MISSED_HEARTBEATS) {
|
|
1683
|
-
this.emit("heartbeat_timeout");
|
|
1684
|
-
this.reconnect();
|
|
1685
|
-
}
|
|
1686
|
-
}, HEARTBEAT_INTERVAL);
|
|
1687
|
-
}
|
|
1688
|
-
/**
|
|
1689
|
-
* Stops heartbeat checking
|
|
1690
|
-
*/
|
|
1691
|
-
stopHeartbeatCheck() {
|
|
1692
|
-
if (this.heartbeatCheckTimer) {
|
|
1693
|
-
clearInterval(this.heartbeatCheckTimer);
|
|
1694
|
-
this.heartbeatCheckTimer = void 0;
|
|
1695
|
-
}
|
|
1696
|
-
}
|
|
1697
|
-
/**
|
|
1698
|
-
* Schedules a reconnection attempt
|
|
1699
|
-
*/
|
|
1700
|
-
scheduleReconnect() {
|
|
1701
|
-
if (this.reconnectTimer || this.isShutdown) return;
|
|
1702
|
-
this.emit("reconnecting");
|
|
1703
|
-
this.reconnectTimer = setTimeout(() => {
|
|
1704
|
-
this.reconnectTimer = void 0;
|
|
1705
|
-
this.reconnect();
|
|
1706
|
-
}, 1e3);
|
|
1707
|
-
}
|
|
1708
|
-
/**
|
|
1709
|
-
* Reconnects to the SSE endpoint
|
|
1710
|
-
*/
|
|
1711
|
-
reconnect() {
|
|
1712
|
-
this.disconnect(true);
|
|
1713
|
-
this.connectAndListen().catch((error) => {
|
|
1714
|
-
this.emit("error", error);
|
|
1715
|
-
});
|
|
1716
|
-
}
|
|
1717
|
-
/**
|
|
1718
|
-
* Disconnects from the SSE endpoint
|
|
1719
|
-
*/
|
|
1720
|
-
disconnect(stopReconnect = true) {
|
|
1721
|
-
if (this.abortController) {
|
|
1722
|
-
this.abortController.abort();
|
|
1723
|
-
this.abortController = void 0;
|
|
1724
|
-
}
|
|
1725
|
-
this.stopHeartbeatCheck();
|
|
1726
|
-
if (stopReconnect && this.reconnectTimer) {
|
|
1727
|
-
clearTimeout(this.reconnectTimer);
|
|
1728
|
-
this.reconnectTimer = void 0;
|
|
1729
|
-
}
|
|
1730
|
-
}
|
|
1731
|
-
/**
|
|
1732
|
-
* Updates the function metadata (params, name) and reconnects
|
|
1733
|
-
*/
|
|
1734
|
-
updateMetadata(params, name) {
|
|
1735
|
-
this.params = params;
|
|
1736
|
-
this.name = name;
|
|
1737
|
-
this.reconnect();
|
|
1738
|
-
}
|
|
1739
|
-
/**
|
|
1740
|
-
* Shuts down the SSE client gracefully
|
|
1741
|
-
*/
|
|
1742
|
-
shutdown() {
|
|
1743
|
-
this.isShutdown = true;
|
|
1744
|
-
this.disconnect(true);
|
|
1745
|
-
this.emit("shutdown");
|
|
1746
|
-
this.removeAllListeners();
|
|
1747
|
-
}
|
|
1509
|
+
const normalizeTraceId = (traceId) => {
|
|
1510
|
+
const id = traceId.trim().toLowerCase().replace(/^0x/, "");
|
|
1511
|
+
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(id)) return id;
|
|
1512
|
+
if (/^[0-9a-f]{32}$/.test(id)) 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");
|
|
1513
|
+
throw new Error(`Invalid trace id "${traceId}". Expected a UUID or a 32-char OTel hex trace id.`);
|
|
1748
1514
|
};
|
|
1749
1515
|
/**
|
|
1750
|
-
*
|
|
1751
|
-
*
|
|
1516
|
+
* Extract the note from a trace's `metadata` column as returned by the SQL
|
|
1517
|
+
* endpoint (a JSON string; tolerate an already-parsed object too). Missing /
|
|
1518
|
+
* malformed metadata reads as "no note".
|
|
1752
1519
|
*/
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
//#region src/subprocess/executor.ts
|
|
1758
|
-
const logger$3 = initializeLogger();
|
|
1759
|
-
/**
|
|
1760
|
-
* Track and kill the currently running subprocess
|
|
1761
|
-
*/
|
|
1762
|
-
var SubprocessManager = class {
|
|
1763
|
-
constructor() {
|
|
1764
|
-
this.currentProcess = null;
|
|
1765
|
-
}
|
|
1766
|
-
/**
|
|
1767
|
-
* Execute a subprocess and track it
|
|
1768
|
-
*/
|
|
1769
|
-
async execute(options) {
|
|
1770
|
-
const { command, args, config } = options;
|
|
1771
|
-
const child = (0, child_process.spawn)(command, args, { stdio: [
|
|
1772
|
-
"pipe",
|
|
1773
|
-
"pipe",
|
|
1774
|
-
"pipe"
|
|
1775
|
-
] });
|
|
1776
|
-
this.currentProcess = child;
|
|
1777
|
-
return new Promise((resolve, reject) => {
|
|
1778
|
-
const result = void 0;
|
|
1779
|
-
let hasError = false;
|
|
1780
|
-
readline.createInterface({
|
|
1781
|
-
input: child.stdout,
|
|
1782
|
-
crlfDelay: Infinity
|
|
1783
|
-
}).on("line", (line) => {
|
|
1784
|
-
if (line.startsWith("__LMNR_WORKER__:")) try {
|
|
1785
|
-
const messageJson = line.substring(16);
|
|
1786
|
-
const message = JSON.parse(messageJson);
|
|
1787
|
-
switch (message.type) {
|
|
1788
|
-
case "log":
|
|
1789
|
-
logger$3[message.level](message.message);
|
|
1790
|
-
break;
|
|
1791
|
-
case "error":
|
|
1792
|
-
hasError = true;
|
|
1793
|
-
logger$3.error(`Worker error: ${message.error}`);
|
|
1794
|
-
if (message.stack) logger$3.error(message.stack);
|
|
1795
|
-
break;
|
|
1796
|
-
}
|
|
1797
|
-
} catch {
|
|
1798
|
-
logger$3.debug("Failed to parse worker protocol message. Printing raw line");
|
|
1799
|
-
console.log(line.substring(16));
|
|
1800
|
-
}
|
|
1801
|
-
else console.log(line);
|
|
1802
|
-
});
|
|
1803
|
-
child.stderr.on("data", (data) => {
|
|
1804
|
-
process.stderr.write(data);
|
|
1805
|
-
});
|
|
1806
|
-
child.on("exit", (code, signal) => {
|
|
1807
|
-
if (this.currentProcess?.pid === child.pid) this.currentProcess = null;
|
|
1808
|
-
if (signal) reject(/* @__PURE__ */ new Error(`Worker terminated by signal: ${signal}`));
|
|
1809
|
-
else if (code === 0) resolve(result);
|
|
1810
|
-
else {
|
|
1811
|
-
if (!hasError) logger$3.error(`Worker exited with code ${code}`);
|
|
1812
|
-
reject(/* @__PURE__ */ new Error(`Worker exited with code ${code}`));
|
|
1813
|
-
}
|
|
1814
|
-
});
|
|
1815
|
-
child.on("error", (error) => {
|
|
1816
|
-
this.currentProcess = null;
|
|
1817
|
-
reject(/* @__PURE__ */ new Error(`Failed to spawn worker: ${error.message}`));
|
|
1818
|
-
});
|
|
1819
|
-
child.stdin?.write(JSON.stringify(config) + "\n");
|
|
1820
|
-
child.stdin?.end();
|
|
1821
|
-
});
|
|
1822
|
-
}
|
|
1823
|
-
/**
|
|
1824
|
-
* Kill the currently running subprocess
|
|
1825
|
-
* @returns true if a process was killed, false if no process was running
|
|
1826
|
-
*/
|
|
1827
|
-
kill() {
|
|
1828
|
-
if (this.currentProcess) {
|
|
1829
|
-
const processToKill = this.currentProcess;
|
|
1830
|
-
this.currentProcess.kill("SIGTERM");
|
|
1831
|
-
setTimeout(() => {
|
|
1832
|
-
if (processToKill && processToKill.exitCode === null) {
|
|
1833
|
-
logger$3.warn("Child process did not terminate, using SIGKILL");
|
|
1834
|
-
processToKill.kill("SIGKILL");
|
|
1835
|
-
}
|
|
1836
|
-
}, 5e3);
|
|
1837
|
-
return true;
|
|
1838
|
-
}
|
|
1839
|
-
return false;
|
|
1840
|
-
}
|
|
1841
|
-
/**
|
|
1842
|
-
* Check if a subprocess is currently running
|
|
1843
|
-
*/
|
|
1844
|
-
isRunning() {
|
|
1845
|
-
return this.currentProcess !== null;
|
|
1846
|
-
}
|
|
1847
|
-
};
|
|
1848
|
-
//#endregion
|
|
1849
|
-
//#region src/worker-registry.ts
|
|
1850
|
-
/**
|
|
1851
|
-
* Default workers mapped by file extension
|
|
1852
|
-
*/
|
|
1853
|
-
const DEFAULT_WORKERS = {
|
|
1854
|
-
".ts": {
|
|
1855
|
-
command: "node",
|
|
1856
|
-
args: []
|
|
1857
|
-
},
|
|
1858
|
-
".cts": {
|
|
1859
|
-
command: "node",
|
|
1860
|
-
args: []
|
|
1861
|
-
},
|
|
1862
|
-
".mts": {
|
|
1863
|
-
command: "node",
|
|
1864
|
-
args: []
|
|
1865
|
-
},
|
|
1866
|
-
".tsx": {
|
|
1867
|
-
command: "node",
|
|
1868
|
-
args: []
|
|
1869
|
-
},
|
|
1870
|
-
".jsx": {
|
|
1871
|
-
command: "node",
|
|
1872
|
-
args: []
|
|
1873
|
-
},
|
|
1874
|
-
".js": {
|
|
1875
|
-
command: "node",
|
|
1876
|
-
args: []
|
|
1877
|
-
},
|
|
1878
|
-
".mjs": {
|
|
1879
|
-
command: "node",
|
|
1880
|
-
args: []
|
|
1881
|
-
},
|
|
1882
|
-
".cjs": {
|
|
1883
|
-
command: "node",
|
|
1884
|
-
args: []
|
|
1885
|
-
},
|
|
1886
|
-
".py": {
|
|
1887
|
-
command: "python3",
|
|
1888
|
-
args: ["-m", "lmnr.cli.worker"]
|
|
1889
|
-
}
|
|
1890
|
-
};
|
|
1891
|
-
/**
|
|
1892
|
-
* Get the worker command for a given file path or module.
|
|
1893
|
-
* Resolves the TypeScript worker dynamically from @lmnr-ai/lmnr package.
|
|
1894
|
-
*/
|
|
1895
|
-
function getWorkerCommand(filePath, options) {
|
|
1896
|
-
if (options?.pythonModule) return {
|
|
1897
|
-
command: "python3",
|
|
1898
|
-
args: ["-m", "lmnr.cli.worker"]
|
|
1899
|
-
};
|
|
1900
|
-
if (!filePath) throw new Error("Either filePath or pythonModule must be provided");
|
|
1901
|
-
const ext = path.extname(filePath);
|
|
1902
|
-
if (!DEFAULT_WORKERS[ext]) throw new Error(`Unsupported file extension: ${ext}. Supported extensions: ${Object.keys(DEFAULT_WORKERS).join(", ")}`);
|
|
1903
|
-
const worker = DEFAULT_WORKERS[ext];
|
|
1904
|
-
if ([
|
|
1905
|
-
".ts",
|
|
1906
|
-
".tsx",
|
|
1907
|
-
".js",
|
|
1908
|
-
".mjs",
|
|
1909
|
-
".cjs",
|
|
1910
|
-
".mts",
|
|
1911
|
-
".cts",
|
|
1912
|
-
".jsx"
|
|
1913
|
-
].includes(ext)) try {
|
|
1914
|
-
const workerPath = require.resolve("@lmnr-ai/lmnr/dist/cli/worker/index.cjs");
|
|
1915
|
-
return {
|
|
1916
|
-
command: worker.command,
|
|
1917
|
-
args: [workerPath]
|
|
1918
|
-
};
|
|
1919
|
-
} catch (error) {
|
|
1920
|
-
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)}`);
|
|
1921
|
-
}
|
|
1922
|
-
return worker;
|
|
1923
|
-
}
|
|
1924
|
-
//#endregion
|
|
1925
|
-
//#region src/commands/dev/metadata.ts
|
|
1926
|
-
const logger$2 = initializeLogger();
|
|
1927
|
-
const TS_JS_EXTENSIONS = [
|
|
1928
|
-
".ts",
|
|
1929
|
-
".tsx",
|
|
1930
|
-
".js",
|
|
1931
|
-
".mjs",
|
|
1932
|
-
".cjs",
|
|
1933
|
-
".jsx",
|
|
1934
|
-
".mts",
|
|
1935
|
-
".cts"
|
|
1936
|
-
];
|
|
1937
|
-
const EXTENSIONS_TO_DISCOVER_METADATA = [...TS_JS_EXTENSIONS, ".py"];
|
|
1938
|
-
/**
|
|
1939
|
-
* Protocol prefix for metadata discovery responses
|
|
1940
|
-
* This allows us to safely parse JSON even if there are other log statements in stdout
|
|
1941
|
-
*/
|
|
1942
|
-
const METADATA_PROTOCOL_PREFIX = "LMNR_METADATA:";
|
|
1943
|
-
const logLmnrPackageNotFoundAndExit = () => {
|
|
1944
|
-
logger$2.error("@lmnr-ai/lmnr package not found or outdated. For JS/TS projects, please install the latest version of @lmnr-ai/lmnr in your project: npm install @lmnr-ai/lmnr\nYou might need to run `lmnr-cli` from the root of your project");
|
|
1945
|
-
process.exit(1);
|
|
1946
|
-
};
|
|
1947
|
-
/**
|
|
1948
|
-
* Discovers function metadata for TypeScript and JavaScript files by:
|
|
1949
|
-
* 1. Extracting TypeScript metadata (params with types from source - TS only)
|
|
1950
|
-
* 2. Building and loading the module with esbuild
|
|
1951
|
-
* 3. Selecting the appropriate function
|
|
1952
|
-
* 4. Matching metadata by span name
|
|
1953
|
-
*
|
|
1954
|
-
* For JavaScript files, TypeScript metadata extraction fails gracefully, but runtime
|
|
1955
|
-
* parameter extraction via regex still works (param names without types).
|
|
1956
|
-
*/
|
|
1957
|
-
const discoverTypeScriptMetadata = async (filePath, options) => {
|
|
1958
|
-
let extractRolloutFunctions;
|
|
1959
|
-
let buildFile;
|
|
1960
|
-
let loadModule;
|
|
1961
|
-
let selectRolloutFunction;
|
|
1962
|
-
try {
|
|
1963
|
-
const lmnrPackage = "@lmnr-ai/lmnr";
|
|
1964
|
-
const tsParserPath = require.resolve(`${lmnrPackage}/dist/cli/worker/ts-parser.cjs`);
|
|
1965
|
-
const buildModulePath = require.resolve(`${lmnrPackage}/dist/cli/worker/build.cjs`);
|
|
1966
|
-
delete require.cache[tsParserPath];
|
|
1967
|
-
delete require.cache[buildModulePath];
|
|
1968
|
-
extractRolloutFunctions = require(tsParserPath).extractRolloutFunctions;
|
|
1969
|
-
const buildModule = require(buildModulePath);
|
|
1970
|
-
buildFile = buildModule.buildFile;
|
|
1971
|
-
loadModule = buildModule.loadModule;
|
|
1972
|
-
selectRolloutFunction = buildModule.selectRolloutFunction;
|
|
1973
|
-
if (!extractRolloutFunctions || !buildFile || !loadModule || !selectRolloutFunction) {
|
|
1974
|
-
logger$2.error("Missing exports from @lmnr-ai/lmnr modules. This may indicate an outdated package version.");
|
|
1975
|
-
logLmnrPackageNotFoundAndExit();
|
|
1976
|
-
}
|
|
1977
|
-
} catch (error) {
|
|
1978
|
-
if (error.code === "MODULE_NOT_FOUND") logLmnrPackageNotFoundAndExit();
|
|
1979
|
-
logger$2.error(`Unexpected error loading @lmnr-ai/lmnr modules: ${error.message}`);
|
|
1980
|
-
throw error;
|
|
1981
|
-
}
|
|
1982
|
-
let paramsMetadata;
|
|
1983
|
-
try {
|
|
1984
|
-
paramsMetadata = extractRolloutFunctions(filePath);
|
|
1985
|
-
logger$2.debug(`Extracted TypeScript metadata for ${paramsMetadata?.size} functions`);
|
|
1986
|
-
} catch (error) {
|
|
1987
|
-
logger$2.warn("Failed to extract TypeScript metadata, falling back to runtime parsing: " + (error instanceof Error ? error.message : String(error)));
|
|
1988
|
-
}
|
|
1989
|
-
const moduleText = await buildFile(filePath, {
|
|
1990
|
-
externalPackages: options.externalPackages,
|
|
1991
|
-
dynamicImportsToSkip: options.dynamicImportsToSkip
|
|
1992
|
-
});
|
|
1993
|
-
loadModule({
|
|
1994
|
-
filename: filePath,
|
|
1995
|
-
moduleText
|
|
1996
|
-
});
|
|
1997
|
-
const selectedFunction = selectRolloutFunction(options.function);
|
|
1998
|
-
if (paramsMetadata) {
|
|
1999
|
-
logger$2.debug(`Available TS metadata keys: ${Array.from(paramsMetadata.keys()).join(", ")}`);
|
|
2000
|
-
logger$2.debug(`Looking for span name: ${selectedFunction.name} (runtime key: ${selectedFunction.exportName})`);
|
|
2001
|
-
let foundMetadata = null;
|
|
2002
|
-
for (const [exportName, metadata] of paramsMetadata.entries()) {
|
|
2003
|
-
logger$2.debug(`Checking ${exportName}: span name = ${metadata.name}, export name = ${exportName}`);
|
|
2004
|
-
if (metadata.name === selectedFunction.name) {
|
|
2005
|
-
foundMetadata = metadata;
|
|
2006
|
-
logger$2.debug(`Match. Export name: ${exportName}, span name: ${metadata.name}`);
|
|
2007
|
-
break;
|
|
2008
|
-
}
|
|
2009
|
-
}
|
|
2010
|
-
if (foundMetadata) {
|
|
2011
|
-
selectedFunction.params = foundMetadata.params;
|
|
2012
|
-
logger$2.debug(`Using TypeScript metadata for span: ${selectedFunction.name}`);
|
|
2013
|
-
} else logger$2.info(`No TypeScript metadata found for span name: ${selectedFunction.name}`);
|
|
2014
|
-
}
|
|
2015
|
-
return {
|
|
2016
|
-
functionName: selectedFunction.name,
|
|
2017
|
-
params: selectedFunction.params || []
|
|
2018
|
-
};
|
|
2019
|
-
};
|
|
2020
|
-
/**
|
|
2021
|
-
* Helper to execute subprocess commands
|
|
2022
|
-
*/
|
|
2023
|
-
const execCommand = async (command, args) => new Promise((resolve, reject) => {
|
|
2024
|
-
const { spawn } = require("child_process");
|
|
2025
|
-
const child = spawn(command, args);
|
|
2026
|
-
let stdout = "";
|
|
2027
|
-
let stderr = "";
|
|
2028
|
-
child.stdout.on("data", (data) => {
|
|
2029
|
-
stdout += data.toString();
|
|
2030
|
-
});
|
|
2031
|
-
child.stderr.on("data", (data) => {
|
|
2032
|
-
stderr += data.toString();
|
|
2033
|
-
});
|
|
2034
|
-
child.on("close", (code) => {
|
|
2035
|
-
if (code === 0) resolve({
|
|
2036
|
-
stdout,
|
|
2037
|
-
stderr
|
|
2038
|
-
});
|
|
2039
|
-
else reject(/* @__PURE__ */ new Error(`Command failed with code ${code}: ${stderr}`));
|
|
2040
|
-
});
|
|
2041
|
-
child.on("error", (error) => {
|
|
2042
|
-
reject(error);
|
|
2043
|
-
});
|
|
2044
|
-
});
|
|
2045
|
-
/**
|
|
2046
|
-
* Extracts JSON metadata from stdout that may contain other log statements
|
|
2047
|
-
* Looks for lines matching the protocol prefix and parses the JSON payload
|
|
2048
|
-
*
|
|
2049
|
-
* @param stdout - Raw stdout output that may contain logs and metadata
|
|
2050
|
-
* @returns Parsed JSON object
|
|
2051
|
-
* @throws Error if no valid metadata line is found or JSON parsing fails
|
|
2052
|
-
*/
|
|
2053
|
-
const extractMetadataFromStdout = (stdout) => {
|
|
2054
|
-
const prefixPositions = [];
|
|
2055
|
-
let searchStart = 0;
|
|
2056
|
-
while (true) {
|
|
2057
|
-
const pos = stdout.indexOf(METADATA_PROTOCOL_PREFIX, searchStart);
|
|
2058
|
-
if (pos === -1) break;
|
|
2059
|
-
prefixPositions.push(pos);
|
|
2060
|
-
searchStart = pos + 14;
|
|
2061
|
-
}
|
|
2062
|
-
if (prefixPositions.length === 0) try {
|
|
2063
|
-
return JSON.parse(stdout.trim());
|
|
2064
|
-
} catch {
|
|
2065
|
-
throw new Error("No metadata found in output. Please make sure you are running the latest version of `lmnr` python package.");
|
|
2066
|
-
}
|
|
2067
|
-
let lastValidJson = null;
|
|
2068
|
-
for (const pos of prefixPositions) {
|
|
2069
|
-
const startPos = pos + 14;
|
|
2070
|
-
const jsonText = stdout.slice(startPos).trim();
|
|
2071
|
-
const nextNewline = stdout.indexOf("\n", startPos);
|
|
2072
|
-
if (nextNewline !== -1) {
|
|
2073
|
-
const lineText = stdout.slice(startPos, nextNewline).trim();
|
|
2074
|
-
try {
|
|
2075
|
-
lastValidJson = JSON.parse(lineText);
|
|
2076
|
-
continue;
|
|
2077
|
-
} catch {}
|
|
2078
|
-
}
|
|
1520
|
+
const readNoteFromMetadata = (metadata) => {
|
|
1521
|
+
let parsed = metadata;
|
|
1522
|
+
if (typeof metadata === "string") {
|
|
1523
|
+
if (metadata === "") return "";
|
|
2079
1524
|
try {
|
|
2080
|
-
|
|
2081
|
-
let inString = false;
|
|
2082
|
-
let escapeNext = false;
|
|
2083
|
-
let firstChar = -1;
|
|
2084
|
-
for (let i = 0; i < jsonText.length; i++) {
|
|
2085
|
-
const char = jsonText[i];
|
|
2086
|
-
if (escapeNext) {
|
|
2087
|
-
escapeNext = false;
|
|
2088
|
-
continue;
|
|
2089
|
-
}
|
|
2090
|
-
if (char === "\\" && inString) {
|
|
2091
|
-
escapeNext = true;
|
|
2092
|
-
continue;
|
|
2093
|
-
}
|
|
2094
|
-
if (char === "\"") {
|
|
2095
|
-
inString = !inString;
|
|
2096
|
-
continue;
|
|
2097
|
-
}
|
|
2098
|
-
if (inString) continue;
|
|
2099
|
-
if (char === "{" || char === "[") {
|
|
2100
|
-
if (firstChar === -1) firstChar = i;
|
|
2101
|
-
depth++;
|
|
2102
|
-
} else if (char === "}" || char === "]") {
|
|
2103
|
-
depth--;
|
|
2104
|
-
if (depth === 0 && firstChar !== -1) {
|
|
2105
|
-
const candidate = jsonText.slice(0, i + 1);
|
|
2106
|
-
lastValidJson = JSON.parse(candidate);
|
|
2107
|
-
break;
|
|
2108
|
-
}
|
|
2109
|
-
}
|
|
2110
|
-
}
|
|
2111
|
-
if (depth !== 0 || firstChar === -1) lastValidJson = JSON.parse(jsonText);
|
|
1525
|
+
parsed = JSON.parse(metadata);
|
|
2112
1526
|
} catch {
|
|
2113
|
-
|
|
1527
|
+
return "";
|
|
2114
1528
|
}
|
|
2115
1529
|
}
|
|
2116
|
-
if (
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
/**
|
|
2120
|
-
* Discovers function metadata for Python files/modules by calling the lmnr Python CLI
|
|
2121
|
-
*/
|
|
2122
|
-
const discoverPythonMetadata = async (filePathOrModule, options) => {
|
|
2123
|
-
logger$2.debug(`Discovering Python metadata for ${filePathOrModule}`);
|
|
2124
|
-
const args = ["discover"];
|
|
2125
|
-
if (options.pythonModule) args.push("--module", options.pythonModule);
|
|
2126
|
-
else args.push("--file", filePathOrModule);
|
|
2127
|
-
if (options.function) args.push("--function", options.function);
|
|
2128
|
-
try {
|
|
2129
|
-
const response = extractMetadataFromStdout((await execCommand("lmnr", args)).stdout);
|
|
2130
|
-
return {
|
|
2131
|
-
functionName: response.name,
|
|
2132
|
-
params: response.params || []
|
|
2133
|
-
};
|
|
2134
|
-
} catch (error) {
|
|
2135
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2136
|
-
logger$2.error(`Error while loading Python file/module: ${errorMessage}`);
|
|
2137
|
-
if (errorMessage.toLowerCase().includes("command not found") || errorMessage.includes("spawn lmnr ENOENT")) logger$2.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}"`);
|
|
2138
|
-
throw error;
|
|
2139
|
-
}
|
|
2140
|
-
};
|
|
2141
|
-
/**
|
|
2142
|
-
* Generic metadata discovery dispatcher that routes to language-specific implementations
|
|
2143
|
-
*/
|
|
2144
|
-
const discoverFunctionMetadata = async (filePathOrModule, options) => {
|
|
2145
|
-
if (options.pythonModule) return await discoverPythonMetadata(filePathOrModule, options);
|
|
2146
|
-
const ext = path.extname(filePathOrModule);
|
|
2147
|
-
if (TS_JS_EXTENSIONS.includes(ext)) return await discoverTypeScriptMetadata(filePathOrModule, options);
|
|
2148
|
-
if (ext === ".py") return await discoverPythonMetadata(filePathOrModule, options);
|
|
2149
|
-
logger$2.warn(`No metadata discovery available for ${ext} files`);
|
|
2150
|
-
return {
|
|
2151
|
-
functionName: options.function || path.basename(filePathOrModule, ext),
|
|
2152
|
-
params: []
|
|
2153
|
-
};
|
|
1530
|
+
if (typeof parsed !== "object" || parsed === null) return "";
|
|
1531
|
+
const note = parsed[NOTE_METADATA_KEY];
|
|
1532
|
+
return typeof note === "string" ? note : "";
|
|
2154
1533
|
};
|
|
2155
1534
|
//#endregion
|
|
2156
|
-
//#region src/commands/
|
|
2157
|
-
const logger$
|
|
2158
|
-
function newUUID() {
|
|
2159
|
-
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return crypto.randomUUID();
|
|
2160
|
-
return (0, uuid.v4)();
|
|
2161
|
-
}
|
|
2162
|
-
function getFrontendUrl(baseUrl, frontendPort) {
|
|
2163
|
-
let url = baseUrl ?? "https://api.lmnr.ai";
|
|
2164
|
-
if (url === "https://api.lmnr.ai") url = "https://www.laminar.sh";
|
|
2165
|
-
url = url.replace(/\/$/, "");
|
|
2166
|
-
if (/localhost|127\.0\.0\.1/.test(url)) {
|
|
2167
|
-
const port = frontendPort ?? url.match(/:\d{1,5}$/g)?.[0]?.slice(1) ?? 5667;
|
|
2168
|
-
url = url.replace(/:\d{1,5}$/g, "");
|
|
2169
|
-
return `${url}:${port}`;
|
|
2170
|
-
}
|
|
2171
|
-
return url;
|
|
2172
|
-
}
|
|
2173
|
-
/**
|
|
2174
|
-
* Parses request arguments, attempting JSON parse for strings
|
|
2175
|
-
*/
|
|
2176
|
-
const tryParseArg = (arg) => {
|
|
2177
|
-
if (typeof arg === "string") try {
|
|
2178
|
-
return JSON.parse(arg);
|
|
2179
|
-
} catch {
|
|
2180
|
-
return arg;
|
|
2181
|
-
}
|
|
2182
|
-
return arg;
|
|
2183
|
-
};
|
|
1535
|
+
//#region src/commands/debug/index.ts
|
|
1536
|
+
const logger$2 = initializeLogger();
|
|
2184
1537
|
/**
|
|
2185
|
-
*
|
|
1538
|
+
* Upsert the display name of a debug session. Update-only on the backend: a
|
|
1539
|
+
* session id unknown to the project 404s rather than creating a ghost session.
|
|
2186
1540
|
*/
|
|
2187
|
-
const
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
setMetadata({
|
|
2193
|
-
pathToCount: {},
|
|
2194
|
-
overrides
|
|
1541
|
+
const handleDebugSessionSetName = async (sessionId, name, options) => {
|
|
1542
|
+
const client = new LaminarClient({
|
|
1543
|
+
projectApiKey: options.projectApiKey,
|
|
1544
|
+
baseUrl: options.baseUrl,
|
|
1545
|
+
port: options.port
|
|
2195
1546
|
});
|
|
2196
1547
|
try {
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
if (paths.length === 0) logger$1.info("No spans to cache, starting fresh");
|
|
2201
|
-
else {
|
|
2202
|
-
const query = `
|
|
2203
|
-
SELECT name, input, output, attributes, path
|
|
2204
|
-
FROM spans
|
|
2205
|
-
WHERE trace_id = {traceId:UUID}
|
|
2206
|
-
AND path IN {paths:String[]}
|
|
2207
|
-
ORDER BY start_time ASC
|
|
2208
|
-
`;
|
|
2209
|
-
logger$1.debug(`Querying spans from trace ${trace_id}...`);
|
|
2210
|
-
const spans = await client.sql.query(query, {
|
|
2211
|
-
traceId: trace_id,
|
|
2212
|
-
paths
|
|
2213
|
-
});
|
|
2214
|
-
logger$1.debug(`Received ${spans.length} spans from backend`);
|
|
2215
|
-
const spansByPath = {};
|
|
2216
|
-
for (const span of spans) {
|
|
2217
|
-
const path$2 = span.path;
|
|
2218
|
-
if (!spansByPath[path$2]) spansByPath[path$2] = [];
|
|
2219
|
-
spansByPath[path$2].push(span);
|
|
2220
|
-
}
|
|
2221
|
-
for (const [path$3, pathSpans] of Object.entries(spansByPath)) {
|
|
2222
|
-
const maxCount = path_to_count?.[path$3] || 0;
|
|
2223
|
-
const spansToCache = pathSpans.slice(0, maxCount);
|
|
2224
|
-
spansToCache.forEach((span, index) => {
|
|
2225
|
-
let parsedInput;
|
|
2226
|
-
let parsedOutput;
|
|
2227
|
-
let parsedAttributes;
|
|
2228
|
-
try {
|
|
2229
|
-
parsedInput = typeof span.input === "string" ? JSON.parse(span.input) : span.input;
|
|
2230
|
-
} catch {
|
|
2231
|
-
parsedInput = span.input;
|
|
2232
|
-
}
|
|
2233
|
-
try {
|
|
2234
|
-
parsedOutput = typeof span.output === "string" ? span.output : JSON.stringify(span.output);
|
|
2235
|
-
} catch {
|
|
2236
|
-
parsedOutput = String(span.output);
|
|
2237
|
-
}
|
|
2238
|
-
try {
|
|
2239
|
-
parsedAttributes = typeof span.attributes === "string" ? JSON.parse(span.attributes) : span.attributes;
|
|
2240
|
-
} catch {
|
|
2241
|
-
parsedAttributes = {};
|
|
2242
|
-
}
|
|
2243
|
-
const cachedSpan = {
|
|
2244
|
-
name: span.name,
|
|
2245
|
-
input: parsedInput,
|
|
2246
|
-
output: parsedOutput,
|
|
2247
|
-
attributes: parsedAttributes
|
|
2248
|
-
};
|
|
2249
|
-
const cacheKey = `${index}:${path$3}`;
|
|
2250
|
-
cache.set(cacheKey, cachedSpan);
|
|
2251
|
-
});
|
|
2252
|
-
logger$1.info(`Cached ${spansToCache.length} spans for path: ${path$3}`);
|
|
2253
|
-
}
|
|
2254
|
-
setMetadata({
|
|
2255
|
-
pathToCount: path_to_count || {},
|
|
2256
|
-
overrides
|
|
2257
|
-
});
|
|
2258
|
-
}
|
|
2259
|
-
}
|
|
2260
|
-
const baseUrl = options.baseUrl ?? process.env.LMNR_BASE_URL ?? "https://api.lmnr.ai";
|
|
2261
|
-
const httpPort = options.port ?? (baseUrl.match(/:\d{1,5}$/g) ? parseInt(baseUrl.match(/:\d{1,5}$/g)[0].slice(1)) : 443);
|
|
2262
|
-
const grpcPort = options.grpcPort ?? 8443;
|
|
2263
|
-
const env = {
|
|
2264
|
-
LMNR_ROLLOUT_SESSION_ID: sessionId,
|
|
2265
|
-
LMNR_ROLLOUT_STATE_SERVER_ADDRESS: `http://localhost:${cacheServerPort}`
|
|
2266
|
-
};
|
|
2267
|
-
const workerConfig = {
|
|
2268
|
-
filePath: options.pythonModule ? void 0 : filePathOrModule,
|
|
2269
|
-
modulePath: options.pythonModule,
|
|
2270
|
-
functionName: options.function,
|
|
2271
|
-
args: parsedArgs,
|
|
2272
|
-
env,
|
|
2273
|
-
cacheServerPort,
|
|
2274
|
-
baseUrl,
|
|
2275
|
-
projectApiKey: options.projectApiKey,
|
|
2276
|
-
httpPort,
|
|
2277
|
-
grpcPort,
|
|
2278
|
-
externalPackages: options.externalPackages,
|
|
2279
|
-
dynamicImportsToSkip: options.dynamicImportsToSkip
|
|
2280
|
-
};
|
|
2281
|
-
const workerCommand = options.command ? {
|
|
2282
|
-
command: options.command,
|
|
2283
|
-
args: options.commandArgs ?? []
|
|
2284
|
-
} : getWorkerCommand(options.pythonModule ? void 0 : filePathOrModule, options);
|
|
2285
|
-
try {
|
|
2286
|
-
await client.rolloutSessions.setStatus({
|
|
2287
|
-
sessionId,
|
|
2288
|
-
status: "RUNNING"
|
|
2289
|
-
});
|
|
2290
|
-
} catch (error) {
|
|
2291
|
-
logger$1.error(`Error setting debugger session status: ${error instanceof Error ? error.message : error}`);
|
|
2292
|
-
}
|
|
2293
|
-
await subprocessManager.execute({
|
|
2294
|
-
command: workerCommand.command,
|
|
2295
|
-
args: workerCommand.args,
|
|
2296
|
-
config: workerConfig
|
|
1548
|
+
await client.rolloutSessions.setName({
|
|
1549
|
+
sessionId,
|
|
1550
|
+
name
|
|
2297
1551
|
});
|
|
2298
|
-
|
|
2299
|
-
|
|
1552
|
+
if (options.json) {
|
|
1553
|
+
outputJson({
|
|
2300
1554
|
sessionId,
|
|
2301
|
-
|
|
1555
|
+
name
|
|
2302
1556
|
});
|
|
2303
|
-
|
|
2304
|
-
logger$1.error(`Error setting debugger session status: ${error instanceof Error ? error.message : error}`);
|
|
1557
|
+
return;
|
|
2305
1558
|
}
|
|
1559
|
+
logger$2.info(`Set name of session ${sessionId} to "${name}".`);
|
|
2306
1560
|
} catch (error) {
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
await client.rolloutSessions.setStatus({
|
|
2311
|
-
sessionId,
|
|
2312
|
-
status: "FINISHED"
|
|
2313
|
-
});
|
|
2314
|
-
} catch (error) {
|
|
2315
|
-
logger$1.error(`Error setting debugger session status: ${error instanceof Error ? error.message : error}`);
|
|
2316
|
-
}
|
|
1561
|
+
if (options.json) outputJsonError(error);
|
|
1562
|
+
logger$2.error(`Failed to set session name: ${errorMessage(error)}`);
|
|
1563
|
+
process.exit(1);
|
|
2317
1564
|
}
|
|
2318
1565
|
};
|
|
1566
|
+
const SUMMARY_PAGE_SIZE = 1e3;
|
|
2319
1567
|
/**
|
|
2320
|
-
*
|
|
1568
|
+
* Print a per-trace summary of a debug session: every trace whose metadata
|
|
1569
|
+
* groups it to the session (`rollout.session_id`), oldest first, with the
|
|
1570
|
+
* agent-authored note (`rollout.note`) attached to each.
|
|
2321
1571
|
*/
|
|
2322
|
-
async
|
|
2323
|
-
const isPythonModule = !!options.pythonModule;
|
|
2324
|
-
const filePathOrModule = filePath || options.pythonModule;
|
|
2325
|
-
let didLogHandshake = false;
|
|
2326
|
-
const sessionId = newUUID();
|
|
1572
|
+
const handleDebugSessionSummary = async (sessionId, options) => {
|
|
2327
1573
|
const client = new LaminarClient({
|
|
2328
|
-
baseUrl: options.baseUrl,
|
|
2329
1574
|
projectApiKey: options.projectApiKey,
|
|
1575
|
+
baseUrl: options.baseUrl,
|
|
2330
1576
|
port: options.port
|
|
2331
1577
|
});
|
|
2332
|
-
logger$1.debug("Starting cache server...");
|
|
2333
|
-
const { port: cacheServerPort, server: cacheServer, cache, setMetadata } = await startCacheServer();
|
|
2334
|
-
logger$1.debug(`Cache server started on port ${cacheServerPort}`);
|
|
2335
|
-
const subprocessManager = new SubprocessManager();
|
|
2336
|
-
let functionName = options.function;
|
|
2337
|
-
let params = [];
|
|
2338
1578
|
try {
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
1579
|
+
const traces = [];
|
|
1580
|
+
let offset = 0;
|
|
1581
|
+
for (;;) {
|
|
1582
|
+
const rows = await client.sql.query("SELECT id, formatDateTime(end_time, '%Y-%m-%dT%H:%i:%S.%fZ') AS end_time, metadata FROM traces WHERE simpleJSONExtractString(metadata, 'rollout.session_id') = {session_id:String} ORDER BY start_time LIMIT {limit:UInt32} OFFSET {offset:UInt32}", {
|
|
1583
|
+
session_id: sessionId,
|
|
1584
|
+
limit: SUMMARY_PAGE_SIZE,
|
|
1585
|
+
offset
|
|
1586
|
+
});
|
|
1587
|
+
for (const row of rows) traces.push({
|
|
1588
|
+
note: readNoteFromMetadata(row.metadata),
|
|
1589
|
+
traceId: String(row.id ?? ""),
|
|
1590
|
+
endTime: String(row.end_time ?? "")
|
|
1591
|
+
});
|
|
1592
|
+
if (rows.length < SUMMARY_PAGE_SIZE) break;
|
|
1593
|
+
offset += SUMMARY_PAGE_SIZE;
|
|
2349
1594
|
}
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
throw error;
|
|
2354
|
-
}
|
|
2355
|
-
logger$1.debug("Setting up file watcher...");
|
|
2356
|
-
const watcher = chokidar.default.watch(".", {
|
|
2357
|
-
ignored: (path$4) => {
|
|
2358
|
-
const ignoredDirs = [
|
|
2359
|
-
"node_modules",
|
|
2360
|
-
".git",
|
|
2361
|
-
"dist",
|
|
2362
|
-
"build",
|
|
2363
|
-
".next",
|
|
2364
|
-
"coverage",
|
|
2365
|
-
".turbo",
|
|
2366
|
-
"tmp",
|
|
2367
|
-
"temp",
|
|
2368
|
-
"venv",
|
|
2369
|
-
".venv",
|
|
2370
|
-
"virtualenv",
|
|
2371
|
-
".virtualenv",
|
|
2372
|
-
"__pycache__",
|
|
2373
|
-
".pytest_cache",
|
|
2374
|
-
".ruff_cache",
|
|
2375
|
-
".mypy_cache",
|
|
2376
|
-
".cache",
|
|
2377
|
-
".DS_Store"
|
|
2378
|
-
];
|
|
2379
|
-
if (path$4.split(/[/\\]/).some((segment) => ignoredDirs.includes(segment))) return true;
|
|
2380
|
-
if (path$4.endsWith(".log") || path$4.endsWith(".map")) return true;
|
|
2381
|
-
return false;
|
|
2382
|
-
},
|
|
2383
|
-
persistent: true,
|
|
2384
|
-
ignoreInitial: true,
|
|
2385
|
-
awaitWriteFinish: {
|
|
2386
|
-
stabilityThreshold: 100,
|
|
2387
|
-
pollInterval: 100
|
|
1595
|
+
if (options.json) {
|
|
1596
|
+
outputJson(traces);
|
|
1597
|
+
return;
|
|
2388
1598
|
}
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
params,
|
|
2397
|
-
name: functionName ?? ""
|
|
2398
|
-
});
|
|
2399
|
-
let currentRunPromise = null;
|
|
2400
|
-
let stopRequested = false;
|
|
2401
|
-
sseClient.on("heartbeat", () => {
|
|
2402
|
-
logger$1.debug("Heartbeat received");
|
|
2403
|
-
});
|
|
2404
|
-
sseClient.on("run", (event) => {
|
|
2405
|
-
if (currentRunPromise !== null) {
|
|
2406
|
-
logger$1.warn("Already processing a run event, skipping new run");
|
|
2407
|
-
return;
|
|
2408
|
-
}
|
|
2409
|
-
currentRunPromise = (async () => {
|
|
2410
|
-
try {
|
|
2411
|
-
stopRequested = false;
|
|
2412
|
-
if (reloadScheduled) {
|
|
2413
|
-
logger$1.info("Reloading function metadata before run...");
|
|
2414
|
-
if (isPythonModule || filePath && EXTENSIONS_TO_DISCOVER_METADATA.includes(path.extname(filePath))) try {
|
|
2415
|
-
const metadata = await discoverFunctionMetadata(filePathOrModule, options);
|
|
2416
|
-
if (stopRequested) {
|
|
2417
|
-
logger$1.info("Run cancelled during metadata discovery");
|
|
2418
|
-
return;
|
|
2419
|
-
}
|
|
2420
|
-
logger$1.debug(`Updated function metadata: ${metadata.functionName}`);
|
|
2421
|
-
logger$1.debug(`Updated parameters: ${JSON.stringify(metadata.params, null, 2)}`);
|
|
2422
|
-
if (sseClient) {
|
|
2423
|
-
sseClient.updateMetadata(metadata.params, metadata.functionName);
|
|
2424
|
-
logger$1.debug("Notified backend of metadata changes");
|
|
2425
|
-
}
|
|
2426
|
-
reloadScheduled = false;
|
|
2427
|
-
} catch (error) {
|
|
2428
|
-
logger$1.error("Failed to update function metadata: " + (error instanceof Error ? error.message : String(error)));
|
|
2429
|
-
if (error instanceof Error && error.stack) logger$1.debug(`Stack trace: ${error.stack}`);
|
|
2430
|
-
return;
|
|
2431
|
-
}
|
|
2432
|
-
else reloadScheduled = false;
|
|
2433
|
-
}
|
|
2434
|
-
if (stopRequested) {
|
|
2435
|
-
logger$1.info("Run cancelled before execution");
|
|
2436
|
-
return;
|
|
2437
|
-
}
|
|
2438
|
-
await handleRunEvent(event, sessionId, filePathOrModule, client, cacheServerPort, cache, setMetadata, options, subprocessManager);
|
|
2439
|
-
} catch (error) {
|
|
2440
|
-
logger$1.error("Unhandled error in run event handler: " + (error instanceof Error ? error.message : String(error)));
|
|
2441
|
-
} finally {
|
|
2442
|
-
currentRunPromise = null;
|
|
2443
|
-
}
|
|
2444
|
-
})();
|
|
2445
|
-
});
|
|
2446
|
-
sseClient.on("handshake", (event) => {
|
|
2447
|
-
const projectId = event.data.project_id;
|
|
2448
|
-
const sessionId = event.data.session_id;
|
|
2449
|
-
const frontendUrl = getFrontendUrl(options.baseUrl, options.frontendPort);
|
|
2450
|
-
if (!didLogHandshake) logger$1.info(`View your session at ${frontendUrl}/project/${projectId}/debugger-sessions/${sessionId}`);
|
|
2451
|
-
didLogHandshake = true;
|
|
2452
|
-
});
|
|
2453
|
-
sseClient.on("error", (error) => {
|
|
2454
|
-
logger$1.warn(`Error connecting to backend: ${error.message}`);
|
|
2455
|
-
});
|
|
2456
|
-
sseClient.on("reconnecting", () => {
|
|
2457
|
-
logger$1.info("Reconnecting to backend...");
|
|
2458
|
-
});
|
|
2459
|
-
sseClient.on("heartbeat_timeout", () => {
|
|
2460
|
-
logger$1.debug("Heartbeat timeout, reconnecting...");
|
|
2461
|
-
});
|
|
2462
|
-
sseClient.on("stop", () => {
|
|
2463
|
-
logger$1.debug("Stop event received");
|
|
2464
|
-
stopRequested = true;
|
|
2465
|
-
if (subprocessManager.kill()) logger$1.info("Current run cancelled");
|
|
2466
|
-
});
|
|
2467
|
-
let reloadTimeout = null;
|
|
2468
|
-
let reloadScheduled = false;
|
|
2469
|
-
watcher.on("change", (changedPath) => {
|
|
2470
|
-
logger$1.info(`File changed: ${changedPath}, scheduling reload...`);
|
|
2471
|
-
if (reloadTimeout) clearTimeout(reloadTimeout);
|
|
2472
|
-
reloadTimeout = setTimeout(() => {
|
|
2473
|
-
logger$1.debug("Marking reload as scheduled for next run...");
|
|
2474
|
-
reloadTimeout = null;
|
|
2475
|
-
reloadScheduled = true;
|
|
2476
|
-
}, 100);
|
|
1599
|
+
if (traces.length === 0) {
|
|
1600
|
+
console.log(`No traces found for session ${sessionId}.`);
|
|
1601
|
+
return;
|
|
1602
|
+
}
|
|
1603
|
+
const blocks = traces.map((trace) => {
|
|
1604
|
+
const tag = `<trace id="${trace.traceId}" end-time="${trace.endTime}"/>`;
|
|
1605
|
+
return trace.note ? `${trace.note}\n${tag}` : tag;
|
|
2477
1606
|
});
|
|
2478
|
-
|
|
2479
|
-
logger$1.debug("Shutting down...");
|
|
2480
|
-
if (reloadTimeout) {
|
|
2481
|
-
clearTimeout(reloadTimeout);
|
|
2482
|
-
reloadTimeout = null;
|
|
2483
|
-
}
|
|
2484
|
-
reloadScheduled = false;
|
|
2485
|
-
logger$1.debug("Closing file watcher...");
|
|
2486
|
-
watcher.close().catch((error) => {
|
|
2487
|
-
logger$1.error(`Failed to close file watcher: ${error instanceof Error ? error.message : error}`);
|
|
2488
|
-
});
|
|
2489
|
-
subprocessManager.kill();
|
|
2490
|
-
logger$1.debug("Deleting debugger session...");
|
|
2491
|
-
client.rolloutSessions.delete({ sessionId }).then(() => {
|
|
2492
|
-
if (sseClient) sseClient.shutdown();
|
|
2493
|
-
cacheServer.close(() => {
|
|
2494
|
-
logger$1.debug("Cache server closed");
|
|
2495
|
-
});
|
|
2496
|
-
process.exit(0);
|
|
2497
|
-
}).catch((error) => {
|
|
2498
|
-
logger$1.warn(`Failed to delete debugger session: ${error instanceof Error ? error.message : error}`);
|
|
2499
|
-
process.exit(1);
|
|
2500
|
-
});
|
|
2501
|
-
};
|
|
2502
|
-
process.on("SIGINT", shutdown);
|
|
2503
|
-
process.on("SIGTERM", shutdown);
|
|
2504
|
-
process.stdin.resume();
|
|
2505
|
-
logger$1.debug("Connecting to backend...");
|
|
2506
|
-
await sseClient.connectAndListen();
|
|
1607
|
+
console.log(blocks.join("\n\n"));
|
|
2507
1608
|
} catch (error) {
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
} catch {}
|
|
2512
|
-
await watcher.close();
|
|
2513
|
-
cacheServer.close(() => {
|
|
2514
|
-
process.exit(1);
|
|
2515
|
-
});
|
|
1609
|
+
if (options.json) outputJsonError(error);
|
|
1610
|
+
logger$2.error(`Failed to summarize session: ${errorMessage(error)}`);
|
|
1611
|
+
process.exit(1);
|
|
2516
1612
|
}
|
|
2517
|
-
}
|
|
1613
|
+
};
|
|
2518
1614
|
//#endregion
|
|
2519
1615
|
//#region src/commands/sql/index.ts
|
|
2520
|
-
const logger = initializeLogger();
|
|
1616
|
+
const logger$1 = initializeLogger();
|
|
2521
1617
|
const handleSqlQuery = async (query, options) => {
|
|
2522
1618
|
const client = new LaminarClient({
|
|
2523
1619
|
projectApiKey: options.projectApiKey,
|
|
@@ -2540,7 +1636,7 @@ const handleSqlQuery = async (query, options) => {
|
|
|
2540
1636
|
console.log(`\n${rows.length} row(s)\n`);
|
|
2541
1637
|
} catch (error) {
|
|
2542
1638
|
if (options.json) outputJsonError(error);
|
|
2543
|
-
logger.error(`Query failed: ${
|
|
1639
|
+
logger$1.error(`Query failed: ${errorMessage(error)}`);
|
|
2544
1640
|
process.exit(1);
|
|
2545
1641
|
}
|
|
2546
1642
|
};
|
|
@@ -2591,27 +1687,59 @@ Available tables:
|
|
|
2591
1687
|
data (String), target (String), metadata (String)
|
|
2592
1688
|
`;
|
|
2593
1689
|
//#endregion
|
|
1690
|
+
//#region src/commands/trace/index.ts
|
|
1691
|
+
const logger = initializeLogger();
|
|
1692
|
+
const NOTE_SEPARATOR = "\n\n";
|
|
1693
|
+
/**
|
|
1694
|
+
* Append a free-text note to an existing trace. Stored under the
|
|
1695
|
+
* `rollout.note` trace-metadata key via the post-factum metadata patch
|
|
1696
|
+
* endpoint. The patch endpoint is last-write-wins per key, so the current
|
|
1697
|
+
* note is read back first (via the SQL endpoint) and the new text is pushed
|
|
1698
|
+
* as `existing + "\n\n" + note`. The note may contain markdown /
|
|
1699
|
+
* span-reference links.
|
|
1700
|
+
*
|
|
1701
|
+
* The read-modify-write is not transactional: the patch lands via the async
|
|
1702
|
+
* ingestion queue, so a second append issued within ~a second of the first
|
|
1703
|
+
* can read the pre-patch note and drop the first append. Fine for the
|
|
1704
|
+
* intended cadence (one note per investigation step), not for concurrent
|
|
1705
|
+
* writers.
|
|
1706
|
+
*
|
|
1707
|
+
* TODO: revisit — make the append atomic server-side (e.g. an append mode on
|
|
1708
|
+
* the metadata patch endpoint that concatenates within the Postgres UPDATE,
|
|
1709
|
+
* which already serializes on the trace row lock).
|
|
1710
|
+
*/
|
|
1711
|
+
const handleTraceAppendNote = async (traceId, note, options) => {
|
|
1712
|
+
const client = new LaminarClient({
|
|
1713
|
+
projectApiKey: options.projectApiKey,
|
|
1714
|
+
baseUrl: options.baseUrl,
|
|
1715
|
+
port: options.port
|
|
1716
|
+
});
|
|
1717
|
+
try {
|
|
1718
|
+
const id = normalizeTraceId(traceId);
|
|
1719
|
+
const rows = await client.sql.query("SELECT metadata FROM traces WHERE id = {trace_id:UUID} LIMIT 1", { trace_id: id });
|
|
1720
|
+
if (rows.length === 0) throw new Error(`Trace ${id} not found. If the run just finished, the trace may not be flushed yet. Retry in a few seconds.`);
|
|
1721
|
+
const existing = readNoteFromMetadata(rows[0].metadata);
|
|
1722
|
+
const updated = existing ? `${existing}${NOTE_SEPARATOR}${note}` : note;
|
|
1723
|
+
await client.traces.pushMetadata(id, { [NOTE_METADATA_KEY]: updated }, { failOnNotFound: true });
|
|
1724
|
+
if (options.json) {
|
|
1725
|
+
outputJson({
|
|
1726
|
+
traceId: id,
|
|
1727
|
+
note: updated
|
|
1728
|
+
});
|
|
1729
|
+
return;
|
|
1730
|
+
}
|
|
1731
|
+
logger.info(`Appended note to trace ${id}.`);
|
|
1732
|
+
} catch (error) {
|
|
1733
|
+
if (options.json) outputJsonError(error);
|
|
1734
|
+
logger.error(`Failed to append trace note: ${errorMessage(error)}`);
|
|
1735
|
+
process.exit(1);
|
|
1736
|
+
}
|
|
1737
|
+
};
|
|
1738
|
+
//#endregion
|
|
2594
1739
|
//#region src/index.ts
|
|
2595
1740
|
async function main() {
|
|
2596
1741
|
const program = new commander.Command();
|
|
2597
1742
|
program.name("lmnr-cli").description("CLI for the Laminar agent observability platform").version(version$1, "-v, --version", "display version number");
|
|
2598
|
-
program.command("dev").description("Start a debugging session").argument("[file]", "Path to file containing the entrypoint 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 entrypoint 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 entrypoint function 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) => {
|
|
2599
|
-
if (!file && !options.pythonModule) {
|
|
2600
|
-
console.error("Error: Must provide either a file path or --python-module (-m) flag");
|
|
2601
|
-
process.exit(1);
|
|
2602
|
-
}
|
|
2603
|
-
if (file && options.pythonModule) {
|
|
2604
|
-
console.error("Error: Cannot specify both file path and --python-module (-m) flag");
|
|
2605
|
-
process.exit(1);
|
|
2606
|
-
}
|
|
2607
|
-
await runDev(file, options);
|
|
2608
|
-
}).addHelpText("after", `
|
|
2609
|
-
Examples:
|
|
2610
|
-
$ lmnr-cli dev agent.ts # TypeScript file
|
|
2611
|
-
$ lmnr-cli dev agent.py # Python file (script mode)
|
|
2612
|
-
$ lmnr-cli dev -m src.agent # Python module (module mode)
|
|
2613
|
-
$ lmnr-cli dev agent.ts --function myAgent # Specific function
|
|
2614
|
-
`);
|
|
2615
1743
|
const datasetsCmd = program.command("dataset").description("Manage datasets").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("--json", "Output structured JSON to stdout");
|
|
2616
1744
|
datasetsCmd.command("list").description("List all datasets").action(async (_options, cmd) => {
|
|
2617
1745
|
await handleDatasetsList(cmd.optsWithGlobals());
|
|
@@ -2637,6 +1765,41 @@ Examples:
|
|
|
2637
1765
|
sqlCmd.command("schema").description("Show available tables and their columns").action(() => {
|
|
2638
1766
|
process.stdout.write(SQL_SCHEMA_HELP);
|
|
2639
1767
|
});
|
|
1768
|
+
program.command("trace").description("Operate on existing traces").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("--json", "Output structured JSON to stdout").command("append-note").description("Append a free-text note to a trace (stored in trace metadata)").argument("<trace-id>", "Trace ID (UUID or 32-char OTel hex trace id)").argument("<note>", "Note text (may contain markdown)").action(async (traceId, note, _options, cmd) => {
|
|
1769
|
+
await handleTraceAppendNote(traceId, note, cmd.optsWithGlobals());
|
|
1770
|
+
}).addHelpText("after", `
|
|
1771
|
+
Notes accumulate: each call appends a new paragraph to the trace's existing
|
|
1772
|
+
note rather than overwriting it.
|
|
1773
|
+
|
|
1774
|
+
Examples:
|
|
1775
|
+
$ lmnr-cli trace append-note <trace-id> "Reproduced the timeout on the search tool."
|
|
1776
|
+
`);
|
|
1777
|
+
const debugSessionCmd = program.command("debug").description("Operate on debug sessions").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("--json", "Output structured JSON to stdout").addHelpText("after", `
|
|
1778
|
+
Learn more about debugging features at https://laminar.sh/docs/platform/debugger
|
|
1779
|
+
`).command("session").description("Manage debug sessions").addHelpText("after", `
|
|
1780
|
+
Learn more about debugging features at https://laminar.sh/docs/platform/debugger
|
|
1781
|
+
`);
|
|
1782
|
+
debugSessionCmd.command("set-name").description("Set the display name of a debug session").argument("<session-id>", "Debug session ID").argument("<name>", "Session display name").action(async (sessionId, name, _options, cmd) => {
|
|
1783
|
+
await handleDebugSessionSetName(sessionId, name, cmd.optsWithGlobals());
|
|
1784
|
+
}).addHelpText("after", `
|
|
1785
|
+
Examples:
|
|
1786
|
+
$ lmnr-cli debug session set-name <session-id> "Fix report length + search tool"
|
|
1787
|
+
`);
|
|
1788
|
+
debugSessionCmd.command("summary").description("Print every trace in a debug session with its note, oldest first").argument("<session-id>", "Debug session ID").action(async (sessionId, _options, cmd) => {
|
|
1789
|
+
await handleDebugSessionSummary(sessionId, cmd.optsWithGlobals());
|
|
1790
|
+
}).addHelpText("after", `
|
|
1791
|
+
Output is one block per trace (oldest first), the trace's note followed by a
|
|
1792
|
+
self-closing tag carrying the trace id and end time:
|
|
1793
|
+
|
|
1794
|
+
{note}
|
|
1795
|
+
<trace id="{trace-id}" end-time="{end-time}"/>
|
|
1796
|
+
|
|
1797
|
+
With --json, prints an array of {"note", "traceId", "endTime"} objects.
|
|
1798
|
+
|
|
1799
|
+
Examples:
|
|
1800
|
+
$ lmnr-cli debug session summary <session-id>
|
|
1801
|
+
$ lmnr-cli debug session summary <session-id> --json
|
|
1802
|
+
`);
|
|
2640
1803
|
program.addHelpText("after", `
|
|
2641
1804
|
Authentication:
|
|
2642
1805
|
Most commands require a project API key. Provide it in one of two ways:
|
|
@@ -2645,15 +1808,15 @@ Authentication:
|
|
|
2645
1808
|
Get your key at https://www.laminar.sh (Settings > Project API Keys).
|
|
2646
1809
|
|
|
2647
1810
|
Examples:
|
|
2648
|
-
lmnr-cli dev agent.ts # Debugger TypeScript entrypoint
|
|
2649
|
-
lmnr-cli dev agent.py # Debugger Python script mode
|
|
2650
|
-
lmnr-cli dev -m src.agent # Debugger Python module mode
|
|
2651
1811
|
lmnr-cli dataset list --json # List all datasets
|
|
2652
1812
|
lmnr-cli dataset push data.jsonl -n my-dataset --json # Push data to a dataset
|
|
2653
1813
|
lmnr-cli dataset pull output.jsonl -n my-dataset --json # Pull data from a dataset
|
|
2654
1814
|
lmnr-cli sql query "SELECT * FROM spans LIMIT 10" --json # Query spans
|
|
2655
1815
|
lmnr-cli sql query "SELECT t.id, s.name FROM traces t JOIN spans s ON t.id = s.trace_id" --json
|
|
2656
1816
|
lmnr-cli sql schema # Show available tables
|
|
1817
|
+
lmnr-cli trace append-note <trace-id> "note text" # Append a note to a trace
|
|
1818
|
+
lmnr-cli debug session set-name <session-id> "title" # Rename a debug session
|
|
1819
|
+
lmnr-cli debug session summary <session-id> # Notes for each trace in a session
|
|
2657
1820
|
|
|
2658
1821
|
For more information about the Laminar platfrom:
|
|
2659
1822
|
Documentation: https://laminar.sh/docs
|
|
@@ -2662,7 +1825,7 @@ For more information about the Laminar platfrom:
|
|
|
2662
1825
|
await program.parseAsync();
|
|
2663
1826
|
}
|
|
2664
1827
|
main().catch((err) => {
|
|
2665
|
-
console.error(err
|
|
1828
|
+
console.error(errorMessage(err));
|
|
2666
1829
|
process.exit(1);
|
|
2667
1830
|
});
|
|
2668
1831
|
//#endregion
|