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/dist/index.cjs CHANGED
@@ -1,23 +1,19 @@
1
1
  #!/usr/bin/env node
2
- //#region rolldown:runtime
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
- for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
13
- key = keys[i];
14
- if (!__hasOwnProp.call(to, key) && key !== except) {
15
- __defProp(to, key, {
16
- get: ((k) => from[k]).bind(null, key),
17
- enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
18
- });
19
- }
20
- }
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
- let chokidar = require("chokidar");
46
- chokidar = __toESM(chokidar);
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 ../../node_modules/.pnpm/dotenv@17.2.3/node_modules/dotenv/package.json
60
- var require_package = /* @__PURE__ */ __commonJSMin(((exports, module) => {
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.3/node_modules/dotenv/lib/main.js
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
- "🔐 encrypt with Dotenvx: https://dotenvx.com",
131
- "🔐 prevent committing .env to code: https://dotenvx.com/precommit",
132
- "🔐 prevent building .env in docker: https://dotenvx.com/prebuild",
133
- "📡 add observability to secrets: https://dotenvx.com/ops",
134
- "👥 sync secrets across teammates & machines: https://dotenvx.com/ops",
135
- "🗂️ backup and recover secrets: https://dotenvx.com/ops",
136
- " audit secrets and track compliance: https://dotenvx.com/ops",
137
- "🔄 add secrets lifecycle management: https://dotenvx.com/ops",
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(`[dotenv@${version}][WARN] ${message}`);
125
+ console.error(`⚠ ${message}`);
210
126
  }
211
127
  function _debug(message) {
212
- console.log(`[dotenv@${version}][DEBUG] ${message}`);
128
+ console.log(`┆ ${message}`);
213
129
  }
214
130
  function _log(message) {
215
- console.log(`[dotenv@${version}] ${message}`);
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("Loading env from encrypted .env.vault");
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("No encoding is specified. UTF-8 is used by default");
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$2 of optionPaths) try {
298
- const parsed = DotenvModule.parse(fs$1.readFileSync(path$2, { encoding }));
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(`Failed to load ${path$2} ${e.message}`);
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(`Failed to load ${filePath} ${e.message}`);
230
+ if (debug) _debug(`failed to load ${filePath} ${e.message}`);
315
231
  lastError = e;
316
232
  }
317
- _log(`injecting env (${keysCount}) from ${shortPaths.join(",")} ${dim(`-- tip: ${_getRandomTip()}`)}`);
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(`You set DOTENV_KEY but you are missing a .env.vault file at ${vaultPath}. Did you forget to build it?`);
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.15";
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: events$1 }) {
378
+ async send({ sessionId, traceId, events }) {
431
379
  const payload = {
432
380
  sessionId,
433
381
  traceId,
434
- events: events$1,
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$2$1 = initializeLogger$1();
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$1 = () => {
410
+ const newUUID = () => {
463
411
  if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return crypto.randomUUID();
464
- else return (0, uuid.v4)();
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$2$1.warn(`Span ID ${spanId} is not 16 hex chars long. This is not a valid OpenTelemetry span ID.`);
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$2$1.error(`Span ID ${spanId} is not a valid hex string. Generating a random UUID instead.`);
472
- return newUUID$1();
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$2$1.warn(`Trace ID ${traceId} is not 32 hex chars long. This is not a valid OpenTelemetry trace ID.`);
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$2$1.error(`Trace ID ${traceId} is not a valid hex string. Generating a random UUID instead.`);
482
- return newUUID$1();
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$1$1 = initializeLogger$1();
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$1$1.debug(`Pushing batch ${batchNum} of ${totalBatches}`);
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$6 = initializeLogger$1();
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$1();
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$1(),
677
- executorSpanId: newUUID$1(),
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$6.warn("evals.getDatapoints() is deprecated. Use client.datasets.pull() instead.");
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$6.debug(`Retrying save datapoints... ${i + 1} of ${maxRetries}, length: ${length}`);
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
- const formattedTraceId = isStringUUID(options.traceId) ? options.traceId : otelTraceIdToUUID(options.traceId);
831
- payload = {
832
- name,
833
- metadata,
834
- score,
835
- source: EvaluatorScoreSourceType.Code,
836
- traceId: formattedTraceId
837
- };
838
- } else if ("spanId" in options && options.spanId) {
839
- const formattedSpanId = isStringUUID(options.spanId) ? options.spanId : otelSpanIdToUUID(options.spanId);
840
- payload = {
841
- name,
842
- metadata,
843
- score,
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
- * Connects to the SSE stream for rollout debugging sessions
862
- * Returns the Response object for streaming SSE events
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 connect({ sessionId, name, params, signal }) {
808
+ async register({ sessionId, name }) {
865
809
  const response = await fetch(`${this.baseHttpUrl}/v1/rollouts/${sessionId}`, {
866
810
  method: "POST",
867
- headers: {
868
- ...this.headers(),
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
- async setStatus({ sessionId, status }) {
889
- const response = await fetch(`${this.baseHttpUrl}/v1/rollouts/${sessionId}/status`, {
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({ status })
832
+ body: JSON.stringify({ name })
893
833
  });
894
834
  if (!response.ok) await this.handleError(response);
895
835
  }
896
- async sendSpanUpdate({ sessionId, span }) {
897
- const response = await fetch(`${this.baseHttpUrl}/v1/rollouts/${sessionId}/update`, {
898
- method: "PATCH",
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
- this.baseUrl = `${(baseUrl ?? process.env.LMNR_BASE_URL)?.replace(/\/$/, "").replace(/:\d{1,5}$/g, "") ?? "https://api.lmnr.ai"}:${httpPort}`;
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$5 = initializeLogger();
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$5.warn(`Skipping unsupported file type: ${filepath}`);
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$5.warn(`Path does not exist or is not accessible: ${filepath}. Error: ${error instanceof Error ? error.message : String(error)}`);
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$5.debug(`Error parsing JSON: ${error instanceof Error ? error.message : String(error)}`);
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$5.warn("No supported files found in the specified paths");
1146
+ logger$4.warn("No supported files found in the specified paths");
1136
1147
  return [];
1137
1148
  }
1138
- logger$5.info(`Found ${files.length} file(s) to read`);
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$5.info(`Read ${data.length} record(s) from ${file}`);
1154
+ logger$4.info(`Read ${data.length} record(s) from ${file}`);
1144
1155
  } catch (error) {
1145
- logger$5.error(`Error reading file ${file}: ${error instanceof Error ? error.message : String(error)}`);
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$5.warn(`Output format ${format} does not match file extension ${path.extname(filepath).slice(1)}`);
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$5.error("No data to print");
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: error instanceof Error ? error.message : String(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$4 = initializeLogger();
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$4.error(`Failed to list datasets: ${error instanceof Error ? error.message : String(error)}`);
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$4.error("Either name or id must be provided");
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$4.error("Only one of name or id must be provided");
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$4.error("No data to push. Skipping");
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$4.info(`Pushed ${data.length} data points to dataset ${options.name || options.id}`);
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$4.error(`Failed to push dataset: ${error instanceof Error ? error.message : String(error)}`);
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$4.error("Either name or id must be provided");
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$4.error("Only one of name or id must be provided");
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$4.info(`Successfully pulled ${result.length} data points to ${outputPath}`);
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$4.error(`Failed to pull dataset: ${error instanceof Error ? error.message : String(error)}`);
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$4.error("No data to push. Skipping");
1469
+ logger$3.error("No data to push. Skipping");
1462
1470
  process.exit(1);
1463
1471
  }
1464
- logger$4.info(`Pushing ${data.length} data points to dataset '${name}'...`);
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$4.info(`Successfully pushed ${data.length} data points to dataset '${name}'`);
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$4.error(`Failed to create dataset: ${error instanceof Error ? error.message : String(error)}`);
1482
+ logger$3.error(`Failed to create dataset: ${errorMessage(error)}`);
1475
1483
  process.exit(1);
1476
1484
  }
1477
- logger$4.info(`Pulling data from dataset '${name}'...`);
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$4.info(`Successfully created dataset '${name}' and saved ${result.length} datapoints to ${options.outputFile}`);
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$4.error(`Failed to pull dataset after creation: ${error instanceof Error ? error.message : String(error)}`);
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/worker-registry.ts
1502
+ //#region src/utils/trace-note.ts
1503
+ const NOTE_METADATA_KEY = "rollout.note";
1878
1504
  /**
1879
- * Default workers mapped by file extension
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 DEFAULT_WORKERS = {
1882
- ".ts": {
1883
- command: "node",
1884
- args: []
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
- * Get the worker command for a given file path or module.
1921
- * Resolves the TypeScript worker dynamically from @lmnr-ai/lmnr package.
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 METADATA_PROTOCOL_PREFIX = "LMNR_METADATA:";
1972
- const logLmnrPackageNotFoundAndExit = () => {
1973
- logger$2.error("@lmnr-ai/lmnr package not found or outdated. For JS/TS projects, please install the latest version of @lmnr-ai/lmnr in your project: npm install @lmnr-ai/lmnr\nYou might need to run `lmnr-cli` from the root of your project");
1974
- process.exit(1);
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
- let depth = 0;
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
- continue;
1527
+ return "";
2143
1528
  }
2144
1529
  }
2145
- if (lastValidJson === null) throw new Error("No valid metadata JSON found in output. Please make sure you are running the latest version of `lmnr` python package.");
2146
- return lastValidJson;
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/dev/index.ts
2187
- const logger$1 = initializeLogger();
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
- * Handles a run event from the backend
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 handleRunEvent = async (event, sessionId, filePathOrModule, client, cacheServerPort, cache, setMetadata, options, subprocessManager) => {
2218
- logger$1.debug("Received run event");
2219
- const { trace_id, path_to_count, args: rawArgs, overrides } = event.data;
2220
- const parsedArgs = Array.isArray(rawArgs) ? rawArgs.map(tryParseArg) : Object.fromEntries(Object.entries(rawArgs).map(([key, value]) => [key, tryParseArg(value)]));
2221
- cache.clear();
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
- if (!trace_id || trace_id.trim() === "") logger$1.info("No spans in cache, starting fresh");
2228
- else {
2229
- const paths = Object.keys(path_to_count || {});
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
- try {
2329
- await client.rolloutSessions.setStatus({
1552
+ if (options.json) {
1553
+ outputJson({
2330
1554
  sessionId,
2331
- status: "FINISHED"
1555
+ name
2332
1556
  });
2333
- } catch (error) {
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
- logger$1.error(`Error handling run event: ${error instanceof Error ? error.message : error}`);
2338
- if (error instanceof Error && error.stack) logger$1.error(error.stack);
2339
- try {
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
- * Main dev command handler
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 function runDev(filePath, options = {}) {
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
- if (isPythonModule || filePath && EXTENSIONS_TO_DISCOVER_METADATA.includes(path.extname(filePath))) {
2370
- logger$1.debug("Discovering entrypoint functions...");
2371
- const metadata = await discoverFunctionMetadata(filePathOrModule, options);
2372
- functionName = metadata.functionName;
2373
- params = metadata.params;
2374
- logger$1.info(`Serving function: ${functionName}`);
2375
- logger$1.debug(`Function parameters: ${JSON.stringify(params, null, 2)}`);
2376
- } else if (filePath) {
2377
- functionName = options.function || path.basename(filePath, path.extname(filePath));
2378
- logger$1.warn(`Metadata discovery not available for ${path.extname(filePath)} files`);
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
- } catch (error) {
2381
- logger$1.error("Failed to discover entrypoint functions: " + (error instanceof Error ? error.message : String(error)));
2382
- cacheServer.close();
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
- logger$1.debug("Setting up SSE client...");
2421
- let sseClient = null;
2422
- try {
2423
- sseClient = createSSEClient({
2424
- client,
2425
- sessionId,
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
- const shutdown = () => {
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
- logger$1.error("Failed to start dev command: " + (error instanceof Error ? error.message : String(error)));
2539
- try {
2540
- await client.rolloutSessions.delete({ sessionId });
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: ${error instanceof Error ? error.message : String(error)}`);
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 instanceof Error ? err.message : err);
1828
+ console.error(errorMessage(err));
2689
1829
  process.exit(1);
2690
1830
  });
2691
-
2692
1831
  //#endregion
1832
+
2693
1833
  //# sourceMappingURL=index.cjs.map