lmnr-cli 0.1.8 → 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/README.md +30 -362
- package/dist/index.cjs +425 -1285
- package/dist/index.cjs.map +1 -1
- package/package.json +7 -19
package/dist/index.cjs
CHANGED
|
@@ -1,23 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
//#region
|
|
2
|
+
//#region \0rolldown/runtime.js
|
|
3
3
|
var __create = Object.create;
|
|
4
4
|
var __defProp = Object.defineProperty;
|
|
5
5
|
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
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
18
|
-
});
|
|
19
|
-
}
|
|
20
|
-
}
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
12
|
+
key = keys[i];
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
14
|
+
get: ((k) => from[k]).bind(null, key),
|
|
15
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
16
|
+
});
|
|
21
17
|
}
|
|
22
18
|
return to;
|
|
23
19
|
};
|
|
@@ -25,16 +21,16 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
25
21
|
value: mod,
|
|
26
22
|
enumerable: true
|
|
27
23
|
}) : target, mod));
|
|
28
|
-
|
|
29
24
|
//#endregion
|
|
30
25
|
let commander = require("commander");
|
|
31
26
|
let fs = require("fs");
|
|
32
27
|
let path = require("path");
|
|
28
|
+
let path$2 = __toESM(path, 1);
|
|
33
29
|
path = __toESM(path);
|
|
34
30
|
let pino = require("pino");
|
|
31
|
+
let pino$3 = __toESM(pino, 1);
|
|
35
32
|
pino = __toESM(pino);
|
|
36
33
|
let pino_pretty = require("pino-pretty");
|
|
37
|
-
let uuid = require("uuid");
|
|
38
34
|
let csv_parser = require("csv-parser");
|
|
39
35
|
csv_parser = __toESM(csv_parser);
|
|
40
36
|
let export_to_csv = require("export-to-csv");
|
|
@@ -42,107 +38,27 @@ let fs_promises = require("fs/promises");
|
|
|
42
38
|
fs_promises = __toESM(fs_promises);
|
|
43
39
|
let cli_table3 = require("cli-table3");
|
|
44
40
|
cli_table3 = __toESM(cli_table3);
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
let http = require("http");
|
|
48
|
-
http = __toESM(http);
|
|
49
|
-
let events = require("events");
|
|
50
|
-
let eventsource_parser = require("eventsource-parser");
|
|
51
|
-
let child_process = require("child_process");
|
|
52
|
-
let readline = require("readline");
|
|
53
|
-
readline = __toESM(readline);
|
|
54
|
-
|
|
55
|
-
//#region package.json
|
|
56
|
-
var version$1 = "0.1.8";
|
|
57
|
-
|
|
41
|
+
//#region ../types/dist/index.mjs
|
|
42
|
+
const errorMessage = (error) => error instanceof Error ? error.message : String(error);
|
|
58
43
|
//#endregion
|
|
59
|
-
//#region
|
|
60
|
-
var
|
|
61
|
-
module.exports = {
|
|
62
|
-
"name": "dotenv",
|
|
63
|
-
"version": "17.2.3",
|
|
64
|
-
"description": "Loads environment variables from .env file",
|
|
65
|
-
"main": "lib/main.js",
|
|
66
|
-
"types": "lib/main.d.ts",
|
|
67
|
-
"exports": {
|
|
68
|
-
".": {
|
|
69
|
-
"types": "./lib/main.d.ts",
|
|
70
|
-
"require": "./lib/main.js",
|
|
71
|
-
"default": "./lib/main.js"
|
|
72
|
-
},
|
|
73
|
-
"./config": "./config.js",
|
|
74
|
-
"./config.js": "./config.js",
|
|
75
|
-
"./lib/env-options": "./lib/env-options.js",
|
|
76
|
-
"./lib/env-options.js": "./lib/env-options.js",
|
|
77
|
-
"./lib/cli-options": "./lib/cli-options.js",
|
|
78
|
-
"./lib/cli-options.js": "./lib/cli-options.js",
|
|
79
|
-
"./package.json": "./package.json"
|
|
80
|
-
},
|
|
81
|
-
"scripts": {
|
|
82
|
-
"dts-check": "tsc --project tests/types/tsconfig.json",
|
|
83
|
-
"lint": "standard",
|
|
84
|
-
"pretest": "npm run lint && npm run dts-check",
|
|
85
|
-
"test": "tap run tests/**/*.js --allow-empty-coverage --disable-coverage --timeout=60000",
|
|
86
|
-
"test:coverage": "tap run tests/**/*.js --show-full-coverage --timeout=60000 --coverage-report=text --coverage-report=lcov",
|
|
87
|
-
"prerelease": "npm test",
|
|
88
|
-
"release": "standard-version"
|
|
89
|
-
},
|
|
90
|
-
"repository": {
|
|
91
|
-
"type": "git",
|
|
92
|
-
"url": "git://github.com/motdotla/dotenv.git"
|
|
93
|
-
},
|
|
94
|
-
"homepage": "https://github.com/motdotla/dotenv#readme",
|
|
95
|
-
"funding": "https://dotenvx.com",
|
|
96
|
-
"keywords": [
|
|
97
|
-
"dotenv",
|
|
98
|
-
"env",
|
|
99
|
-
".env",
|
|
100
|
-
"environment",
|
|
101
|
-
"variables",
|
|
102
|
-
"config",
|
|
103
|
-
"settings"
|
|
104
|
-
],
|
|
105
|
-
"readmeFilename": "README.md",
|
|
106
|
-
"license": "BSD-2-Clause",
|
|
107
|
-
"devDependencies": {
|
|
108
|
-
"@types/node": "^18.11.3",
|
|
109
|
-
"decache": "^4.6.2",
|
|
110
|
-
"sinon": "^14.0.1",
|
|
111
|
-
"standard": "^17.0.0",
|
|
112
|
-
"standard-version": "^9.5.0",
|
|
113
|
-
"tap": "^19.2.0",
|
|
114
|
-
"typescript": "^4.8.4"
|
|
115
|
-
},
|
|
116
|
-
"engines": { "node": ">=12" },
|
|
117
|
-
"browser": { "fs": false }
|
|
118
|
-
};
|
|
119
|
-
}));
|
|
120
|
-
|
|
44
|
+
//#region package.json
|
|
45
|
+
var version$1 = "0.1.10";
|
|
121
46
|
//#endregion
|
|
122
|
-
//#region ../../node_modules/.pnpm/dotenv@17.2
|
|
47
|
+
//#region ../../node_modules/.pnpm/dotenv@17.4.2/node_modules/dotenv/lib/main.js
|
|
123
48
|
var require_main = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
124
49
|
const fs$1 = require("fs");
|
|
125
50
|
const path$1 = require("path");
|
|
126
51
|
const os = require("os");
|
|
127
52
|
const crypto$1 = require("crypto");
|
|
128
|
-
const version = require_package().version;
|
|
129
53
|
const TIPS = [
|
|
130
|
-
"
|
|
131
|
-
"
|
|
132
|
-
"
|
|
133
|
-
"
|
|
134
|
-
"
|
|
135
|
-
"
|
|
136
|
-
"
|
|
137
|
-
"
|
|
138
|
-
"🔑 add access controls to secrets: https://dotenvx.com/ops",
|
|
139
|
-
"🛠️ run anywhere with `dotenvx run -- yourcommand`",
|
|
140
|
-
"⚙️ specify custom .env file path with { path: '/custom/path/.env' }",
|
|
141
|
-
"⚙️ enable debug logging with { debug: true }",
|
|
142
|
-
"⚙️ override existing env vars with { override: true }",
|
|
143
|
-
"⚙️ suppress all logs with { quiet: true }",
|
|
144
|
-
"⚙️ write to custom object with { processEnv: myObject }",
|
|
145
|
-
"⚙️ 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'] }"
|
|
146
62
|
];
|
|
147
63
|
function _getRandomTip() {
|
|
148
64
|
return TIPS[Math.floor(Math.random() * TIPS.length)];
|
|
@@ -206,13 +122,13 @@ var require_main = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
206
122
|
return DotenvModule.parse(decrypted);
|
|
207
123
|
}
|
|
208
124
|
function _warn(message) {
|
|
209
|
-
console.error(
|
|
125
|
+
console.error(`⚠ ${message}`);
|
|
210
126
|
}
|
|
211
127
|
function _debug(message) {
|
|
212
|
-
console.log(
|
|
128
|
+
console.log(`┆ ${message}`);
|
|
213
129
|
}
|
|
214
130
|
function _log(message) {
|
|
215
|
-
console.log(
|
|
131
|
+
console.log(`◇ ${message}`);
|
|
216
132
|
}
|
|
217
133
|
function _dotenvKey(options) {
|
|
218
134
|
if (options && options.DOTENV_KEY && options.DOTENV_KEY.length > 0) return options.DOTENV_KEY;
|
|
@@ -270,7 +186,7 @@ var require_main = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
270
186
|
function _configVault(options) {
|
|
271
187
|
const debug = parseBoolean(process.env.DOTENV_CONFIG_DEBUG || options && options.debug);
|
|
272
188
|
const quiet = parseBoolean(process.env.DOTENV_CONFIG_QUIET || options && options.quiet);
|
|
273
|
-
if (debug || !quiet) _log("
|
|
189
|
+
if (debug || !quiet) _log("loading env from encrypted .env.vault");
|
|
274
190
|
const parsed = DotenvModule._parseVault(options);
|
|
275
191
|
let processEnv = process.env;
|
|
276
192
|
if (options && options.processEnv != null) processEnv = options.processEnv;
|
|
@@ -285,7 +201,7 @@ var require_main = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
285
201
|
let debug = parseBoolean(processEnv.DOTENV_CONFIG_DEBUG || options && options.debug);
|
|
286
202
|
let quiet = parseBoolean(processEnv.DOTENV_CONFIG_QUIET || options && options.quiet);
|
|
287
203
|
if (options && options.encoding) encoding = options.encoding;
|
|
288
|
-
else if (debug) _debug("
|
|
204
|
+
else if (debug) _debug("no encoding is specified (UTF-8 is used by default)");
|
|
289
205
|
let optionPaths = [dotenvPath];
|
|
290
206
|
if (options && options.path) if (!Array.isArray(options.path)) optionPaths = [_resolveHome(options.path)];
|
|
291
207
|
else {
|
|
@@ -294,11 +210,11 @@ var require_main = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
294
210
|
}
|
|
295
211
|
let lastError;
|
|
296
212
|
const parsedAll = {};
|
|
297
|
-
for (const path$
|
|
298
|
-
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 }));
|
|
299
215
|
DotenvModule.populate(parsedAll, parsed, options);
|
|
300
216
|
} catch (e) {
|
|
301
|
-
if (debug) _debug(`
|
|
217
|
+
if (debug) _debug(`failed to load ${path$3} ${e.message}`);
|
|
302
218
|
lastError = e;
|
|
303
219
|
}
|
|
304
220
|
const populated = DotenvModule.populate(processEnv, parsedAll, options);
|
|
@@ -311,10 +227,10 @@ var require_main = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
311
227
|
const relative = path$1.relative(process.cwd(), filePath);
|
|
312
228
|
shortPaths.push(relative);
|
|
313
229
|
} catch (e) {
|
|
314
|
-
if (debug) _debug(`
|
|
230
|
+
if (debug) _debug(`failed to load ${filePath} ${e.message}`);
|
|
315
231
|
lastError = e;
|
|
316
232
|
}
|
|
317
|
-
_log(`
|
|
233
|
+
_log(`injected env (${keysCount}) from ${shortPaths.join(",")} ${dim(`// tip: ${_getRandomTip()}`)}`);
|
|
318
234
|
}
|
|
319
235
|
if (lastError) return {
|
|
320
236
|
parsed: parsedAll,
|
|
@@ -326,7 +242,7 @@ var require_main = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
326
242
|
if (_dotenvKey(options).length === 0) return DotenvModule.configDotenv(options);
|
|
327
243
|
const vaultPath = _vaultPath(options);
|
|
328
244
|
if (!vaultPath) {
|
|
329
|
-
_warn(`
|
|
245
|
+
_warn(`you set DOTENV_KEY but you are missing a .env.vault file at ${vaultPath}`);
|
|
330
246
|
return DotenvModule.configDotenv(options);
|
|
331
247
|
}
|
|
332
248
|
return DotenvModule._configVault(options);
|
|
@@ -396,11 +312,43 @@ var require_main = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
396
312
|
module.exports.populate = DotenvModule.populate;
|
|
397
313
|
module.exports = DotenvModule;
|
|
398
314
|
}));
|
|
399
|
-
|
|
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
|
+
}
|
|
400
348
|
//#endregion
|
|
401
349
|
//#region ../client/dist/index.mjs
|
|
402
350
|
var import_main = require_main();
|
|
403
|
-
var version = "0.8.
|
|
351
|
+
var version = "0.8.27";
|
|
404
352
|
function getLangVersion() {
|
|
405
353
|
if (typeof process !== "undefined" && process.versions && process.versions.node) return `node-${process.versions.node}`;
|
|
406
354
|
if (typeof navigator !== "undefined" && navigator.userAgent) return `browser-${navigator.userAgent}`;
|
|
@@ -427,11 +375,11 @@ var BrowserEventsResource = class extends BaseResource {
|
|
|
427
375
|
constructor(baseHttpUrl, projectApiKey) {
|
|
428
376
|
super(baseHttpUrl, projectApiKey);
|
|
429
377
|
}
|
|
430
|
-
async send({ sessionId, traceId, events
|
|
378
|
+
async send({ sessionId, traceId, events }) {
|
|
431
379
|
const payload = {
|
|
432
380
|
sessionId,
|
|
433
381
|
traceId,
|
|
434
|
-
events
|
|
382
|
+
events,
|
|
435
383
|
source: getLangVersion() ?? "javascript",
|
|
436
384
|
sdkVersion: version
|
|
437
385
|
};
|
|
@@ -452,34 +400,34 @@ var BrowserEventsResource = class extends BaseResource {
|
|
|
452
400
|
function initializeLogger$1(options) {
|
|
453
401
|
const colorize = options?.colorize ?? true;
|
|
454
402
|
const level = options?.level ?? process.env.LMNR_LOG_LEVEL?.toLowerCase()?.trim() ?? "info";
|
|
455
|
-
return (0, pino.default)({ level }, (0, pino_pretty.PinoPretty)({
|
|
403
|
+
return (0, pino$3.default)({ level }, (0, pino_pretty.PinoPretty)({
|
|
456
404
|
colorize,
|
|
457
405
|
minimumLevel: level
|
|
458
406
|
}));
|
|
459
407
|
}
|
|
460
|
-
const logger$
|
|
408
|
+
const logger$4$1 = initializeLogger$1();
|
|
461
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);
|
|
462
|
-
const newUUID
|
|
410
|
+
const newUUID = () => {
|
|
463
411
|
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return crypto.randomUUID();
|
|
464
|
-
else return
|
|
412
|
+
else return v4();
|
|
465
413
|
};
|
|
466
414
|
const otelSpanIdToUUID = (spanId) => {
|
|
467
415
|
let id = spanId.toLowerCase();
|
|
468
416
|
if (id.startsWith("0x")) id = id.slice(2);
|
|
469
|
-
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.`);
|
|
470
418
|
if (!/^[0-9a-f]+$/.test(id)) {
|
|
471
|
-
logger$
|
|
472
|
-
return newUUID
|
|
419
|
+
logger$4$1.error(`Span ID ${spanId} is not a valid hex string. Generating a random UUID instead.`);
|
|
420
|
+
return newUUID();
|
|
473
421
|
}
|
|
474
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");
|
|
475
423
|
};
|
|
476
424
|
const otelTraceIdToUUID = (traceId) => {
|
|
477
425
|
let id = traceId.toLowerCase();
|
|
478
426
|
if (id.startsWith("0x")) id = id.slice(2);
|
|
479
|
-
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.`);
|
|
480
428
|
if (!/^[0-9a-f]+$/.test(id)) {
|
|
481
|
-
logger$
|
|
482
|
-
return newUUID
|
|
429
|
+
logger$4$1.error(`Trace ID ${traceId} is not a valid hex string. Generating a random UUID instead.`);
|
|
430
|
+
return newUUID();
|
|
483
431
|
}
|
|
484
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");
|
|
485
433
|
};
|
|
@@ -502,11 +450,11 @@ const loadEnv = (options) => {
|
|
|
502
450
|
const verbose = ["debug", "trace"].includes(logLevel.trim().toLowerCase());
|
|
503
451
|
const quiet = options?.quiet ?? !verbose;
|
|
504
452
|
(0, import_main.config)({
|
|
505
|
-
path: options?.paths ?? envFiles.map((envFile) => path.resolve(envDir, envFile)),
|
|
453
|
+
path: options?.paths ?? envFiles.map((envFile) => path$2.resolve(envDir, envFile)),
|
|
506
454
|
quiet
|
|
507
455
|
});
|
|
508
456
|
};
|
|
509
|
-
const logger$
|
|
457
|
+
const logger$3$1 = initializeLogger$1();
|
|
510
458
|
const DEFAULT_DATASET_PULL_LIMIT = 100;
|
|
511
459
|
const DEFAULT_DATASET_PUSH_BATCH_SIZE$1 = 100;
|
|
512
460
|
var DatasetsResource = class extends BaseResource {
|
|
@@ -561,7 +509,7 @@ var DatasetsResource = class extends BaseResource {
|
|
|
561
509
|
let response;
|
|
562
510
|
for (let i = 0; i < points.length; i += batchSize) {
|
|
563
511
|
const batchNum = Math.floor(i / batchSize) + 1;
|
|
564
|
-
logger$
|
|
512
|
+
logger$3$1.debug(`Pushing batch ${batchNum} of ${totalBatches}`);
|
|
565
513
|
const batch = points.slice(i, i + batchSize);
|
|
566
514
|
const fetchResponse = await fetch(this.baseHttpUrl + "/v1/datasets/datapoints", {
|
|
567
515
|
method: "POST",
|
|
@@ -609,7 +557,7 @@ var DatasetsResource = class extends BaseResource {
|
|
|
609
557
|
return response.json();
|
|
610
558
|
}
|
|
611
559
|
};
|
|
612
|
-
const logger$
|
|
560
|
+
const logger$2$1 = initializeLogger$1();
|
|
613
561
|
const INITIAL_EVALUATION_DATAPOINT_MAX_DATA_LENGTH = 16e6;
|
|
614
562
|
var EvalsResource = class extends BaseResource {
|
|
615
563
|
constructor(baseHttpUrl, projectApiKey) {
|
|
@@ -667,14 +615,14 @@ var EvalsResource = class extends BaseResource {
|
|
|
667
615
|
* @returns {Promise<StringUUID>} The datapoint ID
|
|
668
616
|
*/
|
|
669
617
|
async createDatapoint({ evalId, data, target, metadata, index, traceId }) {
|
|
670
|
-
const datapointId = newUUID
|
|
618
|
+
const datapointId = newUUID();
|
|
671
619
|
const partialDatapoint = {
|
|
672
620
|
id: datapointId,
|
|
673
621
|
data,
|
|
674
622
|
target,
|
|
675
623
|
index: index ?? 0,
|
|
676
|
-
traceId: traceId ?? newUUID
|
|
677
|
-
executorSpanId: newUUID
|
|
624
|
+
traceId: traceId ?? newUUID(),
|
|
625
|
+
executorSpanId: newUUID(),
|
|
678
626
|
metadata
|
|
679
627
|
};
|
|
680
628
|
await this.saveDatapoints({
|
|
@@ -745,7 +693,7 @@ var EvalsResource = class extends BaseResource {
|
|
|
745
693
|
* @returns {Promise<GetDatapointsResponse>} Response from the datapoint retrieval
|
|
746
694
|
*/
|
|
747
695
|
async getDatapoints({ datasetName, offset, limit }) {
|
|
748
|
-
logger$
|
|
696
|
+
logger$2$1.warn("evals.getDatapoints() is deprecated. Use client.datasets.pull() instead.");
|
|
749
697
|
const params = new URLSearchParams({
|
|
750
698
|
name: datasetName,
|
|
751
699
|
offset: offset.toString(),
|
|
@@ -762,7 +710,7 @@ var EvalsResource = class extends BaseResource {
|
|
|
762
710
|
let length = initialLength;
|
|
763
711
|
let lastResponse = null;
|
|
764
712
|
for (let i = 0; i < maxRetries; i++) {
|
|
765
|
-
logger$
|
|
713
|
+
logger$2$1.debug(`Retrying save datapoints... ${i + 1} of ${maxRetries}, length: ${length}`);
|
|
766
714
|
const response = await fetch(this.baseHttpUrl + `/v1/evals/${evalId}/datapoints`, {
|
|
767
715
|
method: "POST",
|
|
768
716
|
headers: this.headers(),
|
|
@@ -783,11 +731,6 @@ var EvalsResource = class extends BaseResource {
|
|
|
783
731
|
if (lastResponse && !lastResponse.ok) await this.handleError(lastResponse);
|
|
784
732
|
}
|
|
785
733
|
};
|
|
786
|
-
var EvaluatorScoreSourceType = /* @__PURE__ */ function(EvaluatorScoreSourceType$1) {
|
|
787
|
-
EvaluatorScoreSourceType$1["Evaluator"] = "Evaluator";
|
|
788
|
-
EvaluatorScoreSourceType$1["Code"] = "Code";
|
|
789
|
-
return EvaluatorScoreSourceType$1;
|
|
790
|
-
}(EvaluatorScoreSourceType || {});
|
|
791
734
|
/**
|
|
792
735
|
* Resource for creating evaluator scores
|
|
793
736
|
*/
|
|
@@ -826,25 +769,21 @@ var EvaluatorsResource = class extends BaseResource {
|
|
|
826
769
|
async score(options) {
|
|
827
770
|
const { name, metadata, score } = options;
|
|
828
771
|
let payload;
|
|
829
|
-
if ("traceId" in options && options.traceId) {
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
source: EvaluatorScoreSourceType.Code,
|
|
845
|
-
spanId: formattedSpanId
|
|
846
|
-
};
|
|
847
|
-
} 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.");
|
|
848
787
|
const response = await fetch(this.baseHttpUrl + "/v1/evaluators/score", {
|
|
849
788
|
method: "POST",
|
|
850
789
|
headers: this.headers(),
|
|
@@ -853,60 +792,51 @@ var EvaluatorsResource = class extends BaseResource {
|
|
|
853
792
|
if (!response.ok) await this.handleError(response);
|
|
854
793
|
}
|
|
855
794
|
};
|
|
795
|
+
const logger$1$1 = initializeLogger$1();
|
|
856
796
|
var RolloutSessionsResource = class extends BaseResource {
|
|
857
797
|
constructor(baseHttpUrl, projectApiKey) {
|
|
858
798
|
super(baseHttpUrl, projectApiKey);
|
|
859
799
|
}
|
|
860
800
|
/**
|
|
861
|
-
*
|
|
862
|
-
*
|
|
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.
|
|
863
807
|
*/
|
|
864
|
-
async
|
|
808
|
+
async register({ sessionId, name }) {
|
|
865
809
|
const response = await fetch(`${this.baseHttpUrl}/v1/rollouts/${sessionId}`, {
|
|
866
810
|
method: "POST",
|
|
867
|
-
headers:
|
|
868
|
-
|
|
869
|
-
"Accept": "text/event-stream"
|
|
870
|
-
},
|
|
871
|
-
body: JSON.stringify({
|
|
872
|
-
name,
|
|
873
|
-
params
|
|
874
|
-
}),
|
|
875
|
-
signal
|
|
876
|
-
});
|
|
877
|
-
if (!response.ok) throw new Error(`SSE connection failed: ${response.status} ${response.statusText}`);
|
|
878
|
-
if (!response.body) throw new Error("No response body");
|
|
879
|
-
return response;
|
|
880
|
-
}
|
|
881
|
-
async delete({ sessionId }) {
|
|
882
|
-
const response = await fetch(`${this.baseHttpUrl}/v1/rollouts/${sessionId}`, {
|
|
883
|
-
method: "DELETE",
|
|
884
|
-
headers: this.headers()
|
|
811
|
+
headers: this.headers(),
|
|
812
|
+
body: JSON.stringify({ name })
|
|
885
813
|
});
|
|
886
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
|
+
}
|
|
887
821
|
}
|
|
888
|
-
|
|
889
|
-
|
|
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`, {
|
|
890
830
|
method: "PATCH",
|
|
891
831
|
headers: this.headers(),
|
|
892
|
-
body: JSON.stringify({
|
|
832
|
+
body: JSON.stringify({ name })
|
|
893
833
|
});
|
|
894
834
|
if (!response.ok) await this.handleError(response);
|
|
895
835
|
}
|
|
896
|
-
async
|
|
897
|
-
const response = await fetch(`${this.baseHttpUrl}/v1/rollouts/${sessionId}
|
|
898
|
-
method: "
|
|
899
|
-
headers: this.headers()
|
|
900
|
-
body: JSON.stringify({
|
|
901
|
-
type: "spanStart",
|
|
902
|
-
spanId: otelSpanIdToUUID(span.spanId),
|
|
903
|
-
traceId: otelTraceIdToUUID(span.traceId),
|
|
904
|
-
parentSpanId: span.parentSpanId ? otelSpanIdToUUID(span.parentSpanId) : void 0,
|
|
905
|
-
attributes: span.attributes,
|
|
906
|
-
startTime: span.startTime,
|
|
907
|
-
name: span.name,
|
|
908
|
-
spanType: span.spanType
|
|
909
|
-
})
|
|
836
|
+
async delete({ sessionId }) {
|
|
837
|
+
const response = await fetch(`${this.baseHttpUrl}/v1/rollouts/${sessionId}`, {
|
|
838
|
+
method: "DELETE",
|
|
839
|
+
headers: this.headers()
|
|
910
840
|
});
|
|
911
841
|
if (!response.ok) await this.handleError(response);
|
|
912
842
|
}
|
|
@@ -983,12 +913,91 @@ var TagsResource = class extends BaseResource {
|
|
|
983
913
|
return response.json();
|
|
984
914
|
}
|
|
985
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
|
+
};
|
|
986
994
|
var LaminarClient = class {
|
|
987
995
|
constructor({ baseUrl, projectApiKey, port } = {}) {
|
|
988
996
|
loadEnv();
|
|
989
997
|
this.projectApiKey = projectApiKey ?? process.env.LMNR_PROJECT_API_KEY;
|
|
990
998
|
const httpPort = port ?? (baseUrl?.match(/:\d{1,5}$/g) ? parseInt(baseUrl.match(/:\d{1,5}$/g)[0].slice(1)) : 443);
|
|
991
|
-
|
|
999
|
+
const baseUrlNoPort = (baseUrl ?? process.env.LMNR_BASE_URL)?.replace(/\/$/, "").replace(/:\d{1,5}$/g, "");
|
|
1000
|
+
this.baseUrl = `${baseUrlNoPort ?? "https://api.lmnr.ai"}:${httpPort}`;
|
|
992
1001
|
this._browserEvents = new BrowserEventsResource(this.baseUrl, this.projectApiKey);
|
|
993
1002
|
this._datasets = new DatasetsResource(this.baseUrl, this.projectApiKey);
|
|
994
1003
|
this._evals = new EvalsResource(this.baseUrl, this.projectApiKey);
|
|
@@ -996,6 +1005,7 @@ var LaminarClient = class {
|
|
|
996
1005
|
this._rolloutSessions = new RolloutSessionsResource(this.baseUrl, this.projectApiKey);
|
|
997
1006
|
this._sql = new SqlResource(this.baseUrl, this.projectApiKey);
|
|
998
1007
|
this._tags = new TagsResource(this.baseUrl, this.projectApiKey);
|
|
1008
|
+
this._traces = new TracesResource(this.baseUrl, this.projectApiKey);
|
|
999
1009
|
}
|
|
1000
1010
|
get browserEvents() {
|
|
1001
1011
|
return this._browserEvents;
|
|
@@ -1018,8 +1028,10 @@ var LaminarClient = class {
|
|
|
1018
1028
|
get tags() {
|
|
1019
1029
|
return this._tags;
|
|
1020
1030
|
}
|
|
1031
|
+
get traces() {
|
|
1032
|
+
return this._traces;
|
|
1033
|
+
}
|
|
1021
1034
|
};
|
|
1022
|
-
|
|
1023
1035
|
//#endregion
|
|
1024
1036
|
//#region src/utils/logger.ts
|
|
1025
1037
|
function initializeLogger(options) {
|
|
@@ -1031,10 +1043,9 @@ function initializeLogger(options) {
|
|
|
1031
1043
|
destination: 2
|
|
1032
1044
|
}));
|
|
1033
1045
|
}
|
|
1034
|
-
|
|
1035
1046
|
//#endregion
|
|
1036
1047
|
//#region src/utils/file.ts
|
|
1037
|
-
const logger$
|
|
1048
|
+
const logger$4 = initializeLogger();
|
|
1038
1049
|
/**
|
|
1039
1050
|
* Check if a file has a supported extension.
|
|
1040
1051
|
*/
|
|
@@ -1055,7 +1066,7 @@ const collectFiles = async (paths, recursive = false) => {
|
|
|
1055
1066
|
for (const filepath of paths) try {
|
|
1056
1067
|
const stats = await fs_promises.stat(filepath);
|
|
1057
1068
|
if (stats.isFile()) if (isSupportedFile(filepath)) collectedFiles.push(filepath);
|
|
1058
|
-
else logger$
|
|
1069
|
+
else logger$4.warn(`Skipping unsupported file type: ${filepath}`);
|
|
1059
1070
|
else if (stats.isDirectory()) {
|
|
1060
1071
|
const entries = await fs_promises.readdir(filepath);
|
|
1061
1072
|
for (const entry of entries) {
|
|
@@ -1069,7 +1080,7 @@ const collectFiles = async (paths, recursive = false) => {
|
|
|
1069
1080
|
}
|
|
1070
1081
|
}
|
|
1071
1082
|
} catch (error) {
|
|
1072
|
-
logger$
|
|
1083
|
+
logger$4.warn(`Path does not exist or is not accessible: ${filepath}. Error: ${errorMessage(error)}`);
|
|
1073
1084
|
}
|
|
1074
1085
|
return collectedFiles;
|
|
1075
1086
|
};
|
|
@@ -1091,7 +1102,7 @@ const tryParseJson = (content) => {
|
|
|
1091
1102
|
try {
|
|
1092
1103
|
return JSON.parse(content);
|
|
1093
1104
|
} catch (error) {
|
|
1094
|
-
logger$
|
|
1105
|
+
logger$4.debug(`Error parsing JSON: ${errorMessage(error)}`);
|
|
1095
1106
|
return content;
|
|
1096
1107
|
}
|
|
1097
1108
|
};
|
|
@@ -1132,17 +1143,17 @@ async function readFile(filepath) {
|
|
|
1132
1143
|
const loadFromPaths = async (paths, recursive = false) => {
|
|
1133
1144
|
const files = await collectFiles(paths, recursive);
|
|
1134
1145
|
if (files.length === 0) {
|
|
1135
|
-
logger$
|
|
1146
|
+
logger$4.warn("No supported files found in the specified paths");
|
|
1136
1147
|
return [];
|
|
1137
1148
|
}
|
|
1138
|
-
logger$
|
|
1149
|
+
logger$4.info(`Found ${files.length} file(s) to read`);
|
|
1139
1150
|
const result = [];
|
|
1140
1151
|
for (const file of files) try {
|
|
1141
1152
|
const data = await readFile(file);
|
|
1142
1153
|
result.push(...data);
|
|
1143
|
-
logger$
|
|
1154
|
+
logger$4.info(`Read ${data.length} record(s) from ${file}`);
|
|
1144
1155
|
} catch (error) {
|
|
1145
|
-
logger$
|
|
1156
|
+
logger$4.error(`Error reading file ${file}: ${errorMessage(error)}`);
|
|
1146
1157
|
throw error;
|
|
1147
1158
|
}
|
|
1148
1159
|
return result;
|
|
@@ -1182,7 +1193,7 @@ const writeToFile = async (filepath, data, format) => {
|
|
|
1182
1193
|
const dir = path.dirname(filepath);
|
|
1183
1194
|
await fs_promises.mkdir(dir, { recursive: true });
|
|
1184
1195
|
const ext = format ?? path.extname(filepath).slice(1);
|
|
1185
|
-
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)}`);
|
|
1186
1197
|
if (ext === "json") await writeJsonFile(filepath, data);
|
|
1187
1198
|
else if (ext === "csv") await writeCsvFile(filepath, data);
|
|
1188
1199
|
else if (ext === "jsonl") await writeJsonlFile(filepath, data);
|
|
@@ -1207,14 +1218,13 @@ const printToConsole = (data, format = "json") => {
|
|
|
1207
1218
|
if (format === "json") console.log(JSON.stringify(data, null, 2));
|
|
1208
1219
|
else if (format === "csv") {
|
|
1209
1220
|
if (data.length === 0) {
|
|
1210
|
-
logger$
|
|
1221
|
+
logger$4.error("No data to print");
|
|
1211
1222
|
return;
|
|
1212
1223
|
}
|
|
1213
1224
|
console.log(formatCsv(data));
|
|
1214
1225
|
} else if (format === "jsonl") data.forEach((item) => console.log(JSON.stringify(item)));
|
|
1215
1226
|
else throw new Error(`Unsupported output format: ${String(format)}. (supported formats: json, csv, jsonl)`);
|
|
1216
1227
|
};
|
|
1217
|
-
|
|
1218
1228
|
//#endregion
|
|
1219
1229
|
//#region src/utils/output.ts
|
|
1220
1230
|
/**
|
|
@@ -1229,10 +1239,9 @@ function outputJson(data) {
|
|
|
1229
1239
|
* Use this in --json mode so agents can parse the failure.
|
|
1230
1240
|
*/
|
|
1231
1241
|
function outputJsonError(error, exitCode = 1) {
|
|
1232
|
-
console.log(JSON.stringify({ error:
|
|
1242
|
+
console.log(JSON.stringify({ error: errorMessage(error) }));
|
|
1233
1243
|
process.exit(exitCode);
|
|
1234
1244
|
}
|
|
1235
|
-
|
|
1236
1245
|
//#endregion
|
|
1237
1246
|
//#region src/utils/table.ts
|
|
1238
1247
|
const DEFAULT_TERMINAL_WIDTH = 80;
|
|
@@ -1294,10 +1303,9 @@ function renderTable(head, rows) {
|
|
|
1294
1303
|
else for (const row of rows) table.push(row);
|
|
1295
1304
|
return table.toString();
|
|
1296
1305
|
}
|
|
1297
|
-
|
|
1298
1306
|
//#endregion
|
|
1299
1307
|
//#region src/commands/dataset/index.ts
|
|
1300
|
-
const logger$
|
|
1308
|
+
const logger$3 = initializeLogger();
|
|
1301
1309
|
const DEFAULT_DATASET_PULL_BATCH_SIZE = 100;
|
|
1302
1310
|
const DEFAULT_DATASET_PUSH_BATCH_SIZE = 100;
|
|
1303
1311
|
/**
|
|
@@ -1358,7 +1366,7 @@ const handleDatasetsList = async (options) => {
|
|
|
1358
1366
|
console.log(`\nTotal: ${datasets.length} dataset(s)\n`);
|
|
1359
1367
|
} catch (error) {
|
|
1360
1368
|
if (options.json) outputJsonError(error);
|
|
1361
|
-
logger$
|
|
1369
|
+
logger$3.error(`Failed to list datasets: ${errorMessage(error)}`);
|
|
1362
1370
|
process.exit(1);
|
|
1363
1371
|
}
|
|
1364
1372
|
};
|
|
@@ -1368,12 +1376,12 @@ const handleDatasetsList = async (options) => {
|
|
|
1368
1376
|
const handleDatasetsPush = async (paths, options) => {
|
|
1369
1377
|
if (!options.name && !options.id) {
|
|
1370
1378
|
if (options.json) outputJsonError("Either name or id must be provided");
|
|
1371
|
-
logger$
|
|
1379
|
+
logger$3.error("Either name or id must be provided");
|
|
1372
1380
|
process.exit(1);
|
|
1373
1381
|
}
|
|
1374
1382
|
if (options.name && options.id) {
|
|
1375
1383
|
if (options.json) outputJsonError("Only one of name or id must be provided");
|
|
1376
|
-
logger$
|
|
1384
|
+
logger$3.error("Only one of name or id must be provided");
|
|
1377
1385
|
process.exit(1);
|
|
1378
1386
|
}
|
|
1379
1387
|
const client = new LaminarClient({
|
|
@@ -1385,7 +1393,7 @@ const handleDatasetsPush = async (paths, options) => {
|
|
|
1385
1393
|
const data = await loadFromPaths(paths, options.recursive);
|
|
1386
1394
|
if (data.length === 0) {
|
|
1387
1395
|
if (options.json) outputJsonError("No data to push");
|
|
1388
|
-
logger$
|
|
1396
|
+
logger$3.error("No data to push. Skipping");
|
|
1389
1397
|
process.exit(1);
|
|
1390
1398
|
}
|
|
1391
1399
|
const identifier = options.name ? { name: options.name } : { id: options.id };
|
|
@@ -1401,10 +1409,10 @@ const handleDatasetsPush = async (paths, options) => {
|
|
|
1401
1409
|
});
|
|
1402
1410
|
return;
|
|
1403
1411
|
}
|
|
1404
|
-
logger$
|
|
1412
|
+
logger$3.info(`Pushed ${data.length} data points to dataset ${options.name || options.id}`);
|
|
1405
1413
|
} catch (error) {
|
|
1406
1414
|
if (options.json) outputJsonError(error);
|
|
1407
|
-
logger$
|
|
1415
|
+
logger$3.error(`Failed to push dataset: ${errorMessage(error)}`);
|
|
1408
1416
|
process.exit(1);
|
|
1409
1417
|
}
|
|
1410
1418
|
};
|
|
@@ -1414,12 +1422,12 @@ const handleDatasetsPush = async (paths, options) => {
|
|
|
1414
1422
|
const handleDatasetsPull = async (outputPath, options) => {
|
|
1415
1423
|
if (!options.name && !options.id) {
|
|
1416
1424
|
if (options.json) outputJsonError("Either name or id must be provided");
|
|
1417
|
-
logger$
|
|
1425
|
+
logger$3.error("Either name or id must be provided");
|
|
1418
1426
|
process.exit(1);
|
|
1419
1427
|
}
|
|
1420
1428
|
if (options.name && options.id) {
|
|
1421
1429
|
if (options.json) outputJsonError("Only one of name or id must be provided");
|
|
1422
|
-
logger$
|
|
1430
|
+
logger$3.error("Only one of name or id must be provided");
|
|
1423
1431
|
process.exit(1);
|
|
1424
1432
|
}
|
|
1425
1433
|
const client = new LaminarClient({
|
|
@@ -1436,12 +1444,12 @@ const handleDatasetsPull = async (outputPath, options) => {
|
|
|
1436
1444
|
path: outputPath,
|
|
1437
1445
|
count: result.length
|
|
1438
1446
|
});
|
|
1439
|
-
else logger$
|
|
1447
|
+
else logger$3.info(`Successfully pulled ${result.length} data points to ${outputPath}`);
|
|
1440
1448
|
} else if (options.json) outputJson(result);
|
|
1441
1449
|
else printToConsole(result, options.outputFormat ?? "json");
|
|
1442
1450
|
} catch (error) {
|
|
1443
1451
|
if (options.json) outputJsonError(error);
|
|
1444
|
-
logger$
|
|
1452
|
+
logger$3.error(`Failed to pull dataset: ${errorMessage(error)}`);
|
|
1445
1453
|
process.exit(1);
|
|
1446
1454
|
}
|
|
1447
1455
|
};
|
|
@@ -1458,23 +1466,23 @@ const handleDatasetsCreate = async (name, paths, options) => {
|
|
|
1458
1466
|
const data = await loadFromPaths(paths, options.recursive);
|
|
1459
1467
|
if (data.length === 0) {
|
|
1460
1468
|
if (options.json) outputJsonError("No data to push");
|
|
1461
|
-
logger$
|
|
1469
|
+
logger$3.error("No data to push. Skipping");
|
|
1462
1470
|
process.exit(1);
|
|
1463
1471
|
}
|
|
1464
|
-
logger$
|
|
1472
|
+
logger$3.info(`Pushing ${data.length} data points to dataset '${name}'...`);
|
|
1465
1473
|
await client.datasets.push({
|
|
1466
1474
|
points: data,
|
|
1467
1475
|
name,
|
|
1468
1476
|
batchSize: options.batchSize ?? DEFAULT_DATASET_PUSH_BATCH_SIZE,
|
|
1469
1477
|
createDataset: true
|
|
1470
1478
|
});
|
|
1471
|
-
logger$
|
|
1479
|
+
logger$3.info(`Successfully pushed ${data.length} data points to dataset '${name}'`);
|
|
1472
1480
|
} catch (error) {
|
|
1473
1481
|
if (options.json) outputJsonError(error);
|
|
1474
|
-
logger$
|
|
1482
|
+
logger$3.error(`Failed to create dataset: ${errorMessage(error)}`);
|
|
1475
1483
|
process.exit(1);
|
|
1476
1484
|
}
|
|
1477
|
-
logger$
|
|
1485
|
+
logger$3.info(`Pulling data from dataset '${name}'...`);
|
|
1478
1486
|
try {
|
|
1479
1487
|
const result = await pullAllData(client, { name }, options.batchSize ?? DEFAULT_DATASET_PULL_BATCH_SIZE, 0, void 0);
|
|
1480
1488
|
await writeToFile(options.outputFile, result, options.outputFormat);
|
|
@@ -1483,1072 +1491,129 @@ const handleDatasetsCreate = async (name, paths, options) => {
|
|
|
1483
1491
|
path: options.outputFile,
|
|
1484
1492
|
count: result.length
|
|
1485
1493
|
});
|
|
1486
|
-
else logger$
|
|
1494
|
+
else logger$3.info(`Successfully created dataset '${name}' and saved ${result.length} datapoints to ${options.outputFile}`);
|
|
1487
1495
|
} catch (error) {
|
|
1488
1496
|
if (options.json) outputJsonError(error);
|
|
1489
|
-
logger$
|
|
1497
|
+
logger$3.error("Failed to pull dataset after creation: " + errorMessage(error));
|
|
1490
1498
|
process.exit(1);
|
|
1491
1499
|
}
|
|
1492
1500
|
};
|
|
1493
|
-
|
|
1494
|
-
//#endregion
|
|
1495
|
-
//#region src/cache-server.ts
|
|
1496
|
-
const DEFAULT_START_PORT = 35667;
|
|
1497
|
-
/**
|
|
1498
|
-
* Finds an available port starting from the given port number
|
|
1499
|
-
*/
|
|
1500
|
-
async function findAvailablePort(startPort) {
|
|
1501
|
-
return new Promise((resolve, reject) => {
|
|
1502
|
-
const server = http.createServer();
|
|
1503
|
-
server.listen(startPort, () => {
|
|
1504
|
-
const port = server.address().port;
|
|
1505
|
-
server.close(() => resolve(port));
|
|
1506
|
-
});
|
|
1507
|
-
server.on("error", (err) => {
|
|
1508
|
-
if (err.code === "EADDRINUSE") resolve(findAvailablePort(startPort + 1));
|
|
1509
|
-
else reject(err);
|
|
1510
|
-
});
|
|
1511
|
-
});
|
|
1512
|
-
}
|
|
1513
|
-
/**
|
|
1514
|
-
* Parses request body as JSON
|
|
1515
|
-
*/
|
|
1516
|
-
function parseBody(req) {
|
|
1517
|
-
return new Promise((resolve, reject) => {
|
|
1518
|
-
let body = "";
|
|
1519
|
-
req.on("data", (chunk) => {
|
|
1520
|
-
body += chunk.toString();
|
|
1521
|
-
});
|
|
1522
|
-
req.on("end", () => {
|
|
1523
|
-
try {
|
|
1524
|
-
resolve(body ? JSON.parse(body) : {});
|
|
1525
|
-
} catch (err) {
|
|
1526
|
-
reject(/* @__PURE__ */ new Error(`Invalid JSON: ${err instanceof Error ? err.message : String(err)}`));
|
|
1527
|
-
}
|
|
1528
|
-
});
|
|
1529
|
-
req.on("error", reject);
|
|
1530
|
-
});
|
|
1531
|
-
}
|
|
1532
|
-
/**
|
|
1533
|
-
* Starts a local cache server for storing and retrieving cached LLM responses
|
|
1534
|
-
* during rollout debugging sessions.
|
|
1535
|
-
*
|
|
1536
|
-
* @param startPort - Optional starting port number (defaults to 35667)
|
|
1537
|
-
* @returns Server information including port, server instance, cache, and metadata setter
|
|
1538
|
-
*/
|
|
1539
|
-
async function startCacheServer(startPort = DEFAULT_START_PORT) {
|
|
1540
|
-
const cache = /* @__PURE__ */ new Map();
|
|
1541
|
-
let metadata = {
|
|
1542
|
-
pathToCount: {},
|
|
1543
|
-
overrides: void 0
|
|
1544
|
-
};
|
|
1545
|
-
const server = http.createServer((req, res) => {
|
|
1546
|
-
(async () => {
|
|
1547
|
-
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
1548
|
-
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
1549
|
-
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
1550
|
-
if (req.method === "OPTIONS") {
|
|
1551
|
-
res.writeHead(200);
|
|
1552
|
-
res.end();
|
|
1553
|
-
return;
|
|
1554
|
-
}
|
|
1555
|
-
if (req.method === "GET" && req.url === "/health") {
|
|
1556
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1557
|
-
res.end(JSON.stringify({ status: "ok" }));
|
|
1558
|
-
return;
|
|
1559
|
-
}
|
|
1560
|
-
if (req.method === "POST" && req.url === "/cached") {
|
|
1561
|
-
try {
|
|
1562
|
-
const { path: path$2, index } = await parseBody(req);
|
|
1563
|
-
if (typeof path$2 !== "string" || typeof index !== "number") {
|
|
1564
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1565
|
-
res.end(JSON.stringify({ error: "Invalid request: path (string) and index (number) required" }));
|
|
1566
|
-
return;
|
|
1567
|
-
}
|
|
1568
|
-
const cacheKey = `${index}:${path$2}`;
|
|
1569
|
-
const response = {
|
|
1570
|
-
span: cache.get(cacheKey),
|
|
1571
|
-
pathToCount: metadata.pathToCount,
|
|
1572
|
-
overrides: metadata.overrides
|
|
1573
|
-
};
|
|
1574
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1575
|
-
res.end(JSON.stringify(response));
|
|
1576
|
-
} catch (err) {
|
|
1577
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1578
|
-
res.end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }));
|
|
1579
|
-
}
|
|
1580
|
-
return;
|
|
1581
|
-
}
|
|
1582
|
-
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1583
|
-
res.end(JSON.stringify({ error: "Not found" }));
|
|
1584
|
-
})().catch((error) => {
|
|
1585
|
-
if (!res.headersSent) {
|
|
1586
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1587
|
-
res.end(JSON.stringify({ error: error instanceof Error ? error.message : "Internal server error" }));
|
|
1588
|
-
}
|
|
1589
|
-
});
|
|
1590
|
-
});
|
|
1591
|
-
const port = await findAvailablePort(startPort);
|
|
1592
|
-
return new Promise((resolve, reject) => {
|
|
1593
|
-
server.listen(port, () => {
|
|
1594
|
-
resolve({
|
|
1595
|
-
port,
|
|
1596
|
-
server,
|
|
1597
|
-
cache,
|
|
1598
|
-
setMetadata: (newMetadata) => {
|
|
1599
|
-
metadata = newMetadata;
|
|
1600
|
-
}
|
|
1601
|
-
});
|
|
1602
|
-
});
|
|
1603
|
-
server.on("error", reject);
|
|
1604
|
-
});
|
|
1605
|
-
}
|
|
1606
|
-
|
|
1607
|
-
//#endregion
|
|
1608
|
-
//#region src/sse-client.ts
|
|
1609
|
-
const HEARTBEAT_INTERVAL = 5e3;
|
|
1610
|
-
const MAX_MISSED_HEARTBEATS = 3;
|
|
1611
|
-
/**
|
|
1612
|
-
* SSE client for rollout debugging sessions
|
|
1613
|
-
* Connects to the Laminar backend and listens for run events
|
|
1614
|
-
*/
|
|
1615
|
-
var SSEClient = class extends events.EventEmitter {
|
|
1616
|
-
constructor(options) {
|
|
1617
|
-
super();
|
|
1618
|
-
this.lastHeartbeat = Date.now();
|
|
1619
|
-
this.isShutdown = false;
|
|
1620
|
-
this.client = options.client;
|
|
1621
|
-
this.sessionId = options.sessionId;
|
|
1622
|
-
this.params = options.params;
|
|
1623
|
-
this.name = options.name;
|
|
1624
|
-
}
|
|
1625
|
-
/**
|
|
1626
|
-
* Connects to the SSE endpoint
|
|
1627
|
-
*/
|
|
1628
|
-
async connectAndListen() {
|
|
1629
|
-
if (this.isShutdown) return;
|
|
1630
|
-
this.abortController = new AbortController();
|
|
1631
|
-
this.lastHeartbeat = Date.now();
|
|
1632
|
-
try {
|
|
1633
|
-
const response = await this.client.rolloutSessions.connect({
|
|
1634
|
-
sessionId: this.sessionId,
|
|
1635
|
-
params: this.params,
|
|
1636
|
-
signal: this.abortController.signal,
|
|
1637
|
-
name: this.name
|
|
1638
|
-
});
|
|
1639
|
-
this.emit("connected");
|
|
1640
|
-
this.startHeartbeatCheck();
|
|
1641
|
-
await this.parseSSEStream(response.body);
|
|
1642
|
-
} catch (error) {
|
|
1643
|
-
if (error.name === "AbortError") return;
|
|
1644
|
-
this.emit("error", error);
|
|
1645
|
-
if (!this.isShutdown) this.scheduleReconnect();
|
|
1646
|
-
}
|
|
1647
|
-
}
|
|
1648
|
-
/**
|
|
1649
|
-
* Parses SSE stream and emits events
|
|
1650
|
-
*/
|
|
1651
|
-
async parseSSEStream(body) {
|
|
1652
|
-
const reader = body.getReader();
|
|
1653
|
-
const decoder = new TextDecoder();
|
|
1654
|
-
const parser = (0, eventsource_parser.createParser)({ onEvent: (event) => {
|
|
1655
|
-
this.processSSEEvent(event);
|
|
1656
|
-
} });
|
|
1657
|
-
try {
|
|
1658
|
-
while (true) {
|
|
1659
|
-
const { done, value } = await reader.read();
|
|
1660
|
-
if (done) break;
|
|
1661
|
-
const chunk = decoder.decode(value, { stream: true });
|
|
1662
|
-
parser.feed(chunk);
|
|
1663
|
-
}
|
|
1664
|
-
} finally {
|
|
1665
|
-
reader.releaseLock();
|
|
1666
|
-
}
|
|
1667
|
-
if (!this.isShutdown) this.scheduleReconnect();
|
|
1668
|
-
}
|
|
1669
|
-
/**
|
|
1670
|
-
* Processes a parsed SSE event
|
|
1671
|
-
*/
|
|
1672
|
-
processSSEEvent(event) {
|
|
1673
|
-
if (!event.data) return;
|
|
1674
|
-
try {
|
|
1675
|
-
if (event.event === "heartbeat") {
|
|
1676
|
-
this.lastHeartbeat = Date.now();
|
|
1677
|
-
this.emit("heartbeat");
|
|
1678
|
-
} else if (event.event === "run") {
|
|
1679
|
-
const runEvent = {
|
|
1680
|
-
event_type: "run",
|
|
1681
|
-
data: JSON.parse(event.data)
|
|
1682
|
-
};
|
|
1683
|
-
this.emit("run", runEvent);
|
|
1684
|
-
} else if (event.event === "handshake") {
|
|
1685
|
-
const handshakeEvent = {
|
|
1686
|
-
event_type: "handshake",
|
|
1687
|
-
data: JSON.parse(event.data)
|
|
1688
|
-
};
|
|
1689
|
-
this.emit("handshake", handshakeEvent);
|
|
1690
|
-
} else if (event.event === "stop") this.emit("stop");
|
|
1691
|
-
} catch (error) {
|
|
1692
|
-
this.emit("error", /* @__PURE__ */ new Error(`Failed to parse SSE event data: ${error}`));
|
|
1693
|
-
}
|
|
1694
|
-
}
|
|
1695
|
-
/**
|
|
1696
|
-
* Starts checking for missed heartbeats
|
|
1697
|
-
*/
|
|
1698
|
-
startHeartbeatCheck() {
|
|
1699
|
-
this.stopHeartbeatCheck();
|
|
1700
|
-
this.heartbeatCheckTimer = setInterval(() => {
|
|
1701
|
-
if (Date.now() - this.lastHeartbeat > HEARTBEAT_INTERVAL * MAX_MISSED_HEARTBEATS) {
|
|
1702
|
-
this.emit("heartbeat_timeout");
|
|
1703
|
-
this.reconnect();
|
|
1704
|
-
}
|
|
1705
|
-
}, HEARTBEAT_INTERVAL);
|
|
1706
|
-
}
|
|
1707
|
-
/**
|
|
1708
|
-
* Stops heartbeat checking
|
|
1709
|
-
*/
|
|
1710
|
-
stopHeartbeatCheck() {
|
|
1711
|
-
if (this.heartbeatCheckTimer) {
|
|
1712
|
-
clearInterval(this.heartbeatCheckTimer);
|
|
1713
|
-
this.heartbeatCheckTimer = void 0;
|
|
1714
|
-
}
|
|
1715
|
-
}
|
|
1716
|
-
/**
|
|
1717
|
-
* Schedules a reconnection attempt
|
|
1718
|
-
*/
|
|
1719
|
-
scheduleReconnect() {
|
|
1720
|
-
if (this.reconnectTimer || this.isShutdown) return;
|
|
1721
|
-
this.emit("reconnecting");
|
|
1722
|
-
this.reconnectTimer = setTimeout(() => {
|
|
1723
|
-
this.reconnectTimer = void 0;
|
|
1724
|
-
this.reconnect();
|
|
1725
|
-
}, 1e3);
|
|
1726
|
-
}
|
|
1727
|
-
/**
|
|
1728
|
-
* Reconnects to the SSE endpoint
|
|
1729
|
-
*/
|
|
1730
|
-
reconnect() {
|
|
1731
|
-
this.disconnect(true);
|
|
1732
|
-
this.connectAndListen().catch((error) => {
|
|
1733
|
-
this.emit("error", error);
|
|
1734
|
-
});
|
|
1735
|
-
}
|
|
1736
|
-
/**
|
|
1737
|
-
* Disconnects from the SSE endpoint
|
|
1738
|
-
*/
|
|
1739
|
-
disconnect(stopReconnect = true) {
|
|
1740
|
-
if (this.abortController) {
|
|
1741
|
-
this.abortController.abort();
|
|
1742
|
-
this.abortController = void 0;
|
|
1743
|
-
}
|
|
1744
|
-
this.stopHeartbeatCheck();
|
|
1745
|
-
if (stopReconnect && this.reconnectTimer) {
|
|
1746
|
-
clearTimeout(this.reconnectTimer);
|
|
1747
|
-
this.reconnectTimer = void 0;
|
|
1748
|
-
}
|
|
1749
|
-
}
|
|
1750
|
-
/**
|
|
1751
|
-
* Updates the function metadata (params, name) and reconnects
|
|
1752
|
-
*/
|
|
1753
|
-
updateMetadata(params, name) {
|
|
1754
|
-
this.params = params;
|
|
1755
|
-
this.name = name;
|
|
1756
|
-
this.reconnect();
|
|
1757
|
-
}
|
|
1758
|
-
/**
|
|
1759
|
-
* Shuts down the SSE client gracefully
|
|
1760
|
-
*/
|
|
1761
|
-
shutdown() {
|
|
1762
|
-
this.isShutdown = true;
|
|
1763
|
-
this.disconnect(true);
|
|
1764
|
-
this.emit("shutdown");
|
|
1765
|
-
this.removeAllListeners();
|
|
1766
|
-
}
|
|
1767
|
-
};
|
|
1768
|
-
/**
|
|
1769
|
-
* Creates an SSE client (does not auto-connect)
|
|
1770
|
-
* Call client.connect() after registering event listeners
|
|
1771
|
-
*/
|
|
1772
|
-
function createSSEClient(options) {
|
|
1773
|
-
return new SSEClient(options);
|
|
1774
|
-
}
|
|
1775
|
-
|
|
1776
|
-
//#endregion
|
|
1777
|
-
//#region ../types/dist/index.mjs
|
|
1778
|
-
/**
|
|
1779
|
-
* Message prefix for protocol messages in stdout
|
|
1780
|
-
*/
|
|
1781
|
-
const WORKER_MESSAGE_PREFIX = "__LMNR_WORKER__:";
|
|
1782
|
-
|
|
1783
|
-
//#endregion
|
|
1784
|
-
//#region src/subprocess/executor.ts
|
|
1785
|
-
const logger$3 = initializeLogger();
|
|
1786
|
-
/**
|
|
1787
|
-
* Track and kill the currently running subprocess
|
|
1788
|
-
*/
|
|
1789
|
-
var SubprocessManager = class {
|
|
1790
|
-
constructor() {
|
|
1791
|
-
this.currentProcess = null;
|
|
1792
|
-
}
|
|
1793
|
-
/**
|
|
1794
|
-
* Execute a subprocess and track it
|
|
1795
|
-
*/
|
|
1796
|
-
async execute(options) {
|
|
1797
|
-
const { command, args, config: config$1 } = options;
|
|
1798
|
-
const child = (0, child_process.spawn)(command, args, { stdio: [
|
|
1799
|
-
"pipe",
|
|
1800
|
-
"pipe",
|
|
1801
|
-
"pipe"
|
|
1802
|
-
] });
|
|
1803
|
-
this.currentProcess = child;
|
|
1804
|
-
return new Promise((resolve, reject) => {
|
|
1805
|
-
const result = void 0;
|
|
1806
|
-
let hasError = false;
|
|
1807
|
-
readline.createInterface({
|
|
1808
|
-
input: child.stdout,
|
|
1809
|
-
crlfDelay: Infinity
|
|
1810
|
-
}).on("line", (line) => {
|
|
1811
|
-
if (line.startsWith(WORKER_MESSAGE_PREFIX)) try {
|
|
1812
|
-
const messageJson = line.substring(WORKER_MESSAGE_PREFIX.length);
|
|
1813
|
-
const message = JSON.parse(messageJson);
|
|
1814
|
-
switch (message.type) {
|
|
1815
|
-
case "log":
|
|
1816
|
-
logger$3[message.level](message.message);
|
|
1817
|
-
break;
|
|
1818
|
-
case "error":
|
|
1819
|
-
hasError = true;
|
|
1820
|
-
logger$3.error(`Worker error: ${message.error}`);
|
|
1821
|
-
if (message.stack) logger$3.error(message.stack);
|
|
1822
|
-
break;
|
|
1823
|
-
}
|
|
1824
|
-
} catch {
|
|
1825
|
-
logger$3.debug("Failed to parse worker protocol message. Printing raw line");
|
|
1826
|
-
console.log(line.substring(WORKER_MESSAGE_PREFIX.length));
|
|
1827
|
-
}
|
|
1828
|
-
else console.log(line);
|
|
1829
|
-
});
|
|
1830
|
-
child.stderr.on("data", (data) => {
|
|
1831
|
-
process.stderr.write(data);
|
|
1832
|
-
});
|
|
1833
|
-
child.on("exit", (code, signal) => {
|
|
1834
|
-
if (this.currentProcess?.pid === child.pid) this.currentProcess = null;
|
|
1835
|
-
if (signal) reject(/* @__PURE__ */ new Error(`Worker terminated by signal: ${signal}`));
|
|
1836
|
-
else if (code === 0) resolve(result);
|
|
1837
|
-
else {
|
|
1838
|
-
if (!hasError) logger$3.error(`Worker exited with code ${code}`);
|
|
1839
|
-
reject(/* @__PURE__ */ new Error(`Worker exited with code ${code}`));
|
|
1840
|
-
}
|
|
1841
|
-
});
|
|
1842
|
-
child.on("error", (error) => {
|
|
1843
|
-
this.currentProcess = null;
|
|
1844
|
-
reject(/* @__PURE__ */ new Error(`Failed to spawn worker: ${error.message}`));
|
|
1845
|
-
});
|
|
1846
|
-
child.stdin?.write(JSON.stringify(config$1) + "\n");
|
|
1847
|
-
child.stdin?.end();
|
|
1848
|
-
});
|
|
1849
|
-
}
|
|
1850
|
-
/**
|
|
1851
|
-
* Kill the currently running subprocess
|
|
1852
|
-
* @returns true if a process was killed, false if no process was running
|
|
1853
|
-
*/
|
|
1854
|
-
kill() {
|
|
1855
|
-
if (this.currentProcess) {
|
|
1856
|
-
const processToKill = this.currentProcess;
|
|
1857
|
-
this.currentProcess.kill("SIGTERM");
|
|
1858
|
-
setTimeout(() => {
|
|
1859
|
-
if (processToKill && processToKill.exitCode === null) {
|
|
1860
|
-
logger$3.warn("Child process did not terminate, using SIGKILL");
|
|
1861
|
-
processToKill.kill("SIGKILL");
|
|
1862
|
-
}
|
|
1863
|
-
}, 5e3);
|
|
1864
|
-
return true;
|
|
1865
|
-
}
|
|
1866
|
-
return false;
|
|
1867
|
-
}
|
|
1868
|
-
/**
|
|
1869
|
-
* Check if a subprocess is currently running
|
|
1870
|
-
*/
|
|
1871
|
-
isRunning() {
|
|
1872
|
-
return this.currentProcess !== null;
|
|
1873
|
-
}
|
|
1874
|
-
};
|
|
1875
|
-
|
|
1876
1501
|
//#endregion
|
|
1877
|
-
//#region src/
|
|
1502
|
+
//#region src/utils/trace-note.ts
|
|
1503
|
+
const NOTE_METADATA_KEY = "rollout.note";
|
|
1878
1504
|
/**
|
|
1879
|
-
*
|
|
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.
|
|
1880
1508
|
*/
|
|
1881
|
-
const
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
}
|
|
1886
|
-
".cts": {
|
|
1887
|
-
command: "node",
|
|
1888
|
-
args: []
|
|
1889
|
-
},
|
|
1890
|
-
".mts": {
|
|
1891
|
-
command: "node",
|
|
1892
|
-
args: []
|
|
1893
|
-
},
|
|
1894
|
-
".tsx": {
|
|
1895
|
-
command: "node",
|
|
1896
|
-
args: []
|
|
1897
|
-
},
|
|
1898
|
-
".jsx": {
|
|
1899
|
-
command: "node",
|
|
1900
|
-
args: []
|
|
1901
|
-
},
|
|
1902
|
-
".js": {
|
|
1903
|
-
command: "node",
|
|
1904
|
-
args: []
|
|
1905
|
-
},
|
|
1906
|
-
".mjs": {
|
|
1907
|
-
command: "node",
|
|
1908
|
-
args: []
|
|
1909
|
-
},
|
|
1910
|
-
".cjs": {
|
|
1911
|
-
command: "node",
|
|
1912
|
-
args: []
|
|
1913
|
-
},
|
|
1914
|
-
".py": {
|
|
1915
|
-
command: "python3",
|
|
1916
|
-
args: ["-m", "lmnr.cli.worker"]
|
|
1917
|
-
}
|
|
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.`);
|
|
1918
1514
|
};
|
|
1919
1515
|
/**
|
|
1920
|
-
*
|
|
1921
|
-
*
|
|
1922
|
-
|
|
1923
|
-
function getWorkerCommand(filePath, options) {
|
|
1924
|
-
if (options?.pythonModule) return {
|
|
1925
|
-
command: "python3",
|
|
1926
|
-
args: ["-m", "lmnr.cli.worker"]
|
|
1927
|
-
};
|
|
1928
|
-
if (!filePath) throw new Error("Either filePath or pythonModule must be provided");
|
|
1929
|
-
const ext = path.extname(filePath);
|
|
1930
|
-
if (!DEFAULT_WORKERS[ext]) throw new Error(`Unsupported file extension: ${ext}. Supported extensions: ${Object.keys(DEFAULT_WORKERS).join(", ")}`);
|
|
1931
|
-
const worker = DEFAULT_WORKERS[ext];
|
|
1932
|
-
if ([
|
|
1933
|
-
".ts",
|
|
1934
|
-
".tsx",
|
|
1935
|
-
".js",
|
|
1936
|
-
".mjs",
|
|
1937
|
-
".cjs",
|
|
1938
|
-
".mts",
|
|
1939
|
-
".cts",
|
|
1940
|
-
".jsx"
|
|
1941
|
-
].includes(ext)) try {
|
|
1942
|
-
const workerPath = require.resolve("@lmnr-ai/lmnr/dist/cli/worker/index.cjs");
|
|
1943
|
-
return {
|
|
1944
|
-
command: worker.command,
|
|
1945
|
-
args: [workerPath]
|
|
1946
|
-
};
|
|
1947
|
-
} catch (error) {
|
|
1948
|
-
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)}`);
|
|
1949
|
-
}
|
|
1950
|
-
return worker;
|
|
1951
|
-
}
|
|
1952
|
-
|
|
1953
|
-
//#endregion
|
|
1954
|
-
//#region src/commands/dev/metadata.ts
|
|
1955
|
-
const logger$2 = initializeLogger();
|
|
1956
|
-
const TS_JS_EXTENSIONS = [
|
|
1957
|
-
".ts",
|
|
1958
|
-
".tsx",
|
|
1959
|
-
".js",
|
|
1960
|
-
".mjs",
|
|
1961
|
-
".cjs",
|
|
1962
|
-
".jsx",
|
|
1963
|
-
".mts",
|
|
1964
|
-
".cts"
|
|
1965
|
-
];
|
|
1966
|
-
const EXTENSIONS_TO_DISCOVER_METADATA = [...TS_JS_EXTENSIONS, ".py"];
|
|
1967
|
-
/**
|
|
1968
|
-
* Protocol prefix for metadata discovery responses
|
|
1969
|
-
* This allows us to safely parse JSON even if there are other log statements in stdout
|
|
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".
|
|
1970
1519
|
*/
|
|
1971
|
-
const
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
};
|
|
1976
|
-
/**
|
|
1977
|
-
* Discovers function metadata for TypeScript and JavaScript files by:
|
|
1978
|
-
* 1. Extracting TypeScript metadata (params with types from source - TS only)
|
|
1979
|
-
* 2. Building and loading the module with esbuild
|
|
1980
|
-
* 3. Selecting the appropriate function
|
|
1981
|
-
* 4. Matching metadata by span name
|
|
1982
|
-
*
|
|
1983
|
-
* For JavaScript files, TypeScript metadata extraction fails gracefully, but runtime
|
|
1984
|
-
* parameter extraction via regex still works (param names without types).
|
|
1985
|
-
*/
|
|
1986
|
-
const discoverTypeScriptMetadata = async (filePath, options) => {
|
|
1987
|
-
let extractRolloutFunctions;
|
|
1988
|
-
let buildFile;
|
|
1989
|
-
let loadModule;
|
|
1990
|
-
let selectRolloutFunction;
|
|
1991
|
-
try {
|
|
1992
|
-
const lmnrPackage = "@lmnr-ai/lmnr";
|
|
1993
|
-
const tsParserPath = require.resolve(`${lmnrPackage}/dist/cli/worker/ts-parser.cjs`);
|
|
1994
|
-
const buildModulePath = require.resolve(`${lmnrPackage}/dist/cli/worker/build.cjs`);
|
|
1995
|
-
delete require.cache[tsParserPath];
|
|
1996
|
-
delete require.cache[buildModulePath];
|
|
1997
|
-
extractRolloutFunctions = require(tsParserPath).extractRolloutFunctions;
|
|
1998
|
-
const buildModule = require(buildModulePath);
|
|
1999
|
-
buildFile = buildModule.buildFile;
|
|
2000
|
-
loadModule = buildModule.loadModule;
|
|
2001
|
-
selectRolloutFunction = buildModule.selectRolloutFunction;
|
|
2002
|
-
if (!extractRolloutFunctions || !buildFile || !loadModule || !selectRolloutFunction) {
|
|
2003
|
-
logger$2.error("Missing exports from @lmnr-ai/lmnr modules. This may indicate an outdated package version.");
|
|
2004
|
-
logLmnrPackageNotFoundAndExit();
|
|
2005
|
-
}
|
|
2006
|
-
} catch (error) {
|
|
2007
|
-
if (error.code === "MODULE_NOT_FOUND") logLmnrPackageNotFoundAndExit();
|
|
2008
|
-
logger$2.error(`Unexpected error loading @lmnr-ai/lmnr modules: ${error.message}`);
|
|
2009
|
-
throw error;
|
|
2010
|
-
}
|
|
2011
|
-
let paramsMetadata;
|
|
2012
|
-
try {
|
|
2013
|
-
paramsMetadata = extractRolloutFunctions(filePath);
|
|
2014
|
-
logger$2.debug(`Extracted TypeScript metadata for ${paramsMetadata?.size} functions`);
|
|
2015
|
-
} catch (error) {
|
|
2016
|
-
logger$2.warn("Failed to extract TypeScript metadata, falling back to runtime parsing: " + (error instanceof Error ? error.message : String(error)));
|
|
2017
|
-
}
|
|
2018
|
-
const moduleText = await buildFile(filePath, {
|
|
2019
|
-
externalPackages: options.externalPackages,
|
|
2020
|
-
dynamicImportsToSkip: options.dynamicImportsToSkip
|
|
2021
|
-
});
|
|
2022
|
-
loadModule({
|
|
2023
|
-
filename: filePath,
|
|
2024
|
-
moduleText
|
|
2025
|
-
});
|
|
2026
|
-
const selectedFunction = selectRolloutFunction(options.function);
|
|
2027
|
-
if (paramsMetadata) {
|
|
2028
|
-
logger$2.debug(`Available TS metadata keys: ${Array.from(paramsMetadata.keys()).join(", ")}`);
|
|
2029
|
-
logger$2.debug(`Looking for span name: ${selectedFunction.name} (runtime key: ${selectedFunction.exportName})`);
|
|
2030
|
-
let foundMetadata = null;
|
|
2031
|
-
for (const [exportName, metadata] of paramsMetadata.entries()) {
|
|
2032
|
-
logger$2.debug(`Checking ${exportName}: span name = ${metadata.name}, export name = ${exportName}`);
|
|
2033
|
-
if (metadata.name === selectedFunction.name) {
|
|
2034
|
-
foundMetadata = metadata;
|
|
2035
|
-
logger$2.debug(`Match. Export name: ${exportName}, span name: ${metadata.name}`);
|
|
2036
|
-
break;
|
|
2037
|
-
}
|
|
2038
|
-
}
|
|
2039
|
-
if (foundMetadata) {
|
|
2040
|
-
selectedFunction.params = foundMetadata.params;
|
|
2041
|
-
logger$2.debug(`Using TypeScript metadata for span: ${selectedFunction.name}`);
|
|
2042
|
-
} else logger$2.info(`No TypeScript metadata found for span name: ${selectedFunction.name}`);
|
|
2043
|
-
}
|
|
2044
|
-
return {
|
|
2045
|
-
functionName: selectedFunction.name,
|
|
2046
|
-
params: selectedFunction.params || []
|
|
2047
|
-
};
|
|
2048
|
-
};
|
|
2049
|
-
/**
|
|
2050
|
-
* Helper to execute subprocess commands
|
|
2051
|
-
*/
|
|
2052
|
-
const execCommand = async (command, args) => new Promise((resolve, reject) => {
|
|
2053
|
-
const { spawn: spawn$1 } = require("child_process");
|
|
2054
|
-
const child = spawn$1(command, args);
|
|
2055
|
-
let stdout = "";
|
|
2056
|
-
let stderr = "";
|
|
2057
|
-
child.stdout.on("data", (data) => {
|
|
2058
|
-
stdout += data.toString();
|
|
2059
|
-
});
|
|
2060
|
-
child.stderr.on("data", (data) => {
|
|
2061
|
-
stderr += data.toString();
|
|
2062
|
-
});
|
|
2063
|
-
child.on("close", (code) => {
|
|
2064
|
-
if (code === 0) resolve({
|
|
2065
|
-
stdout,
|
|
2066
|
-
stderr
|
|
2067
|
-
});
|
|
2068
|
-
else reject(/* @__PURE__ */ new Error(`Command failed with code ${code}: ${stderr}`));
|
|
2069
|
-
});
|
|
2070
|
-
child.on("error", (error) => {
|
|
2071
|
-
reject(error);
|
|
2072
|
-
});
|
|
2073
|
-
});
|
|
2074
|
-
/**
|
|
2075
|
-
* Extracts JSON metadata from stdout that may contain other log statements
|
|
2076
|
-
* Looks for lines matching the protocol prefix and parses the JSON payload
|
|
2077
|
-
*
|
|
2078
|
-
* @param stdout - Raw stdout output that may contain logs and metadata
|
|
2079
|
-
* @returns Parsed JSON object
|
|
2080
|
-
* @throws Error if no valid metadata line is found or JSON parsing fails
|
|
2081
|
-
*/
|
|
2082
|
-
const extractMetadataFromStdout = (stdout) => {
|
|
2083
|
-
const prefixPositions = [];
|
|
2084
|
-
let searchStart = 0;
|
|
2085
|
-
while (true) {
|
|
2086
|
-
const pos = stdout.indexOf(METADATA_PROTOCOL_PREFIX, searchStart);
|
|
2087
|
-
if (pos === -1) break;
|
|
2088
|
-
prefixPositions.push(pos);
|
|
2089
|
-
searchStart = pos + 14;
|
|
2090
|
-
}
|
|
2091
|
-
if (prefixPositions.length === 0) try {
|
|
2092
|
-
return JSON.parse(stdout.trim());
|
|
2093
|
-
} catch {
|
|
2094
|
-
throw new Error("No metadata found in output. Please make sure you are running the latest version of `lmnr` python package.");
|
|
2095
|
-
}
|
|
2096
|
-
let lastValidJson = null;
|
|
2097
|
-
for (const pos of prefixPositions) {
|
|
2098
|
-
const startPos = pos + 14;
|
|
2099
|
-
const jsonText = stdout.slice(startPos).trim();
|
|
2100
|
-
const nextNewline = stdout.indexOf("\n", startPos);
|
|
2101
|
-
if (nextNewline !== -1) {
|
|
2102
|
-
const lineText = stdout.slice(startPos, nextNewline).trim();
|
|
2103
|
-
try {
|
|
2104
|
-
lastValidJson = JSON.parse(lineText);
|
|
2105
|
-
continue;
|
|
2106
|
-
} catch {}
|
|
2107
|
-
}
|
|
1520
|
+
const readNoteFromMetadata = (metadata) => {
|
|
1521
|
+
let parsed = metadata;
|
|
1522
|
+
if (typeof metadata === "string") {
|
|
1523
|
+
if (metadata === "") return "";
|
|
2108
1524
|
try {
|
|
2109
|
-
|
|
2110
|
-
let inString = false;
|
|
2111
|
-
let escapeNext = false;
|
|
2112
|
-
let firstChar = -1;
|
|
2113
|
-
for (let i = 0; i < jsonText.length; i++) {
|
|
2114
|
-
const char = jsonText[i];
|
|
2115
|
-
if (escapeNext) {
|
|
2116
|
-
escapeNext = false;
|
|
2117
|
-
continue;
|
|
2118
|
-
}
|
|
2119
|
-
if (char === "\\" && inString) {
|
|
2120
|
-
escapeNext = true;
|
|
2121
|
-
continue;
|
|
2122
|
-
}
|
|
2123
|
-
if (char === "\"") {
|
|
2124
|
-
inString = !inString;
|
|
2125
|
-
continue;
|
|
2126
|
-
}
|
|
2127
|
-
if (inString) continue;
|
|
2128
|
-
if (char === "{" || char === "[") {
|
|
2129
|
-
if (firstChar === -1) firstChar = i;
|
|
2130
|
-
depth++;
|
|
2131
|
-
} else if (char === "}" || char === "]") {
|
|
2132
|
-
depth--;
|
|
2133
|
-
if (depth === 0 && firstChar !== -1) {
|
|
2134
|
-
const candidate = jsonText.slice(0, i + 1);
|
|
2135
|
-
lastValidJson = JSON.parse(candidate);
|
|
2136
|
-
break;
|
|
2137
|
-
}
|
|
2138
|
-
}
|
|
2139
|
-
}
|
|
2140
|
-
if (depth !== 0 || firstChar === -1) lastValidJson = JSON.parse(jsonText);
|
|
1525
|
+
parsed = JSON.parse(metadata);
|
|
2141
1526
|
} catch {
|
|
2142
|
-
|
|
1527
|
+
return "";
|
|
2143
1528
|
}
|
|
2144
1529
|
}
|
|
2145
|
-
if (
|
|
2146
|
-
|
|
1530
|
+
if (typeof parsed !== "object" || parsed === null) return "";
|
|
1531
|
+
const note = parsed[NOTE_METADATA_KEY];
|
|
1532
|
+
return typeof note === "string" ? note : "";
|
|
2147
1533
|
};
|
|
2148
|
-
/**
|
|
2149
|
-
* Discovers function metadata for Python files/modules by calling the lmnr Python CLI
|
|
2150
|
-
*/
|
|
2151
|
-
const discoverPythonMetadata = async (filePathOrModule, options) => {
|
|
2152
|
-
logger$2.debug(`Discovering Python metadata for ${filePathOrModule}`);
|
|
2153
|
-
const args = ["discover"];
|
|
2154
|
-
if (options.pythonModule) args.push("--module", options.pythonModule);
|
|
2155
|
-
else args.push("--file", filePathOrModule);
|
|
2156
|
-
if (options.function) args.push("--function", options.function);
|
|
2157
|
-
try {
|
|
2158
|
-
const response = extractMetadataFromStdout((await execCommand("lmnr", args)).stdout);
|
|
2159
|
-
return {
|
|
2160
|
-
functionName: response.name,
|
|
2161
|
-
params: response.params || []
|
|
2162
|
-
};
|
|
2163
|
-
} catch (error) {
|
|
2164
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2165
|
-
logger$2.error(`Error while loading Python file/module: ${errorMessage}`);
|
|
2166
|
-
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}"`);
|
|
2167
|
-
throw error;
|
|
2168
|
-
}
|
|
2169
|
-
};
|
|
2170
|
-
/**
|
|
2171
|
-
* Generic metadata discovery dispatcher that routes to language-specific implementations
|
|
2172
|
-
*/
|
|
2173
|
-
const discoverFunctionMetadata = async (filePathOrModule, options) => {
|
|
2174
|
-
if (options.pythonModule) return await discoverPythonMetadata(filePathOrModule, options);
|
|
2175
|
-
const ext = path.extname(filePathOrModule);
|
|
2176
|
-
if (TS_JS_EXTENSIONS.includes(ext)) return await discoverTypeScriptMetadata(filePathOrModule, options);
|
|
2177
|
-
if (ext === ".py") return await discoverPythonMetadata(filePathOrModule, options);
|
|
2178
|
-
logger$2.warn(`No metadata discovery available for ${ext} files`);
|
|
2179
|
-
return {
|
|
2180
|
-
functionName: options.function || path.basename(filePathOrModule, ext),
|
|
2181
|
-
params: []
|
|
2182
|
-
};
|
|
2183
|
-
};
|
|
2184
|
-
|
|
2185
1534
|
//#endregion
|
|
2186
|
-
//#region src/commands/
|
|
2187
|
-
const logger$
|
|
2188
|
-
function newUUID() {
|
|
2189
|
-
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return crypto.randomUUID();
|
|
2190
|
-
return (0, uuid.v4)();
|
|
2191
|
-
}
|
|
2192
|
-
function getFrontendUrl(baseUrl, frontendPort) {
|
|
2193
|
-
let url = baseUrl ?? "https://api.lmnr.ai";
|
|
2194
|
-
if (url === "https://api.lmnr.ai") url = "https://www.laminar.sh";
|
|
2195
|
-
url = url.replace(/\/$/, "");
|
|
2196
|
-
if (/localhost|127\.0\.0\.1/.test(url)) {
|
|
2197
|
-
const port = frontendPort ?? url.match(/:\d{1,5}$/g)?.[0]?.slice(1) ?? 5667;
|
|
2198
|
-
url = url.replace(/:\d{1,5}$/g, "");
|
|
2199
|
-
return `${url}:${port}`;
|
|
2200
|
-
}
|
|
2201
|
-
return url;
|
|
2202
|
-
}
|
|
2203
|
-
/**
|
|
2204
|
-
* Parses request arguments, attempting JSON parse for strings
|
|
2205
|
-
*/
|
|
2206
|
-
const tryParseArg = (arg) => {
|
|
2207
|
-
if (typeof arg === "string") try {
|
|
2208
|
-
return JSON.parse(arg);
|
|
2209
|
-
} catch {
|
|
2210
|
-
return arg;
|
|
2211
|
-
}
|
|
2212
|
-
return arg;
|
|
2213
|
-
};
|
|
1535
|
+
//#region src/commands/debug/index.ts
|
|
1536
|
+
const logger$2 = initializeLogger();
|
|
2214
1537
|
/**
|
|
2215
|
-
*
|
|
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.
|
|
2216
1540
|
*/
|
|
2217
|
-
const
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
setMetadata({
|
|
2223
|
-
pathToCount: {},
|
|
2224
|
-
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
|
|
2225
1546
|
});
|
|
2226
1547
|
try {
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
if (paths.length === 0) logger$1.info("No spans to cache, starting fresh");
|
|
2231
|
-
else {
|
|
2232
|
-
const query = `
|
|
2233
|
-
SELECT name, input, output, attributes, path
|
|
2234
|
-
FROM spans
|
|
2235
|
-
WHERE trace_id = {traceId:UUID}
|
|
2236
|
-
AND path IN {paths:String[]}
|
|
2237
|
-
ORDER BY start_time ASC
|
|
2238
|
-
`;
|
|
2239
|
-
logger$1.debug(`Querying spans from trace ${trace_id}...`);
|
|
2240
|
-
const spans = await client.sql.query(query, {
|
|
2241
|
-
traceId: trace_id,
|
|
2242
|
-
paths
|
|
2243
|
-
});
|
|
2244
|
-
logger$1.debug(`Received ${spans.length} spans from backend`);
|
|
2245
|
-
const spansByPath = {};
|
|
2246
|
-
for (const span of spans) {
|
|
2247
|
-
const path$2 = span.path;
|
|
2248
|
-
if (!spansByPath[path$2]) spansByPath[path$2] = [];
|
|
2249
|
-
spansByPath[path$2].push(span);
|
|
2250
|
-
}
|
|
2251
|
-
for (const [path$2, pathSpans] of Object.entries(spansByPath)) {
|
|
2252
|
-
const maxCount = path_to_count?.[path$2] || 0;
|
|
2253
|
-
const spansToCache = pathSpans.slice(0, maxCount);
|
|
2254
|
-
spansToCache.forEach((span, index) => {
|
|
2255
|
-
let parsedInput;
|
|
2256
|
-
let parsedOutput;
|
|
2257
|
-
let parsedAttributes;
|
|
2258
|
-
try {
|
|
2259
|
-
parsedInput = typeof span.input === "string" ? JSON.parse(span.input) : span.input;
|
|
2260
|
-
} catch {
|
|
2261
|
-
parsedInput = span.input;
|
|
2262
|
-
}
|
|
2263
|
-
try {
|
|
2264
|
-
parsedOutput = typeof span.output === "string" ? span.output : JSON.stringify(span.output);
|
|
2265
|
-
} catch {
|
|
2266
|
-
parsedOutput = String(span.output);
|
|
2267
|
-
}
|
|
2268
|
-
try {
|
|
2269
|
-
parsedAttributes = typeof span.attributes === "string" ? JSON.parse(span.attributes) : span.attributes;
|
|
2270
|
-
} catch {
|
|
2271
|
-
parsedAttributes = {};
|
|
2272
|
-
}
|
|
2273
|
-
const cachedSpan = {
|
|
2274
|
-
name: span.name,
|
|
2275
|
-
input: parsedInput,
|
|
2276
|
-
output: parsedOutput,
|
|
2277
|
-
attributes: parsedAttributes
|
|
2278
|
-
};
|
|
2279
|
-
const cacheKey = `${index}:${path$2}`;
|
|
2280
|
-
cache.set(cacheKey, cachedSpan);
|
|
2281
|
-
});
|
|
2282
|
-
logger$1.info(`Cached ${spansToCache.length} spans for path: ${path$2}`);
|
|
2283
|
-
}
|
|
2284
|
-
setMetadata({
|
|
2285
|
-
pathToCount: path_to_count || {},
|
|
2286
|
-
overrides
|
|
2287
|
-
});
|
|
2288
|
-
}
|
|
2289
|
-
}
|
|
2290
|
-
const baseUrl = options.baseUrl ?? process.env.LMNR_BASE_URL ?? "https://api.lmnr.ai";
|
|
2291
|
-
const httpPort = options.port ?? (baseUrl.match(/:\d{1,5}$/g) ? parseInt(baseUrl.match(/:\d{1,5}$/g)[0].slice(1)) : 443);
|
|
2292
|
-
const grpcPort = options.grpcPort ?? 8443;
|
|
2293
|
-
const env = {
|
|
2294
|
-
LMNR_ROLLOUT_SESSION_ID: sessionId,
|
|
2295
|
-
LMNR_ROLLOUT_STATE_SERVER_ADDRESS: `http://localhost:${cacheServerPort}`
|
|
2296
|
-
};
|
|
2297
|
-
const workerConfig = {
|
|
2298
|
-
filePath: options.pythonModule ? void 0 : filePathOrModule,
|
|
2299
|
-
modulePath: options.pythonModule,
|
|
2300
|
-
functionName: options.function,
|
|
2301
|
-
args: parsedArgs,
|
|
2302
|
-
env,
|
|
2303
|
-
cacheServerPort,
|
|
2304
|
-
baseUrl,
|
|
2305
|
-
projectApiKey: options.projectApiKey,
|
|
2306
|
-
httpPort,
|
|
2307
|
-
grpcPort,
|
|
2308
|
-
externalPackages: options.externalPackages,
|
|
2309
|
-
dynamicImportsToSkip: options.dynamicImportsToSkip
|
|
2310
|
-
};
|
|
2311
|
-
const workerCommand = options.command ? {
|
|
2312
|
-
command: options.command,
|
|
2313
|
-
args: options.commandArgs ?? []
|
|
2314
|
-
} : getWorkerCommand(options.pythonModule ? void 0 : filePathOrModule, options);
|
|
2315
|
-
try {
|
|
2316
|
-
await client.rolloutSessions.setStatus({
|
|
2317
|
-
sessionId,
|
|
2318
|
-
status: "RUNNING"
|
|
2319
|
-
});
|
|
2320
|
-
} catch (error) {
|
|
2321
|
-
logger$1.error(`Error setting debugger session status: ${error instanceof Error ? error.message : error}`);
|
|
2322
|
-
}
|
|
2323
|
-
await subprocessManager.execute({
|
|
2324
|
-
command: workerCommand.command,
|
|
2325
|
-
args: workerCommand.args,
|
|
2326
|
-
config: workerConfig
|
|
1548
|
+
await client.rolloutSessions.setName({
|
|
1549
|
+
sessionId,
|
|
1550
|
+
name
|
|
2327
1551
|
});
|
|
2328
|
-
|
|
2329
|
-
|
|
1552
|
+
if (options.json) {
|
|
1553
|
+
outputJson({
|
|
2330
1554
|
sessionId,
|
|
2331
|
-
|
|
1555
|
+
name
|
|
2332
1556
|
});
|
|
2333
|
-
|
|
2334
|
-
logger$1.error(`Error setting debugger session status: ${error instanceof Error ? error.message : error}`);
|
|
1557
|
+
return;
|
|
2335
1558
|
}
|
|
1559
|
+
logger$2.info(`Set name of session ${sessionId} to "${name}".`);
|
|
2336
1560
|
} catch (error) {
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
await client.rolloutSessions.setStatus({
|
|
2341
|
-
sessionId,
|
|
2342
|
-
status: "FINISHED"
|
|
2343
|
-
});
|
|
2344
|
-
} catch (error$1) {
|
|
2345
|
-
logger$1.error(`Error setting debugger session status: ${error$1 instanceof Error ? error$1.message : error$1}`);
|
|
2346
|
-
}
|
|
1561
|
+
if (options.json) outputJsonError(error);
|
|
1562
|
+
logger$2.error(`Failed to set session name: ${errorMessage(error)}`);
|
|
1563
|
+
process.exit(1);
|
|
2347
1564
|
}
|
|
2348
1565
|
};
|
|
1566
|
+
const SUMMARY_PAGE_SIZE = 1e3;
|
|
2349
1567
|
/**
|
|
2350
|
-
*
|
|
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.
|
|
2351
1571
|
*/
|
|
2352
|
-
async
|
|
2353
|
-
const isPythonModule = !!options.pythonModule;
|
|
2354
|
-
const filePathOrModule = filePath || options.pythonModule;
|
|
2355
|
-
let didLogHandshake = false;
|
|
2356
|
-
const sessionId = newUUID();
|
|
1572
|
+
const handleDebugSessionSummary = async (sessionId, options) => {
|
|
2357
1573
|
const client = new LaminarClient({
|
|
2358
|
-
baseUrl: options.baseUrl,
|
|
2359
1574
|
projectApiKey: options.projectApiKey,
|
|
1575
|
+
baseUrl: options.baseUrl,
|
|
2360
1576
|
port: options.port
|
|
2361
1577
|
});
|
|
2362
|
-
logger$1.debug("Starting cache server...");
|
|
2363
|
-
const { port: cacheServerPort, server: cacheServer, cache, setMetadata } = await startCacheServer();
|
|
2364
|
-
logger$1.debug(`Cache server started on port ${cacheServerPort}`);
|
|
2365
|
-
const subprocessManager = new SubprocessManager();
|
|
2366
|
-
let functionName = options.function;
|
|
2367
|
-
let params = [];
|
|
2368
1578
|
try {
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
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;
|
|
2379
1594
|
}
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
throw error;
|
|
2384
|
-
}
|
|
2385
|
-
logger$1.debug("Setting up file watcher...");
|
|
2386
|
-
const watcher = chokidar.default.watch(".", {
|
|
2387
|
-
ignored: (path$2) => {
|
|
2388
|
-
const ignoredDirs = [
|
|
2389
|
-
"node_modules",
|
|
2390
|
-
".git",
|
|
2391
|
-
"dist",
|
|
2392
|
-
"build",
|
|
2393
|
-
".next",
|
|
2394
|
-
"coverage",
|
|
2395
|
-
".turbo",
|
|
2396
|
-
"tmp",
|
|
2397
|
-
"temp",
|
|
2398
|
-
"venv",
|
|
2399
|
-
".venv",
|
|
2400
|
-
"virtualenv",
|
|
2401
|
-
".virtualenv",
|
|
2402
|
-
"__pycache__",
|
|
2403
|
-
".pytest_cache",
|
|
2404
|
-
".ruff_cache",
|
|
2405
|
-
".mypy_cache",
|
|
2406
|
-
".cache",
|
|
2407
|
-
".DS_Store"
|
|
2408
|
-
];
|
|
2409
|
-
if (path$2.split(/[/\\]/).some((segment) => ignoredDirs.includes(segment))) return true;
|
|
2410
|
-
if (path$2.endsWith(".log") || path$2.endsWith(".map")) return true;
|
|
2411
|
-
return false;
|
|
2412
|
-
},
|
|
2413
|
-
persistent: true,
|
|
2414
|
-
ignoreInitial: true,
|
|
2415
|
-
awaitWriteFinish: {
|
|
2416
|
-
stabilityThreshold: 100,
|
|
2417
|
-
pollInterval: 100
|
|
1595
|
+
if (options.json) {
|
|
1596
|
+
outputJson(traces);
|
|
1597
|
+
return;
|
|
2418
1598
|
}
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
params,
|
|
2427
|
-
name: functionName ?? ""
|
|
2428
|
-
});
|
|
2429
|
-
let currentRunPromise = null;
|
|
2430
|
-
let stopRequested = false;
|
|
2431
|
-
sseClient.on("heartbeat", () => {
|
|
2432
|
-
logger$1.debug("Heartbeat received");
|
|
2433
|
-
});
|
|
2434
|
-
sseClient.on("run", (event) => {
|
|
2435
|
-
if (currentRunPromise !== null) {
|
|
2436
|
-
logger$1.warn("Already processing a run event, skipping new run");
|
|
2437
|
-
return;
|
|
2438
|
-
}
|
|
2439
|
-
currentRunPromise = (async () => {
|
|
2440
|
-
try {
|
|
2441
|
-
stopRequested = false;
|
|
2442
|
-
if (reloadScheduled) {
|
|
2443
|
-
logger$1.info("Reloading function metadata before run...");
|
|
2444
|
-
if (isPythonModule || filePath && EXTENSIONS_TO_DISCOVER_METADATA.includes(path.extname(filePath))) try {
|
|
2445
|
-
const metadata = await discoverFunctionMetadata(filePathOrModule, options);
|
|
2446
|
-
if (stopRequested) {
|
|
2447
|
-
logger$1.info("Run cancelled during metadata discovery");
|
|
2448
|
-
return;
|
|
2449
|
-
}
|
|
2450
|
-
logger$1.debug(`Updated function metadata: ${metadata.functionName}`);
|
|
2451
|
-
logger$1.debug(`Updated parameters: ${JSON.stringify(metadata.params, null, 2)}`);
|
|
2452
|
-
if (sseClient) {
|
|
2453
|
-
sseClient.updateMetadata(metadata.params, metadata.functionName);
|
|
2454
|
-
logger$1.debug("Notified backend of metadata changes");
|
|
2455
|
-
}
|
|
2456
|
-
reloadScheduled = false;
|
|
2457
|
-
} catch (error) {
|
|
2458
|
-
logger$1.error("Failed to update function metadata: " + (error instanceof Error ? error.message : String(error)));
|
|
2459
|
-
if (error instanceof Error && error.stack) logger$1.debug(`Stack trace: ${error.stack}`);
|
|
2460
|
-
return;
|
|
2461
|
-
}
|
|
2462
|
-
else reloadScheduled = false;
|
|
2463
|
-
}
|
|
2464
|
-
if (stopRequested) {
|
|
2465
|
-
logger$1.info("Run cancelled before execution");
|
|
2466
|
-
return;
|
|
2467
|
-
}
|
|
2468
|
-
await handleRunEvent(event, sessionId, filePathOrModule, client, cacheServerPort, cache, setMetadata, options, subprocessManager);
|
|
2469
|
-
} catch (error) {
|
|
2470
|
-
logger$1.error("Unhandled error in run event handler: " + (error instanceof Error ? error.message : String(error)));
|
|
2471
|
-
} finally {
|
|
2472
|
-
currentRunPromise = null;
|
|
2473
|
-
}
|
|
2474
|
-
})();
|
|
2475
|
-
});
|
|
2476
|
-
sseClient.on("handshake", (event) => {
|
|
2477
|
-
const projectId = event.data.project_id;
|
|
2478
|
-
const sessionId$1 = event.data.session_id;
|
|
2479
|
-
const frontendUrl = getFrontendUrl(options.baseUrl, options.frontendPort);
|
|
2480
|
-
if (!didLogHandshake) logger$1.info(`View your session at ${frontendUrl}/project/${projectId}/debugger-sessions/${sessionId$1}`);
|
|
2481
|
-
didLogHandshake = true;
|
|
2482
|
-
});
|
|
2483
|
-
sseClient.on("error", (error) => {
|
|
2484
|
-
logger$1.warn(`Error connecting to backend: ${error.message}`);
|
|
2485
|
-
});
|
|
2486
|
-
sseClient.on("reconnecting", () => {
|
|
2487
|
-
logger$1.info("Reconnecting to backend...");
|
|
2488
|
-
});
|
|
2489
|
-
sseClient.on("heartbeat_timeout", () => {
|
|
2490
|
-
logger$1.debug("Heartbeat timeout, reconnecting...");
|
|
2491
|
-
});
|
|
2492
|
-
sseClient.on("stop", () => {
|
|
2493
|
-
logger$1.debug("Stop event received");
|
|
2494
|
-
stopRequested = true;
|
|
2495
|
-
if (subprocessManager.kill()) logger$1.info("Current run cancelled");
|
|
2496
|
-
});
|
|
2497
|
-
let reloadTimeout = null;
|
|
2498
|
-
let reloadScheduled = false;
|
|
2499
|
-
watcher.on("change", (changedPath) => {
|
|
2500
|
-
logger$1.info(`File changed: ${changedPath}, scheduling reload...`);
|
|
2501
|
-
if (reloadTimeout) clearTimeout(reloadTimeout);
|
|
2502
|
-
reloadTimeout = setTimeout(() => {
|
|
2503
|
-
logger$1.debug("Marking reload as scheduled for next run...");
|
|
2504
|
-
reloadTimeout = null;
|
|
2505
|
-
reloadScheduled = true;
|
|
2506
|
-
}, 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;
|
|
2507
1606
|
});
|
|
2508
|
-
|
|
2509
|
-
logger$1.debug("Shutting down...");
|
|
2510
|
-
if (reloadTimeout) {
|
|
2511
|
-
clearTimeout(reloadTimeout);
|
|
2512
|
-
reloadTimeout = null;
|
|
2513
|
-
}
|
|
2514
|
-
reloadScheduled = false;
|
|
2515
|
-
logger$1.debug("Closing file watcher...");
|
|
2516
|
-
watcher.close().catch((error) => {
|
|
2517
|
-
logger$1.error(`Failed to close file watcher: ${error instanceof Error ? error.message : error}`);
|
|
2518
|
-
});
|
|
2519
|
-
subprocessManager.kill();
|
|
2520
|
-
logger$1.debug("Deleting debugger session...");
|
|
2521
|
-
client.rolloutSessions.delete({ sessionId }).then(() => {
|
|
2522
|
-
if (sseClient) sseClient.shutdown();
|
|
2523
|
-
cacheServer.close(() => {
|
|
2524
|
-
logger$1.debug("Cache server closed");
|
|
2525
|
-
});
|
|
2526
|
-
process.exit(0);
|
|
2527
|
-
}).catch((error) => {
|
|
2528
|
-
logger$1.warn(`Failed to delete debugger session: ${error instanceof Error ? error.message : error}`);
|
|
2529
|
-
process.exit(1);
|
|
2530
|
-
});
|
|
2531
|
-
};
|
|
2532
|
-
process.on("SIGINT", shutdown);
|
|
2533
|
-
process.on("SIGTERM", shutdown);
|
|
2534
|
-
process.stdin.resume();
|
|
2535
|
-
logger$1.debug("Connecting to backend...");
|
|
2536
|
-
await sseClient.connectAndListen();
|
|
1607
|
+
console.log(blocks.join("\n\n"));
|
|
2537
1608
|
} catch (error) {
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
} catch {}
|
|
2542
|
-
await watcher.close();
|
|
2543
|
-
cacheServer.close(() => {
|
|
2544
|
-
process.exit(1);
|
|
2545
|
-
});
|
|
1609
|
+
if (options.json) outputJsonError(error);
|
|
1610
|
+
logger$2.error(`Failed to summarize session: ${errorMessage(error)}`);
|
|
1611
|
+
process.exit(1);
|
|
2546
1612
|
}
|
|
2547
|
-
}
|
|
2548
|
-
|
|
1613
|
+
};
|
|
2549
1614
|
//#endregion
|
|
2550
1615
|
//#region src/commands/sql/index.ts
|
|
2551
|
-
const logger = initializeLogger();
|
|
1616
|
+
const logger$1 = initializeLogger();
|
|
2552
1617
|
const handleSqlQuery = async (query, options) => {
|
|
2553
1618
|
const client = new LaminarClient({
|
|
2554
1619
|
projectApiKey: options.projectApiKey,
|
|
@@ -2571,11 +1636,10 @@ const handleSqlQuery = async (query, options) => {
|
|
|
2571
1636
|
console.log(`\n${rows.length} row(s)\n`);
|
|
2572
1637
|
} catch (error) {
|
|
2573
1638
|
if (options.json) outputJsonError(error);
|
|
2574
|
-
logger.error(`Query failed: ${
|
|
1639
|
+
logger$1.error(`Query failed: ${errorMessage(error)}`);
|
|
2575
1640
|
process.exit(1);
|
|
2576
1641
|
}
|
|
2577
1642
|
};
|
|
2578
|
-
|
|
2579
1643
|
//#endregion
|
|
2580
1644
|
//#region src/commands/sql/schema.ts
|
|
2581
1645
|
const SQL_SCHEMA_HELP = `
|
|
@@ -2622,29 +1686,60 @@ Available tables:
|
|
|
2622
1686
|
id (UUID), created_at (DateTime64), dataset_id (UUID),
|
|
2623
1687
|
data (String), target (String), metadata (String)
|
|
2624
1688
|
`;
|
|
2625
|
-
|
|
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
|
+
};
|
|
2626
1738
|
//#endregion
|
|
2627
1739
|
//#region src/index.ts
|
|
2628
1740
|
async function main() {
|
|
2629
1741
|
const program = new commander.Command();
|
|
2630
1742
|
program.name("lmnr-cli").description("CLI for the Laminar agent observability platform").version(version$1, "-v, --version", "display version number");
|
|
2631
|
-
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) => {
|
|
2632
|
-
if (!file && !options.pythonModule) {
|
|
2633
|
-
console.error("Error: Must provide either a file path or --python-module (-m) flag");
|
|
2634
|
-
process.exit(1);
|
|
2635
|
-
}
|
|
2636
|
-
if (file && options.pythonModule) {
|
|
2637
|
-
console.error("Error: Cannot specify both file path and --python-module (-m) flag");
|
|
2638
|
-
process.exit(1);
|
|
2639
|
-
}
|
|
2640
|
-
await runDev(file, options);
|
|
2641
|
-
}).addHelpText("after", `
|
|
2642
|
-
Examples:
|
|
2643
|
-
$ lmnr-cli dev agent.ts # TypeScript file
|
|
2644
|
-
$ lmnr-cli dev agent.py # Python file (script mode)
|
|
2645
|
-
$ lmnr-cli dev -m src.agent # Python module (module mode)
|
|
2646
|
-
$ lmnr-cli dev agent.ts --function myAgent # Specific function
|
|
2647
|
-
`);
|
|
2648
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");
|
|
2649
1744
|
datasetsCmd.command("list").description("List all datasets").action(async (_options, cmd) => {
|
|
2650
1745
|
await handleDatasetsList(cmd.optsWithGlobals());
|
|
@@ -2670,24 +1765,69 @@ Examples:
|
|
|
2670
1765
|
sqlCmd.command("schema").description("Show available tables and their columns").action(() => {
|
|
2671
1766
|
process.stdout.write(SQL_SCHEMA_HELP);
|
|
2672
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
|
+
`);
|
|
2673
1803
|
program.addHelpText("after", `
|
|
1804
|
+
Authentication:
|
|
1805
|
+
Most commands require a project API key. Provide it in one of two ways:
|
|
1806
|
+
1. Environment variable: export LMNR_PROJECT_API_KEY=<your-key>
|
|
1807
|
+
2. CLI flag: --project-api-key <your-key>
|
|
1808
|
+
Get your key at https://www.laminar.sh (Settings > Project API Keys).
|
|
1809
|
+
|
|
2674
1810
|
Examples:
|
|
2675
|
-
lmnr-cli dev agent.ts # Debugger TypeScript entrypoint
|
|
2676
|
-
lmnr-cli dev agent.py # Debugger Python script mode
|
|
2677
|
-
lmnr-cli dev -m src.agent # Debugger Python module mode
|
|
2678
1811
|
lmnr-cli dataset list --json # List all datasets
|
|
2679
1812
|
lmnr-cli dataset push data.jsonl -n my-dataset --json # Push data to a dataset
|
|
2680
1813
|
lmnr-cli dataset pull output.jsonl -n my-dataset --json # Pull data from a dataset
|
|
2681
1814
|
lmnr-cli sql query "SELECT * FROM spans LIMIT 10" --json # Query spans
|
|
2682
1815
|
lmnr-cli sql query "SELECT t.id, s.name FROM traces t JOIN spans s ON t.id = s.trace_id" --json
|
|
2683
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
|
|
1820
|
+
|
|
1821
|
+
For more information about the Laminar platfrom:
|
|
1822
|
+
Documentation: https://laminar.sh/docs
|
|
1823
|
+
Dashboard: https://www.laminar.sh
|
|
2684
1824
|
`);
|
|
2685
1825
|
await program.parseAsync();
|
|
2686
1826
|
}
|
|
2687
1827
|
main().catch((err) => {
|
|
2688
|
-
console.error(err
|
|
1828
|
+
console.error(errorMessage(err));
|
|
2689
1829
|
process.exit(1);
|
|
2690
1830
|
});
|
|
2691
|
-
|
|
2692
1831
|
//#endregion
|
|
1832
|
+
|
|
2693
1833
|
//# sourceMappingURL=index.cjs.map
|