lmnr-cli 0.1.9 → 0.1.10

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