lmnr-cli 0.1.6 → 0.1.8

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
@@ -28,12 +28,20 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
28
28
 
29
29
  //#endregion
30
30
  let commander = require("commander");
31
+ let fs = require("fs");
31
32
  let path = require("path");
32
33
  path = __toESM(path);
33
34
  let pino = require("pino");
34
35
  pino = __toESM(pino);
35
36
  let pino_pretty = require("pino-pretty");
36
37
  let uuid = require("uuid");
38
+ let csv_parser = require("csv-parser");
39
+ csv_parser = __toESM(csv_parser);
40
+ let export_to_csv = require("export-to-csv");
41
+ let fs_promises = require("fs/promises");
42
+ fs_promises = __toESM(fs_promises);
43
+ let cli_table3 = require("cli-table3");
44
+ cli_table3 = __toESM(cli_table3);
37
45
  let chokidar = require("chokidar");
38
46
  chokidar = __toESM(chokidar);
39
47
  let http = require("http");
@@ -45,7 +53,7 @@ let readline = require("readline");
45
53
  readline = __toESM(readline);
46
54
 
47
55
  //#region package.json
48
- var version$1 = "0.1.6";
56
+ var version$1 = "0.1.8";
49
57
 
50
58
  //#endregion
51
59
  //#region ../../node_modules/.pnpm/dotenv@17.2.3/node_modules/dotenv/package.json
@@ -113,7 +121,7 @@ var require_package = /* @__PURE__ */ __commonJSMin(((exports, module) => {
113
121
  //#endregion
114
122
  //#region ../../node_modules/.pnpm/dotenv@17.2.3/node_modules/dotenv/lib/main.js
115
123
  var require_main = /* @__PURE__ */ __commonJSMin(((exports, module) => {
116
- const fs = require("fs");
124
+ const fs$1 = require("fs");
117
125
  const path$1 = require("path");
118
126
  const os = require("os");
119
127
  const crypto$1 = require("crypto");
@@ -250,10 +258,10 @@ var require_main = /* @__PURE__ */ __commonJSMin(((exports, module) => {
250
258
  function _vaultPath(options) {
251
259
  let possibleVaultPath = null;
252
260
  if (options && options.path && options.path.length > 0) if (Array.isArray(options.path)) {
253
- for (const filepath of options.path) if (fs.existsSync(filepath)) possibleVaultPath = filepath.endsWith(".vault") ? filepath : `${filepath}.vault`;
261
+ for (const filepath of options.path) if (fs$1.existsSync(filepath)) possibleVaultPath = filepath.endsWith(".vault") ? filepath : `${filepath}.vault`;
254
262
  } else possibleVaultPath = options.path.endsWith(".vault") ? options.path : `${options.path}.vault`;
255
263
  else possibleVaultPath = path$1.resolve(process.cwd(), ".env.vault");
256
- if (fs.existsSync(possibleVaultPath)) return possibleVaultPath;
264
+ if (fs$1.existsSync(possibleVaultPath)) return possibleVaultPath;
257
265
  return null;
258
266
  }
259
267
  function _resolveHome(envPath) {
@@ -287,7 +295,7 @@ var require_main = /* @__PURE__ */ __commonJSMin(((exports, module) => {
287
295
  let lastError;
288
296
  const parsedAll = {};
289
297
  for (const path$2 of optionPaths) try {
290
- const parsed = DotenvModule.parse(fs.readFileSync(path$2, { encoding }));
298
+ const parsed = DotenvModule.parse(fs$1.readFileSync(path$2, { encoding }));
291
299
  DotenvModule.populate(parsedAll, parsed, options);
292
300
  } catch (e) {
293
301
  if (debug) _debug(`Failed to load ${path$2} ${e.message}`);
@@ -392,7 +400,7 @@ var require_main = /* @__PURE__ */ __commonJSMin(((exports, module) => {
392
400
  //#endregion
393
401
  //#region ../client/dist/index.mjs
394
402
  var import_main = require_main();
395
- var version = "0.8.13";
403
+ var version = "0.8.15";
396
404
  function getLangVersion() {
397
405
  if (typeof process !== "undefined" && process.versions && process.versions.node) return `node-${process.versions.node}`;
398
406
  if (typeof navigator !== "undefined" && navigator.userAgent) return `browser-${navigator.userAgent}`;
@@ -500,7 +508,7 @@ const loadEnv = (options) => {
500
508
  };
501
509
  const logger$1$1 = initializeLogger$1();
502
510
  const DEFAULT_DATASET_PULL_LIMIT = 100;
503
- const DEFAULT_DATASET_PUSH_BATCH_SIZE = 100;
511
+ const DEFAULT_DATASET_PUSH_BATCH_SIZE$1 = 100;
504
512
  var DatasetsResource = class extends BaseResource {
505
513
  constructor(baseHttpUrl, projectApiKey) {
506
514
  super(baseHttpUrl, projectApiKey);
@@ -544,7 +552,7 @@ var DatasetsResource = class extends BaseResource {
544
552
  * @param {boolean} [options.createDataset] - Whether to create the dataset if it doesn't exist
545
553
  * @returns {Promise<PushDatapointsResponse | undefined>}
546
554
  */
547
- async push({ points, name, id, batchSize = DEFAULT_DATASET_PUSH_BATCH_SIZE, createDataset = false }) {
555
+ async push({ points, name, id, batchSize = DEFAULT_DATASET_PUSH_BATCH_SIZE$1, createDataset = false }) {
548
556
  if (!name && !id) throw new Error("Either name or id must be provided");
549
557
  if (name && id) throw new Error("Only one of name or id must be provided");
550
558
  if (createDataset && !name) throw new Error("Name must be provided when creating a new dataset");
@@ -601,7 +609,7 @@ var DatasetsResource = class extends BaseResource {
601
609
  return response.json();
602
610
  }
603
611
  };
604
- const logger$3 = initializeLogger$1();
612
+ const logger$6 = initializeLogger$1();
605
613
  const INITIAL_EVALUATION_DATAPOINT_MAX_DATA_LENGTH = 16e6;
606
614
  var EvalsResource = class extends BaseResource {
607
615
  constructor(baseHttpUrl, projectApiKey) {
@@ -737,7 +745,7 @@ var EvalsResource = class extends BaseResource {
737
745
  * @returns {Promise<GetDatapointsResponse>} Response from the datapoint retrieval
738
746
  */
739
747
  async getDatapoints({ datasetName, offset, limit }) {
740
- logger$3.warn("evals.getDatapoints() is deprecated. Use client.datasets.pull() instead.");
748
+ logger$6.warn("evals.getDatapoints() is deprecated. Use client.datasets.pull() instead.");
741
749
  const params = new URLSearchParams({
742
750
  name: datasetName,
743
751
  offset: offset.toString(),
@@ -754,7 +762,7 @@ var EvalsResource = class extends BaseResource {
754
762
  let length = initialLength;
755
763
  let lastResponse = null;
756
764
  for (let i = 0; i < maxRetries; i++) {
757
- logger$3.debug(`Retrying save datapoints... ${i + 1} of ${maxRetries}, length: ${length}`);
765
+ logger$6.debug(`Retrying save datapoints... ${i + 1} of ${maxRetries}, length: ${length}`);
758
766
  const response = await fetch(this.baseHttpUrl + `/v1/evals/${evalId}/datapoints`, {
759
767
  method: "POST",
760
768
  headers: this.headers(),
@@ -1012,6 +1020,477 @@ var LaminarClient = class {
1012
1020
  }
1013
1021
  };
1014
1022
 
1023
+ //#endregion
1024
+ //#region src/utils/logger.ts
1025
+ function initializeLogger(options) {
1026
+ const colorize = options?.colorize ?? true;
1027
+ const level = options?.level ?? process.env.LMNR_LOG_LEVEL?.toLowerCase()?.trim() ?? "info";
1028
+ return (0, pino.default)({ level }, (0, pino_pretty.PinoPretty)({
1029
+ colorize,
1030
+ minimumLevel: level,
1031
+ destination: 2
1032
+ }));
1033
+ }
1034
+
1035
+ //#endregion
1036
+ //#region src/utils/file.ts
1037
+ const logger$5 = initializeLogger();
1038
+ /**
1039
+ * Check if a file has a supported extension.
1040
+ */
1041
+ const isSupportedFile = (file) => {
1042
+ const ext = path.extname(file).toLowerCase();
1043
+ return [
1044
+ ".json",
1045
+ ".csv",
1046
+ ".jsonl"
1047
+ ].includes(ext);
1048
+ };
1049
+ /**
1050
+ * Collect all supported files from the given paths.
1051
+ * Handles both files and directories.
1052
+ */
1053
+ const collectFiles = async (paths, recursive = false) => {
1054
+ const collectedFiles = [];
1055
+ for (const filepath of paths) try {
1056
+ const stats = await fs_promises.stat(filepath);
1057
+ if (stats.isFile()) if (isSupportedFile(filepath)) collectedFiles.push(filepath);
1058
+ else logger$5.warn(`Skipping unsupported file type: ${filepath}`);
1059
+ else if (stats.isDirectory()) {
1060
+ const entries = await fs_promises.readdir(filepath);
1061
+ for (const entry of entries) {
1062
+ const fullPath = path.join(filepath, entry);
1063
+ const entryStats = await fs_promises.stat(fullPath);
1064
+ if (entryStats.isFile() && isSupportedFile(fullPath)) collectedFiles.push(fullPath);
1065
+ else if (recursive && entryStats.isDirectory()) {
1066
+ const subFiles = await collectFiles([fullPath], true);
1067
+ collectedFiles.push(...subFiles);
1068
+ }
1069
+ }
1070
+ }
1071
+ } catch (error) {
1072
+ logger$5.warn(`Path does not exist or is not accessible: ${filepath}. Error: ${error instanceof Error ? error.message : String(error)}`);
1073
+ }
1074
+ return collectedFiles;
1075
+ };
1076
+ /**
1077
+ * Read a JSON file and return its contents.
1078
+ */
1079
+ const readJsonFile = async (filepath) => {
1080
+ const content = await fs_promises.readFile(filepath, "utf-8");
1081
+ const parsed = JSON.parse(content);
1082
+ return Array.isArray(parsed) ? parsed : [parsed];
1083
+ };
1084
+ /**
1085
+ * Try to parse a string as JSON. If it fails, return the original string.
1086
+ */
1087
+ const tryParseJson = (content) => {
1088
+ if (typeof content !== "string") return content;
1089
+ const trimmed = content.trim();
1090
+ if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return content;
1091
+ try {
1092
+ return JSON.parse(content);
1093
+ } catch (error) {
1094
+ logger$5.debug(`Error parsing JSON: ${error instanceof Error ? error.message : String(error)}`);
1095
+ return content;
1096
+ }
1097
+ };
1098
+ /**
1099
+ * Parse each field in a CSV row, attempting to convert JSON strings back to objects.
1100
+ */
1101
+ const parseCsvRow = (row) => {
1102
+ const parsed = {};
1103
+ for (const [key, value] of Object.entries(row)) parsed[key] = tryParseJson(value);
1104
+ return parsed;
1105
+ };
1106
+ /**
1107
+ * Read a CSV file and return its contents as an array of objects.
1108
+ */
1109
+ const readCsvFile = async (filepath) => new Promise((resolve, reject) => {
1110
+ const results = [];
1111
+ (0, fs.createReadStream)(filepath).pipe((0, csv_parser.default)()).on("data", (data) => results.push(parseCsvRow(data))).on("end", () => resolve(results)).on("error", reject);
1112
+ });
1113
+ /**
1114
+ * Read a JSONL file and return its contents as an array of objects.
1115
+ */
1116
+ async function readJsonlFile(filepath) {
1117
+ return (await fs_promises.readFile(filepath, "utf-8")).split("\n").filter((line) => line.trim()).map((line) => JSON.parse(line));
1118
+ }
1119
+ /**
1120
+ * Read a single file and return its contents.
1121
+ */
1122
+ async function readFile(filepath) {
1123
+ const ext = path.extname(filepath).toLowerCase();
1124
+ if (ext === ".json") return readJsonFile(filepath);
1125
+ else if (ext === ".csv") return readCsvFile(filepath);
1126
+ else if (ext === ".jsonl") return readJsonlFile(filepath);
1127
+ else throw new Error(`Unsupported file type: ${ext}`);
1128
+ }
1129
+ /**
1130
+ * Load data from all files in the specified paths.
1131
+ */
1132
+ const loadFromPaths = async (paths, recursive = false) => {
1133
+ const files = await collectFiles(paths, recursive);
1134
+ if (files.length === 0) {
1135
+ logger$5.warn("No supported files found in the specified paths");
1136
+ return [];
1137
+ }
1138
+ logger$5.info(`Found ${files.length} file(s) to read`);
1139
+ const result = [];
1140
+ for (const file of files) try {
1141
+ const data = await readFile(file);
1142
+ result.push(...data);
1143
+ logger$5.info(`Read ${data.length} record(s) from ${file}`);
1144
+ } catch (error) {
1145
+ logger$5.error(`Error reading file ${file}: ${error instanceof Error ? error.message : String(error)}`);
1146
+ throw error;
1147
+ }
1148
+ return result;
1149
+ };
1150
+ /**
1151
+ * Write data to a JSON file.
1152
+ */
1153
+ const writeJsonFile = async (filepath, data) => {
1154
+ const content = JSON.stringify(data, null, 2);
1155
+ await fs_promises.writeFile(filepath, content, "utf-8");
1156
+ };
1157
+ /**
1158
+ * Format data as a CSV string.
1159
+ */
1160
+ const formatCsv = (data) => {
1161
+ const formattedData = data.map((item) => Object.fromEntries(Object.entries(item).map(([key, value]) => [key, stringifyForCsv(value)])));
1162
+ return (0, export_to_csv.asString)((0, export_to_csv.generateCsv)((0, export_to_csv.mkConfig)({ useKeysAsHeaders: true }))(formattedData));
1163
+ };
1164
+ /**
1165
+ * Write data to a CSV file.
1166
+ */
1167
+ const writeCsvFile = async (filepath, data) => {
1168
+ if (data.length === 0) throw new Error("No data to write to CSV");
1169
+ await fs_promises.writeFile(filepath, formatCsv(data), "utf-8");
1170
+ };
1171
+ /**
1172
+ * Write data to a JSONL file.
1173
+ */
1174
+ const writeJsonlFile = async (filepath, data) => {
1175
+ const lines = data.map((item) => JSON.stringify(item)).join("\n");
1176
+ await fs_promises.writeFile(filepath, lines + "\n", "utf-8");
1177
+ };
1178
+ /**
1179
+ * Write data to a file based on the file extension.
1180
+ */
1181
+ const writeToFile = async (filepath, data, format) => {
1182
+ const dir = path.dirname(filepath);
1183
+ await fs_promises.mkdir(dir, { recursive: true });
1184
+ const ext = format ?? path.extname(filepath).slice(1);
1185
+ if (format && format !== path.extname(filepath).slice(1)) logger$5.warn(`Output format ${format} does not match file extension ${path.extname(filepath).slice(1)}`);
1186
+ if (ext === "json") await writeJsonFile(filepath, data);
1187
+ else if (ext === "csv") await writeCsvFile(filepath, data);
1188
+ else if (ext === "jsonl") await writeJsonlFile(filepath, data);
1189
+ else throw new Error(`Unsupported output format: ${ext}`);
1190
+ };
1191
+ /**
1192
+ * Convert a value to a CSV-safe string.
1193
+ * - Strings and numbers pass through
1194
+ * - null/undefined become empty strings
1195
+ * - Objects and arrays are stringified to JSON
1196
+ */
1197
+ const stringifyForCsv = (value) => {
1198
+ if (value === null || value === void 0) return "";
1199
+ if (typeof value === "string") return value;
1200
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
1201
+ return JSON.stringify(value);
1202
+ };
1203
+ /**
1204
+ * Print data to console in the specified format.
1205
+ */
1206
+ const printToConsole = (data, format = "json") => {
1207
+ if (format === "json") console.log(JSON.stringify(data, null, 2));
1208
+ else if (format === "csv") {
1209
+ if (data.length === 0) {
1210
+ logger$5.error("No data to print");
1211
+ return;
1212
+ }
1213
+ console.log(formatCsv(data));
1214
+ } else if (format === "jsonl") data.forEach((item) => console.log(JSON.stringify(item)));
1215
+ else throw new Error(`Unsupported output format: ${String(format)}. (supported formats: json, csv, jsonl)`);
1216
+ };
1217
+
1218
+ //#endregion
1219
+ //#region src/utils/output.ts
1220
+ /**
1221
+ * Write structured JSON to stdout. Use this for machine-readable output
1222
+ * when --json is set.
1223
+ */
1224
+ function outputJson(data) {
1225
+ console.log(JSON.stringify(data));
1226
+ }
1227
+ /**
1228
+ * Write a JSON error to stdout and exit with code 1.
1229
+ * Use this in --json mode so agents can parse the failure.
1230
+ */
1231
+ function outputJsonError(error, exitCode = 1) {
1232
+ console.log(JSON.stringify({ error: error instanceof Error ? error.message : String(error) }));
1233
+ process.exit(exitCode);
1234
+ }
1235
+
1236
+ //#endregion
1237
+ //#region src/utils/table.ts
1238
+ const DEFAULT_TERMINAL_WIDTH = 80;
1239
+ const PADDING_RIGHT = 2;
1240
+ const noBorderChars = {
1241
+ top: "",
1242
+ "top-mid": "",
1243
+ "top-left": "",
1244
+ "top-right": "",
1245
+ bottom: "",
1246
+ "bottom-mid": "",
1247
+ "bottom-left": "",
1248
+ "bottom-right": "",
1249
+ left: "",
1250
+ "left-mid": "",
1251
+ mid: "",
1252
+ "mid-mid": "",
1253
+ right: "",
1254
+ "right-mid": "",
1255
+ middle: ""
1256
+ };
1257
+ function getTerminalWidth() {
1258
+ return process.stdout.columns || DEFAULT_TERMINAL_WIDTH;
1259
+ }
1260
+ /**
1261
+ * Calculate column widths that fit within the terminal width.
1262
+ * Distributes available space proportionally based on content width.
1263
+ */
1264
+ function fitColumnWidths(contentWidths, terminalWidth) {
1265
+ const overhead = contentWidths.length * PADDING_RIGHT;
1266
+ const totalContentWidth = contentWidths.reduce((sum, w) => sum + w, 0);
1267
+ if (totalContentWidth + overhead <= terminalWidth) return;
1268
+ const availableContent = terminalWidth - overhead;
1269
+ const minColWidth = 5;
1270
+ return contentWidths.map((w) => Math.max(minColWidth + PADDING_RIGHT, Math.floor(w / totalContentWidth * availableContent) + PADDING_RIGHT));
1271
+ }
1272
+ function truncate(str, maxLen) {
1273
+ if (maxLen < 1) return str;
1274
+ if (str.length <= maxLen) return str;
1275
+ return str.slice(0, maxLen - 1) + "…";
1276
+ }
1277
+ /**
1278
+ * Render a borderless table from column headers and row data.
1279
+ * Automatically truncates content when the table exceeds terminal width.
1280
+ */
1281
+ function renderTable(head, rows) {
1282
+ const terminalWidth = getTerminalWidth();
1283
+ const colWidths = fitColumnWidths(head.map((h, i) => rows.reduce((max, row) => Math.max(max, (row[i] ?? "").length), h.length)), terminalWidth);
1284
+ const table = new cli_table3.default({
1285
+ head: colWidths ? head.map((h, i) => truncate(h, colWidths[i] - PADDING_RIGHT)) : head,
1286
+ chars: noBorderChars,
1287
+ style: {
1288
+ "padding-left": 0,
1289
+ "padding-right": PADDING_RIGHT
1290
+ },
1291
+ ...colWidths && { colWidths }
1292
+ });
1293
+ if (colWidths) for (const row of rows) table.push(row.map((cell, i) => truncate(cell, colWidths[i] - PADDING_RIGHT)));
1294
+ else for (const row of rows) table.push(row);
1295
+ return table.toString();
1296
+ }
1297
+
1298
+ //#endregion
1299
+ //#region src/commands/dataset/index.ts
1300
+ const logger$4 = initializeLogger();
1301
+ const DEFAULT_DATASET_PULL_BATCH_SIZE = 100;
1302
+ const DEFAULT_DATASET_PUSH_BATCH_SIZE = 100;
1303
+ /**
1304
+ * Pull all data from a dataset in batches.
1305
+ */
1306
+ const pullAllData = async (client, identifier, batchSize = DEFAULT_DATASET_PULL_BATCH_SIZE, offset = 0, limit) => {
1307
+ let hasMore = true;
1308
+ let currentOffset = offset;
1309
+ const stopAt = limit !== void 0 ? offset + limit : void 0;
1310
+ const result = [];
1311
+ while (hasMore && (stopAt === void 0 || currentOffset < stopAt)) {
1312
+ const data = await client.datasets.pull({
1313
+ ...identifier,
1314
+ offset: currentOffset,
1315
+ limit: batchSize
1316
+ });
1317
+ result.push(...data.items);
1318
+ if (data.items.length === 0 || data.items.length < batchSize) hasMore = false;
1319
+ else if (stopAt !== void 0 && currentOffset + batchSize >= stopAt) hasMore = false;
1320
+ else if (data.totalCount !== void 0 && currentOffset + batchSize >= data.totalCount) hasMore = false;
1321
+ currentOffset += batchSize;
1322
+ }
1323
+ if (limit !== void 0) return result.slice(0, limit);
1324
+ return result;
1325
+ };
1326
+ /**
1327
+ * Handle datasets list command.
1328
+ */
1329
+ const handleDatasetsList = async (options) => {
1330
+ const client = new LaminarClient({
1331
+ projectApiKey: options.projectApiKey,
1332
+ baseUrl: options.baseUrl,
1333
+ port: options.port
1334
+ });
1335
+ try {
1336
+ const datasets = await client.datasets.listDatasets();
1337
+ if (options.json) {
1338
+ outputJson(datasets);
1339
+ return;
1340
+ }
1341
+ if (datasets.length === 0) {
1342
+ console.log("No datasets found.");
1343
+ return;
1344
+ }
1345
+ const rows = datasets.map((dataset) => {
1346
+ const createdAtStr = new Date(dataset.createdAt).toISOString().replace("T", " ").substring(0, 19);
1347
+ return [
1348
+ dataset.id,
1349
+ createdAtStr,
1350
+ dataset.name
1351
+ ];
1352
+ });
1353
+ console.log(renderTable([
1354
+ "ID",
1355
+ "Created At",
1356
+ "Name"
1357
+ ], rows));
1358
+ console.log(`\nTotal: ${datasets.length} dataset(s)\n`);
1359
+ } catch (error) {
1360
+ if (options.json) outputJsonError(error);
1361
+ logger$4.error(`Failed to list datasets: ${error instanceof Error ? error.message : String(error)}`);
1362
+ process.exit(1);
1363
+ }
1364
+ };
1365
+ /**
1366
+ * Handle datasets push command.
1367
+ */
1368
+ const handleDatasetsPush = async (paths, options) => {
1369
+ if (!options.name && !options.id) {
1370
+ if (options.json) outputJsonError("Either name or id must be provided");
1371
+ logger$4.error("Either name or id must be provided");
1372
+ process.exit(1);
1373
+ }
1374
+ if (options.name && options.id) {
1375
+ if (options.json) outputJsonError("Only one of name or id must be provided");
1376
+ logger$4.error("Only one of name or id must be provided");
1377
+ process.exit(1);
1378
+ }
1379
+ const client = new LaminarClient({
1380
+ projectApiKey: options.projectApiKey,
1381
+ baseUrl: options.baseUrl,
1382
+ port: options.port
1383
+ });
1384
+ try {
1385
+ const data = await loadFromPaths(paths, options.recursive);
1386
+ if (data.length === 0) {
1387
+ if (options.json) outputJsonError("No data to push");
1388
+ logger$4.error("No data to push. Skipping");
1389
+ process.exit(1);
1390
+ }
1391
+ const identifier = options.name ? { name: options.name } : { id: options.id };
1392
+ const result = await client.datasets.push({
1393
+ points: data,
1394
+ ...identifier,
1395
+ batchSize: options.batchSize ?? DEFAULT_DATASET_PUSH_BATCH_SIZE
1396
+ });
1397
+ if (options.json) {
1398
+ outputJson({
1399
+ datasetId: result?.datasetId,
1400
+ count: data.length
1401
+ });
1402
+ return;
1403
+ }
1404
+ logger$4.info(`Pushed ${data.length} data points to dataset ${options.name || options.id}`);
1405
+ } catch (error) {
1406
+ if (options.json) outputJsonError(error);
1407
+ logger$4.error(`Failed to push dataset: ${error instanceof Error ? error.message : String(error)}`);
1408
+ process.exit(1);
1409
+ }
1410
+ };
1411
+ /**
1412
+ * Handle datasets pull command.
1413
+ */
1414
+ const handleDatasetsPull = async (outputPath, options) => {
1415
+ if (!options.name && !options.id) {
1416
+ if (options.json) outputJsonError("Either name or id must be provided");
1417
+ logger$4.error("Either name or id must be provided");
1418
+ process.exit(1);
1419
+ }
1420
+ if (options.name && options.id) {
1421
+ if (options.json) outputJsonError("Only one of name or id must be provided");
1422
+ logger$4.error("Only one of name or id must be provided");
1423
+ process.exit(1);
1424
+ }
1425
+ const client = new LaminarClient({
1426
+ projectApiKey: options.projectApiKey,
1427
+ baseUrl: options.baseUrl,
1428
+ port: options.port
1429
+ });
1430
+ const identifier = options.name ? { name: options.name } : { id: options.id };
1431
+ try {
1432
+ const result = await pullAllData(client, identifier, options.batchSize ?? DEFAULT_DATASET_PULL_BATCH_SIZE, options.offset ?? 0, options.limit);
1433
+ if (outputPath) {
1434
+ await writeToFile(outputPath, result, options.outputFormat);
1435
+ if (options.json) outputJson({
1436
+ path: outputPath,
1437
+ count: result.length
1438
+ });
1439
+ else logger$4.info(`Successfully pulled ${result.length} data points to ${outputPath}`);
1440
+ } else if (options.json) outputJson(result);
1441
+ else printToConsole(result, options.outputFormat ?? "json");
1442
+ } catch (error) {
1443
+ if (options.json) outputJsonError(error);
1444
+ logger$4.error(`Failed to pull dataset: ${error instanceof Error ? error.message : String(error)}`);
1445
+ process.exit(1);
1446
+ }
1447
+ };
1448
+ /**
1449
+ * Handle datasets create command.
1450
+ */
1451
+ const handleDatasetsCreate = async (name, paths, options) => {
1452
+ const client = new LaminarClient({
1453
+ projectApiKey: options.projectApiKey,
1454
+ baseUrl: options.baseUrl,
1455
+ port: options.port
1456
+ });
1457
+ try {
1458
+ const data = await loadFromPaths(paths, options.recursive);
1459
+ if (data.length === 0) {
1460
+ if (options.json) outputJsonError("No data to push");
1461
+ logger$4.error("No data to push. Skipping");
1462
+ process.exit(1);
1463
+ }
1464
+ logger$4.info(`Pushing ${data.length} data points to dataset '${name}'...`);
1465
+ await client.datasets.push({
1466
+ points: data,
1467
+ name,
1468
+ batchSize: options.batchSize ?? DEFAULT_DATASET_PUSH_BATCH_SIZE,
1469
+ createDataset: true
1470
+ });
1471
+ logger$4.info(`Successfully pushed ${data.length} data points to dataset '${name}'`);
1472
+ } catch (error) {
1473
+ if (options.json) outputJsonError(error);
1474
+ logger$4.error(`Failed to create dataset: ${error instanceof Error ? error.message : String(error)}`);
1475
+ process.exit(1);
1476
+ }
1477
+ logger$4.info(`Pulling data from dataset '${name}'...`);
1478
+ try {
1479
+ const result = await pullAllData(client, { name }, options.batchSize ?? DEFAULT_DATASET_PULL_BATCH_SIZE, 0, void 0);
1480
+ await writeToFile(options.outputFile, result, options.outputFormat);
1481
+ if (options.json) outputJson({
1482
+ name,
1483
+ path: options.outputFile,
1484
+ count: result.length
1485
+ });
1486
+ else logger$4.info(`Successfully created dataset '${name}' and saved ${result.length} datapoints to ${options.outputFile}`);
1487
+ } catch (error) {
1488
+ if (options.json) outputJsonError(error);
1489
+ logger$4.error(`Failed to pull dataset after creation: ${error instanceof Error ? error.message : String(error)}`);
1490
+ process.exit(1);
1491
+ }
1492
+ };
1493
+
1015
1494
  //#endregion
1016
1495
  //#region src/cache-server.ts
1017
1496
  const DEFAULT_START_PORT = 35667;
@@ -1301,20 +1780,9 @@ function createSSEClient(options) {
1301
1780
  */
1302
1781
  const WORKER_MESSAGE_PREFIX = "__LMNR_WORKER__:";
1303
1782
 
1304
- //#endregion
1305
- //#region src/utils.ts
1306
- function initializeLogger(options) {
1307
- const colorize = options?.colorize ?? true;
1308
- const level = options?.level ?? process.env.LMNR_LOG_LEVEL?.toLowerCase()?.trim() ?? "info";
1309
- return (0, pino.default)({ level }, (0, pino_pretty.PinoPretty)({
1310
- colorize,
1311
- minimumLevel: level
1312
- }));
1313
- }
1314
-
1315
1783
  //#endregion
1316
1784
  //#region src/subprocess/executor.ts
1317
- const logger$2 = initializeLogger();
1785
+ const logger$3 = initializeLogger();
1318
1786
  /**
1319
1787
  * Track and kill the currently running subprocess
1320
1788
  */
@@ -1345,16 +1813,16 @@ var SubprocessManager = class {
1345
1813
  const message = JSON.parse(messageJson);
1346
1814
  switch (message.type) {
1347
1815
  case "log":
1348
- logger$2[message.level](message.message);
1816
+ logger$3[message.level](message.message);
1349
1817
  break;
1350
1818
  case "error":
1351
1819
  hasError = true;
1352
- logger$2.error(`Worker error: ${message.error}`);
1353
- if (message.stack) logger$2.error(message.stack);
1820
+ logger$3.error(`Worker error: ${message.error}`);
1821
+ if (message.stack) logger$3.error(message.stack);
1354
1822
  break;
1355
1823
  }
1356
1824
  } catch {
1357
- logger$2.debug("Failed to parse worker protocol message. Printing raw line");
1825
+ logger$3.debug("Failed to parse worker protocol message. Printing raw line");
1358
1826
  console.log(line.substring(WORKER_MESSAGE_PREFIX.length));
1359
1827
  }
1360
1828
  else console.log(line);
@@ -1367,7 +1835,7 @@ var SubprocessManager = class {
1367
1835
  if (signal) reject(/* @__PURE__ */ new Error(`Worker terminated by signal: ${signal}`));
1368
1836
  else if (code === 0) resolve(result);
1369
1837
  else {
1370
- if (!hasError) logger$2.error(`Worker exited with code ${code}`);
1838
+ if (!hasError) logger$3.error(`Worker exited with code ${code}`);
1371
1839
  reject(/* @__PURE__ */ new Error(`Worker exited with code ${code}`));
1372
1840
  }
1373
1841
  });
@@ -1389,7 +1857,7 @@ var SubprocessManager = class {
1389
1857
  this.currentProcess.kill("SIGTERM");
1390
1858
  setTimeout(() => {
1391
1859
  if (processToKill && processToKill.exitCode === null) {
1392
- logger$2.warn("Child process did not terminate, using SIGKILL");
1860
+ logger$3.warn("Child process did not terminate, using SIGKILL");
1393
1861
  processToKill.kill("SIGKILL");
1394
1862
  }
1395
1863
  }, 5e3);
@@ -1484,7 +1952,7 @@ function getWorkerCommand(filePath, options) {
1484
1952
 
1485
1953
  //#endregion
1486
1954
  //#region src/commands/dev/metadata.ts
1487
- const logger$1 = initializeLogger();
1955
+ const logger$2 = initializeLogger();
1488
1956
  const TS_JS_EXTENSIONS = [
1489
1957
  ".ts",
1490
1958
  ".tsx",
@@ -1502,7 +1970,7 @@ const EXTENSIONS_TO_DISCOVER_METADATA = [...TS_JS_EXTENSIONS, ".py"];
1502
1970
  */
1503
1971
  const METADATA_PROTOCOL_PREFIX = "LMNR_METADATA:";
1504
1972
  const logLmnrPackageNotFoundAndExit = () => {
1505
- logger$1.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");
1973
+ logger$2.error("@lmnr-ai/lmnr package not found or outdated. For JS/TS projects, please install the latest version of @lmnr-ai/lmnr in your project: npm install @lmnr-ai/lmnr\nYou might need to run `lmnr-cli` from the root of your project");
1506
1974
  process.exit(1);
1507
1975
  };
1508
1976
  /**
@@ -1532,20 +2000,20 @@ const discoverTypeScriptMetadata = async (filePath, options) => {
1532
2000
  loadModule = buildModule.loadModule;
1533
2001
  selectRolloutFunction = buildModule.selectRolloutFunction;
1534
2002
  if (!extractRolloutFunctions || !buildFile || !loadModule || !selectRolloutFunction) {
1535
- logger$1.error("Missing exports from @lmnr-ai/lmnr modules. This may indicate an outdated package version.");
2003
+ logger$2.error("Missing exports from @lmnr-ai/lmnr modules. This may indicate an outdated package version.");
1536
2004
  logLmnrPackageNotFoundAndExit();
1537
2005
  }
1538
2006
  } catch (error) {
1539
2007
  if (error.code === "MODULE_NOT_FOUND") logLmnrPackageNotFoundAndExit();
1540
- logger$1.error(`Unexpected error loading @lmnr-ai/lmnr modules: ${error.message}`);
2008
+ logger$2.error(`Unexpected error loading @lmnr-ai/lmnr modules: ${error.message}`);
1541
2009
  throw error;
1542
2010
  }
1543
2011
  let paramsMetadata;
1544
2012
  try {
1545
2013
  paramsMetadata = extractRolloutFunctions(filePath);
1546
- logger$1.debug(`Extracted TypeScript metadata for ${paramsMetadata?.size} functions`);
2014
+ logger$2.debug(`Extracted TypeScript metadata for ${paramsMetadata?.size} functions`);
1547
2015
  } catch (error) {
1548
- logger$1.warn("Failed to extract TypeScript metadata, falling back to runtime parsing: " + (error instanceof Error ? error.message : String(error)));
2016
+ logger$2.warn("Failed to extract TypeScript metadata, falling back to runtime parsing: " + (error instanceof Error ? error.message : String(error)));
1549
2017
  }
1550
2018
  const moduleText = await buildFile(filePath, {
1551
2019
  externalPackages: options.externalPackages,
@@ -1557,21 +2025,21 @@ const discoverTypeScriptMetadata = async (filePath, options) => {
1557
2025
  });
1558
2026
  const selectedFunction = selectRolloutFunction(options.function);
1559
2027
  if (paramsMetadata) {
1560
- logger$1.debug(`Available TS metadata keys: ${Array.from(paramsMetadata.keys()).join(", ")}`);
1561
- logger$1.debug(`Looking for span name: ${selectedFunction.name} (runtime key: ${selectedFunction.exportName})`);
2028
+ logger$2.debug(`Available TS metadata keys: ${Array.from(paramsMetadata.keys()).join(", ")}`);
2029
+ logger$2.debug(`Looking for span name: ${selectedFunction.name} (runtime key: ${selectedFunction.exportName})`);
1562
2030
  let foundMetadata = null;
1563
2031
  for (const [exportName, metadata] of paramsMetadata.entries()) {
1564
- logger$1.debug(`Checking ${exportName}: span name = ${metadata.name}, export name = ${exportName}`);
2032
+ logger$2.debug(`Checking ${exportName}: span name = ${metadata.name}, export name = ${exportName}`);
1565
2033
  if (metadata.name === selectedFunction.name) {
1566
2034
  foundMetadata = metadata;
1567
- logger$1.debug(`Match. Export name: ${exportName}, span name: ${metadata.name}`);
2035
+ logger$2.debug(`Match. Export name: ${exportName}, span name: ${metadata.name}`);
1568
2036
  break;
1569
2037
  }
1570
2038
  }
1571
2039
  if (foundMetadata) {
1572
2040
  selectedFunction.params = foundMetadata.params;
1573
- logger$1.debug(`Using TypeScript metadata for span: ${selectedFunction.name}`);
1574
- } else logger$1.info(`No TypeScript metadata found for span name: ${selectedFunction.name}`);
2041
+ logger$2.debug(`Using TypeScript metadata for span: ${selectedFunction.name}`);
2042
+ } else logger$2.info(`No TypeScript metadata found for span name: ${selectedFunction.name}`);
1575
2043
  }
1576
2044
  return {
1577
2045
  functionName: selectedFunction.name,
@@ -1681,7 +2149,7 @@ const extractMetadataFromStdout = (stdout) => {
1681
2149
  * Discovers function metadata for Python files/modules by calling the lmnr Python CLI
1682
2150
  */
1683
2151
  const discoverPythonMetadata = async (filePathOrModule, options) => {
1684
- logger$1.debug(`Discovering Python metadata for ${filePathOrModule}`);
2152
+ logger$2.debug(`Discovering Python metadata for ${filePathOrModule}`);
1685
2153
  const args = ["discover"];
1686
2154
  if (options.pythonModule) args.push("--module", options.pythonModule);
1687
2155
  else args.push("--file", filePathOrModule);
@@ -1694,8 +2162,8 @@ const discoverPythonMetadata = async (filePathOrModule, options) => {
1694
2162
  };
1695
2163
  } catch (error) {
1696
2164
  const errorMessage = error instanceof Error ? error.message : String(error);
1697
- logger$1.error(`Error while loading Python file/module: ${errorMessage}`);
1698
- if (errorMessage.toLowerCase().includes("command not found") || errorMessage.includes("spawn lmnr ENOENT")) logger$1.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}"`);
2165
+ logger$2.error(`Error while loading Python file/module: ${errorMessage}`);
2166
+ if (errorMessage.toLowerCase().includes("command not found") || errorMessage.includes("spawn lmnr ENOENT")) logger$2.info(`HINT: Make sure latest version of \`lmnr\` python package is installed. \`pip install --upgrade lmnr\`, or if you are running this command from a virtual environment, make sure to activate it. For \`uv\` users, rerun the command with \`uv run\`, e.g. "uv run npx lmnr-cli dev ${filePathOrModule}"`);
1699
2167
  throw error;
1700
2168
  }
1701
2169
  };
@@ -1707,7 +2175,7 @@ const discoverFunctionMetadata = async (filePathOrModule, options) => {
1707
2175
  const ext = path.extname(filePathOrModule);
1708
2176
  if (TS_JS_EXTENSIONS.includes(ext)) return await discoverTypeScriptMetadata(filePathOrModule, options);
1709
2177
  if (ext === ".py") return await discoverPythonMetadata(filePathOrModule, options);
1710
- logger$1.warn(`No metadata discovery available for ${ext} files`);
2178
+ logger$2.warn(`No metadata discovery available for ${ext} files`);
1711
2179
  return {
1712
2180
  functionName: options.function || path.basename(filePathOrModule, ext),
1713
2181
  params: []
@@ -1716,7 +2184,7 @@ const discoverFunctionMetadata = async (filePathOrModule, options) => {
1716
2184
 
1717
2185
  //#endregion
1718
2186
  //#region src/commands/dev/index.ts
1719
- const logger = initializeLogger();
2187
+ const logger$1 = initializeLogger();
1720
2188
  function newUUID() {
1721
2189
  if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return crypto.randomUUID();
1722
2190
  return (0, uuid.v4)();
@@ -1747,7 +2215,7 @@ const tryParseArg = (arg) => {
1747
2215
  * Handles a run event from the backend
1748
2216
  */
1749
2217
  const handleRunEvent = async (event, sessionId, filePathOrModule, client, cacheServerPort, cache, setMetadata, options, subprocessManager) => {
1750
- logger.debug("Received run event");
2218
+ logger$1.debug("Received run event");
1751
2219
  const { trace_id, path_to_count, args: rawArgs, overrides } = event.data;
1752
2220
  const parsedArgs = Array.isArray(rawArgs) ? rawArgs.map(tryParseArg) : Object.fromEntries(Object.entries(rawArgs).map(([key, value]) => [key, tryParseArg(value)]));
1753
2221
  cache.clear();
@@ -1756,10 +2224,10 @@ const handleRunEvent = async (event, sessionId, filePathOrModule, client, cacheS
1756
2224
  overrides
1757
2225
  });
1758
2226
  try {
1759
- if (!trace_id || trace_id.trim() === "") logger.info("No spans in cache, starting fresh");
2227
+ if (!trace_id || trace_id.trim() === "") logger$1.info("No spans in cache, starting fresh");
1760
2228
  else {
1761
2229
  const paths = Object.keys(path_to_count || {});
1762
- if (paths.length === 0) logger.info("No spans to cache, starting fresh");
2230
+ if (paths.length === 0) logger$1.info("No spans to cache, starting fresh");
1763
2231
  else {
1764
2232
  const query = `
1765
2233
  SELECT name, input, output, attributes, path
@@ -1768,12 +2236,12 @@ const handleRunEvent = async (event, sessionId, filePathOrModule, client, cacheS
1768
2236
  AND path IN {paths:String[]}
1769
2237
  ORDER BY start_time ASC
1770
2238
  `;
1771
- logger.debug(`Querying spans from trace ${trace_id}...`);
2239
+ logger$1.debug(`Querying spans from trace ${trace_id}...`);
1772
2240
  const spans = await client.sql.query(query, {
1773
2241
  traceId: trace_id,
1774
2242
  paths
1775
2243
  });
1776
- logger.debug(`Received ${spans.length} spans from backend`);
2244
+ logger$1.debug(`Received ${spans.length} spans from backend`);
1777
2245
  const spansByPath = {};
1778
2246
  for (const span of spans) {
1779
2247
  const path$2 = span.path;
@@ -1811,7 +2279,7 @@ const handleRunEvent = async (event, sessionId, filePathOrModule, client, cacheS
1811
2279
  const cacheKey = `${index}:${path$2}`;
1812
2280
  cache.set(cacheKey, cachedSpan);
1813
2281
  });
1814
- logger.info(`Cached ${spansToCache.length} spans for path: ${path$2}`);
2282
+ logger$1.info(`Cached ${spansToCache.length} spans for path: ${path$2}`);
1815
2283
  }
1816
2284
  setMetadata({
1817
2285
  pathToCount: path_to_count || {},
@@ -1850,7 +2318,7 @@ const handleRunEvent = async (event, sessionId, filePathOrModule, client, cacheS
1850
2318
  status: "RUNNING"
1851
2319
  });
1852
2320
  } catch (error) {
1853
- logger.error(`Error setting debugger session status: ${error instanceof Error ? error.message : error}`);
2321
+ logger$1.error(`Error setting debugger session status: ${error instanceof Error ? error.message : error}`);
1854
2322
  }
1855
2323
  await subprocessManager.execute({
1856
2324
  command: workerCommand.command,
@@ -1863,18 +2331,18 @@ const handleRunEvent = async (event, sessionId, filePathOrModule, client, cacheS
1863
2331
  status: "FINISHED"
1864
2332
  });
1865
2333
  } catch (error) {
1866
- logger.error(`Error setting debugger session status: ${error instanceof Error ? error.message : error}`);
2334
+ logger$1.error(`Error setting debugger session status: ${error instanceof Error ? error.message : error}`);
1867
2335
  }
1868
2336
  } catch (error) {
1869
- logger.error(`Error handling run event: ${error instanceof Error ? error.message : error}`);
1870
- if (error instanceof Error && error.stack) logger.error(error.stack);
2337
+ logger$1.error(`Error handling run event: ${error instanceof Error ? error.message : error}`);
2338
+ if (error instanceof Error && error.stack) logger$1.error(error.stack);
1871
2339
  try {
1872
2340
  await client.rolloutSessions.setStatus({
1873
2341
  sessionId,
1874
2342
  status: "FINISHED"
1875
2343
  });
1876
2344
  } catch (error$1) {
1877
- logger.error(`Error setting debugger session status: ${error$1 instanceof Error ? error$1.message : error$1}`);
2345
+ logger$1.error(`Error setting debugger session status: ${error$1 instanceof Error ? error$1.message : error$1}`);
1878
2346
  }
1879
2347
  }
1880
2348
  };
@@ -1891,30 +2359,30 @@ async function runDev(filePath, options = {}) {
1891
2359
  projectApiKey: options.projectApiKey,
1892
2360
  port: options.port
1893
2361
  });
1894
- logger.debug("Starting cache server...");
2362
+ logger$1.debug("Starting cache server...");
1895
2363
  const { port: cacheServerPort, server: cacheServer, cache, setMetadata } = await startCacheServer();
1896
- logger.debug(`Cache server started on port ${cacheServerPort}`);
2364
+ logger$1.debug(`Cache server started on port ${cacheServerPort}`);
1897
2365
  const subprocessManager = new SubprocessManager();
1898
2366
  let functionName = options.function;
1899
2367
  let params = [];
1900
2368
  try {
1901
2369
  if (isPythonModule || filePath && EXTENSIONS_TO_DISCOVER_METADATA.includes(path.extname(filePath))) {
1902
- logger.debug("Discovering entrypoint functions...");
2370
+ logger$1.debug("Discovering entrypoint functions...");
1903
2371
  const metadata = await discoverFunctionMetadata(filePathOrModule, options);
1904
2372
  functionName = metadata.functionName;
1905
2373
  params = metadata.params;
1906
- logger.info(`Serving function: ${functionName}`);
1907
- logger.debug(`Function parameters: ${JSON.stringify(params, null, 2)}`);
2374
+ logger$1.info(`Serving function: ${functionName}`);
2375
+ logger$1.debug(`Function parameters: ${JSON.stringify(params, null, 2)}`);
1908
2376
  } else if (filePath) {
1909
2377
  functionName = options.function || path.basename(filePath, path.extname(filePath));
1910
- logger.warn(`Metadata discovery not available for ${path.extname(filePath)} files`);
2378
+ logger$1.warn(`Metadata discovery not available for ${path.extname(filePath)} files`);
1911
2379
  }
1912
2380
  } catch (error) {
1913
- logger.error("Failed to discover entrypoint functions: " + (error instanceof Error ? error.message : String(error)));
2381
+ logger$1.error("Failed to discover entrypoint functions: " + (error instanceof Error ? error.message : String(error)));
1914
2382
  cacheServer.close();
1915
2383
  throw error;
1916
2384
  }
1917
- logger.debug("Setting up file watcher...");
2385
+ logger$1.debug("Setting up file watcher...");
1918
2386
  const watcher = chokidar.default.watch(".", {
1919
2387
  ignored: (path$2) => {
1920
2388
  const ignoredDirs = [
@@ -1949,7 +2417,7 @@ async function runDev(filePath, options = {}) {
1949
2417
  pollInterval: 100
1950
2418
  }
1951
2419
  });
1952
- logger.debug("Setting up SSE client...");
2420
+ logger$1.debug("Setting up SSE client...");
1953
2421
  let sseClient = null;
1954
2422
  try {
1955
2423
  sseClient = createSSEClient({
@@ -1961,45 +2429,45 @@ async function runDev(filePath, options = {}) {
1961
2429
  let currentRunPromise = null;
1962
2430
  let stopRequested = false;
1963
2431
  sseClient.on("heartbeat", () => {
1964
- logger.debug("Heartbeat received");
2432
+ logger$1.debug("Heartbeat received");
1965
2433
  });
1966
2434
  sseClient.on("run", (event) => {
1967
2435
  if (currentRunPromise !== null) {
1968
- logger.warn("Already processing a run event, skipping new run");
2436
+ logger$1.warn("Already processing a run event, skipping new run");
1969
2437
  return;
1970
2438
  }
1971
2439
  currentRunPromise = (async () => {
1972
2440
  try {
1973
2441
  stopRequested = false;
1974
2442
  if (reloadScheduled) {
1975
- logger.info("Reloading function metadata before run...");
2443
+ logger$1.info("Reloading function metadata before run...");
1976
2444
  if (isPythonModule || filePath && EXTENSIONS_TO_DISCOVER_METADATA.includes(path.extname(filePath))) try {
1977
2445
  const metadata = await discoverFunctionMetadata(filePathOrModule, options);
1978
2446
  if (stopRequested) {
1979
- logger.info("Run cancelled during metadata discovery");
2447
+ logger$1.info("Run cancelled during metadata discovery");
1980
2448
  return;
1981
2449
  }
1982
- logger.debug(`Updated function metadata: ${metadata.functionName}`);
1983
- logger.debug(`Updated parameters: ${JSON.stringify(metadata.params, null, 2)}`);
2450
+ logger$1.debug(`Updated function metadata: ${metadata.functionName}`);
2451
+ logger$1.debug(`Updated parameters: ${JSON.stringify(metadata.params, null, 2)}`);
1984
2452
  if (sseClient) {
1985
2453
  sseClient.updateMetadata(metadata.params, metadata.functionName);
1986
- logger.debug("Notified backend of metadata changes");
2454
+ logger$1.debug("Notified backend of metadata changes");
1987
2455
  }
1988
2456
  reloadScheduled = false;
1989
2457
  } catch (error) {
1990
- logger.error("Failed to update function metadata: " + (error instanceof Error ? error.message : String(error)));
1991
- if (error instanceof Error && error.stack) logger.debug(`Stack trace: ${error.stack}`);
2458
+ logger$1.error("Failed to update function metadata: " + (error instanceof Error ? error.message : String(error)));
2459
+ if (error instanceof Error && error.stack) logger$1.debug(`Stack trace: ${error.stack}`);
1992
2460
  return;
1993
2461
  }
1994
2462
  else reloadScheduled = false;
1995
2463
  }
1996
2464
  if (stopRequested) {
1997
- logger.info("Run cancelled before execution");
2465
+ logger$1.info("Run cancelled before execution");
1998
2466
  return;
1999
2467
  }
2000
2468
  await handleRunEvent(event, sessionId, filePathOrModule, client, cacheServerPort, cache, setMetadata, options, subprocessManager);
2001
2469
  } catch (error) {
2002
- logger.error("Unhandled error in run event handler: " + (error instanceof Error ? error.message : String(error)));
2470
+ logger$1.error("Unhandled error in run event handler: " + (error instanceof Error ? error.message : String(error)));
2003
2471
  } finally {
2004
2472
  currentRunPromise = null;
2005
2473
  }
@@ -2009,65 +2477,65 @@ async function runDev(filePath, options = {}) {
2009
2477
  const projectId = event.data.project_id;
2010
2478
  const sessionId$1 = event.data.session_id;
2011
2479
  const frontendUrl = getFrontendUrl(options.baseUrl, options.frontendPort);
2012
- if (!didLogHandshake) logger.info(`View your session at ${frontendUrl}/project/${projectId}/debugger-sessions/${sessionId$1}`);
2480
+ if (!didLogHandshake) logger$1.info(`View your session at ${frontendUrl}/project/${projectId}/debugger-sessions/${sessionId$1}`);
2013
2481
  didLogHandshake = true;
2014
2482
  });
2015
2483
  sseClient.on("error", (error) => {
2016
- logger.warn(`Error connecting to backend: ${error.message}`);
2484
+ logger$1.warn(`Error connecting to backend: ${error.message}`);
2017
2485
  });
2018
2486
  sseClient.on("reconnecting", () => {
2019
- logger.info("Reconnecting to backend...");
2487
+ logger$1.info("Reconnecting to backend...");
2020
2488
  });
2021
2489
  sseClient.on("heartbeat_timeout", () => {
2022
- logger.debug("Heartbeat timeout, reconnecting...");
2490
+ logger$1.debug("Heartbeat timeout, reconnecting...");
2023
2491
  });
2024
2492
  sseClient.on("stop", () => {
2025
- logger.debug("Stop event received");
2493
+ logger$1.debug("Stop event received");
2026
2494
  stopRequested = true;
2027
- if (subprocessManager.kill()) logger.info("Current run cancelled");
2495
+ if (subprocessManager.kill()) logger$1.info("Current run cancelled");
2028
2496
  });
2029
2497
  let reloadTimeout = null;
2030
2498
  let reloadScheduled = false;
2031
2499
  watcher.on("change", (changedPath) => {
2032
- logger.info(`File changed: ${changedPath}, scheduling reload...`);
2500
+ logger$1.info(`File changed: ${changedPath}, scheduling reload...`);
2033
2501
  if (reloadTimeout) clearTimeout(reloadTimeout);
2034
2502
  reloadTimeout = setTimeout(() => {
2035
- logger.debug("Marking reload as scheduled for next run...");
2503
+ logger$1.debug("Marking reload as scheduled for next run...");
2036
2504
  reloadTimeout = null;
2037
2505
  reloadScheduled = true;
2038
2506
  }, 100);
2039
2507
  });
2040
2508
  const shutdown = () => {
2041
- logger.debug("Shutting down...");
2509
+ logger$1.debug("Shutting down...");
2042
2510
  if (reloadTimeout) {
2043
2511
  clearTimeout(reloadTimeout);
2044
2512
  reloadTimeout = null;
2045
2513
  }
2046
2514
  reloadScheduled = false;
2047
- logger.debug("Closing file watcher...");
2515
+ logger$1.debug("Closing file watcher...");
2048
2516
  watcher.close().catch((error) => {
2049
- logger.error(`Failed to close file watcher: ${error instanceof Error ? error.message : error}`);
2517
+ logger$1.error(`Failed to close file watcher: ${error instanceof Error ? error.message : error}`);
2050
2518
  });
2051
2519
  subprocessManager.kill();
2052
- logger.debug("Deleting debugger session...");
2520
+ logger$1.debug("Deleting debugger session...");
2053
2521
  client.rolloutSessions.delete({ sessionId }).then(() => {
2054
2522
  if (sseClient) sseClient.shutdown();
2055
2523
  cacheServer.close(() => {
2056
- logger.debug("Cache server closed");
2524
+ logger$1.debug("Cache server closed");
2057
2525
  });
2058
2526
  process.exit(0);
2059
2527
  }).catch((error) => {
2060
- logger.warn(`Failed to delete debugger session: ${error instanceof Error ? error.message : error}`);
2528
+ logger$1.warn(`Failed to delete debugger session: ${error instanceof Error ? error.message : error}`);
2061
2529
  process.exit(1);
2062
2530
  });
2063
2531
  };
2064
2532
  process.on("SIGINT", shutdown);
2065
2533
  process.on("SIGTERM", shutdown);
2066
2534
  process.stdin.resume();
2067
- logger.debug("Connecting to backend...");
2535
+ logger$1.debug("Connecting to backend...");
2068
2536
  await sseClient.connectAndListen();
2069
2537
  } catch (error) {
2070
- logger.error("Failed to start dev command: " + (error instanceof Error ? error.message : String(error)));
2538
+ logger$1.error("Failed to start dev command: " + (error instanceof Error ? error.message : String(error)));
2071
2539
  try {
2072
2540
  await client.rolloutSessions.delete({ sessionId });
2073
2541
  } catch {}
@@ -2078,11 +2546,88 @@ async function runDev(filePath, options = {}) {
2078
2546
  }
2079
2547
  }
2080
2548
 
2549
+ //#endregion
2550
+ //#region src/commands/sql/index.ts
2551
+ const logger = initializeLogger();
2552
+ const handleSqlQuery = async (query, options) => {
2553
+ const client = new LaminarClient({
2554
+ projectApiKey: options.projectApiKey,
2555
+ baseUrl: options.baseUrl,
2556
+ port: options.port
2557
+ });
2558
+ try {
2559
+ const rows = await client.sql.query(query);
2560
+ if (options.json) {
2561
+ outputJson(rows);
2562
+ return;
2563
+ }
2564
+ if (rows.length === 0) {
2565
+ console.log("No rows returned.");
2566
+ return;
2567
+ }
2568
+ const columns = Object.keys(rows[0]);
2569
+ const tableRows = rows.map((row) => columns.map((col) => String(row[col] ?? "")));
2570
+ console.log(renderTable(columns, tableRows));
2571
+ console.log(`\n${rows.length} row(s)\n`);
2572
+ } catch (error) {
2573
+ if (options.json) outputJsonError(error);
2574
+ logger.error(`Query failed: ${error instanceof Error ? error.message : String(error)}`);
2575
+ process.exit(1);
2576
+ }
2577
+ };
2578
+
2579
+ //#endregion
2580
+ //#region src/commands/sql/schema.ts
2581
+ const SQL_SCHEMA_HELP = `
2582
+ Available tables:
2583
+ spans
2584
+ span_id (UUID), name (String), span_type (String: DEFAULT|LLM|TOOL),
2585
+ start_time (DateTime64), end_time (DateTime64), duration (Float64),
2586
+ input_cost (Float64), output_cost (Float64), total_cost (Float64),
2587
+ input_tokens (Int64), output_tokens (Int64), total_tokens (Int64),
2588
+ request_model (String), response_model (String), model (String),
2589
+ trace_id (UUID), provider (String), path (String),
2590
+ input (String), output (String), status (String),
2591
+ parent_span_id (UUID), attributes (String), tags (Array(String))
2592
+
2593
+ traces
2594
+ id (UUID), start_time (DateTime64), end_time (DateTime64),
2595
+ input_tokens (Int64), output_tokens (Int64), total_tokens (Int64),
2596
+ input_cost (Float64), output_cost (Float64), total_cost (Float64),
2597
+ duration (Float64), metadata (String), session_id (String),
2598
+ user_id (String), status (String), top_span_id (UUID),
2599
+ top_span_name (String), top_span_type (String), trace_type (String),
2600
+ tags (Array(String)), has_browser_session (Bool)
2601
+
2602
+ events
2603
+ id (UUID), type (String), name (String), span_id (UUID),
2604
+ timestamp (DateTime64), attributes (String)
2605
+
2606
+ signal_events
2607
+ id (UUID), signal_id (UUID), trace_id (UUID), run_id (UUID),
2608
+ name (String), payload (String), timestamp (DateTime64)
2609
+
2610
+ signal_runs
2611
+ signal_id (UUID), job_id (UUID), trigger_id (UUID), run_id (UUID),
2612
+ trace_id (UUID), status (String), event_id (UUID), updated_at (DateTime64)
2613
+
2614
+ evaluation_datapoints
2615
+ id (UUID), evaluation_id (UUID), data (String), target (String),
2616
+ metadata (String), executor_output (String), index (UInt64),
2617
+ trace_id (UUID), group_id (String), scores (String),
2618
+ created_at (DateTime64), dataset_id (UUID),
2619
+ dataset_datapoint_id (UUID), dataset_datapoint_created_at (DateTime64)
2620
+
2621
+ dataset_datapoints
2622
+ id (UUID), created_at (DateTime64), dataset_id (UUID),
2623
+ data (String), target (String), metadata (String)
2624
+ `;
2625
+
2081
2626
  //#endregion
2082
2627
  //#region src/index.ts
2083
2628
  async function main() {
2084
2629
  const program = new commander.Command();
2085
- program.name("lmnr-cli").description("CLI for Laminar agent debugger").version(version$1, "-v, --version", "display version number");
2630
+ program.name("lmnr-cli").description("CLI for the Laminar agent observability platform").version(version$1, "-v, --version", "display version number");
2086
2631
  program.command("dev").description("Start a debugging session").argument("[file]", "Path to file containing the entrypoint function(s). Either `file` or `-m` must be provided.").option("-m, --python-module <module>", "Python module path (e.g., src.myfile). Either `file` or `-m` must be provided.").option("--function <name>", "Specific function to serve (if multiple entrypoint functions found)").option("--project-api-key <key>", "Project API key. If not provided, reads from LMNR_PROJECT_API_KEY env variable").option("--base-url <url>", "Base URL for the Laminar API. Defaults to https://api.lmnr.ai or LMNR_BASE_URL env variable").option("--port <port>", "Port for the Laminar API. Defaults to 443", (val) => parseInt(val, 10)).option("--grpc-port <port>", "Port for the Laminar gRPC backend. Defaults to 8443", (val) => parseInt(val, 10)).option("--frontend-port <port>", "Port for the Laminar frontend. Defaults to 5667", (val) => parseInt(val, 10)).option("--external-packages <packages...>", "[ADVANCED] List of packages to pass as external to esbuild. This will not link the packages directly into the dev file, but will instead require them at runtime. Read more: https://esbuild.github.io/api/#external").option("--dynamic-imports-to-skip <modules...>", "[ADVANCED] List of module names to skip when encountered as dynamic imports. These dynamic imports will resolve to an empty module to prevent build failures. This is meant to skip the imports that are not used in the entrypoint function itself.").option("--command <command>", "[ADVANCED] Custom command to run the worker (e.g., python3, node)").option("--command-args <args...>", "[ADVANCED] Arguments for the custom command").action(async (file, options) => {
2087
2632
  if (!file && !options.pythonModule) {
2088
2633
  console.error("Error: Must provide either a file path or --python-module (-m) flag");
@@ -2099,6 +2644,43 @@ Examples:
2099
2644
  $ lmnr-cli dev agent.py # Python file (script mode)
2100
2645
  $ lmnr-cli dev -m src.agent # Python module (module mode)
2101
2646
  $ lmnr-cli dev agent.ts --function myAgent # Specific function
2647
+ `);
2648
+ const datasetsCmd = program.command("dataset").description("Manage datasets").option("--project-api-key <key>", "Project API key. If not provided, reads from LMNR_PROJECT_API_KEY env variable").option("--base-url <url>", "Base URL for the Laminar API. Defaults to https://api.lmnr.ai or LMNR_BASE_URL env variable").option("--port <port>", "Port for the Laminar API. Defaults to 443", (val) => parseInt(val, 10)).option("--json", "Output structured JSON to stdout");
2649
+ datasetsCmd.command("list").description("List all datasets").action(async (_options, cmd) => {
2650
+ await handleDatasetsList(cmd.optsWithGlobals());
2651
+ });
2652
+ datasetsCmd.command("push").description("Push datapoints to an existing dataset").argument("<paths...>", "Paths to files or directories containing data to push").option("-n, --name <name>", "Name of the dataset (either name or id must be provided)").option("--id <id>", "ID of the dataset (either name or id must be provided)").option("-r, --recursive", "Recursively read files in directories", false).option("--batch-size <size>", "Batch size for pushing data", (val) => parseInt(val, 10), 100).action(async (paths, _options, cmd) => {
2653
+ await handleDatasetsPush(paths, cmd.optsWithGlobals());
2654
+ });
2655
+ datasetsCmd.command("pull").description("Pull data from a dataset").argument("[output-path]", "Path to save the data. If not provided, prints to console").option("-n, --name <name>", "Name of the dataset (either name or id must be provided)").option("--id <id>", "ID of the dataset (either name or id must be provided)").option("--output-format <format>", "Output format (json, csv, jsonl). Inferred from file extension if not provided").option("--batch-size <size>", "Batch size for pulling data", (val) => parseInt(val, 10), 100).option("--limit <limit>", "Limit number of datapoints to pull", (val) => parseInt(val, 10)).option("--offset <offset>", "Offset for pagination", (val) => parseInt(val, 10), 0).action(async (outputPath, _options, cmd) => {
2656
+ await handleDatasetsPull(outputPath, cmd.optsWithGlobals());
2657
+ });
2658
+ datasetsCmd.command("create").description("Create a dataset from input files").argument("<name>", "Name of the dataset to create").argument("<paths...>", "Paths to files or directories containing data to push").requiredOption("-o, --output-file <file>", "Path to save the pulled data").option("--output-format <format>", "Output format (json, csv, jsonl). Inferred from file extension if not provided").option("-r, --recursive", "Recursively read files in directories", false).option("--batch-size <size>", "Batch size for pushing/pulling data", (val) => parseInt(val, 10), 100).action(async (name, paths, _options, cmd) => {
2659
+ await handleDatasetsCreate(name, paths, cmd.optsWithGlobals());
2660
+ });
2661
+ const sqlCmd = program.command("sql").description("Run SQL queries against your Laminar project data").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");
2662
+ sqlCmd.command("query").description("Execute a SQL query").argument("<query>", "SQL query string").action(async (query, _options, cmd) => {
2663
+ await handleSqlQuery(query, cmd.optsWithGlobals());
2664
+ }).addHelpText("after", SQL_SCHEMA_HELP + `
2665
+ Examples:
2666
+ $ lmnr-cli sql query "SELECT * FROM spans LIMIT 10"
2667
+ $ lmnr-cli sql query "SELECT id, total_cost, status FROM traces LIMIT 20"
2668
+ $ lmnr-cli sql query "SELECT * FROM spans LIMIT 10" --json
2669
+ `);
2670
+ sqlCmd.command("schema").description("Show available tables and their columns").action(() => {
2671
+ process.stdout.write(SQL_SCHEMA_HELP);
2672
+ });
2673
+ program.addHelpText("after", `
2674
+ Examples:
2675
+ lmnr-cli dev agent.ts # Debugger TypeScript entrypoint
2676
+ lmnr-cli dev agent.py # Debugger Python script mode
2677
+ lmnr-cli dev -m src.agent # Debugger Python module mode
2678
+ lmnr-cli dataset list --json # List all datasets
2679
+ lmnr-cli dataset push data.jsonl -n my-dataset --json # Push data to a dataset
2680
+ lmnr-cli dataset pull output.jsonl -n my-dataset --json # Pull data from a dataset
2681
+ lmnr-cli sql query "SELECT * FROM spans LIMIT 10" --json # Query spans
2682
+ lmnr-cli sql query "SELECT t.id, s.name FROM traces t JOIN spans s ON t.id = s.trace_id" --json
2683
+ lmnr-cli sql schema # Show available tables
2102
2684
  `);
2103
2685
  await program.parseAsync();
2104
2686
  }