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 +676 -94
- package/dist/index.cjs.map +1 -1
- package/package.json +9 -4
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.
|
|
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.
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
1816
|
+
logger$3[message.level](message.message);
|
|
1349
1817
|
break;
|
|
1350
1818
|
case "error":
|
|
1351
1819
|
hasError = true;
|
|
1352
|
-
logger$
|
|
1353
|
-
if (message.stack) logger$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
2014
|
+
logger$2.debug(`Extracted TypeScript metadata for ${paramsMetadata?.size} functions`);
|
|
1547
2015
|
} catch (error) {
|
|
1548
|
-
logger$
|
|
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$
|
|
1561
|
-
logger$
|
|
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$
|
|
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$
|
|
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$
|
|
1574
|
-
} else logger$
|
|
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$
|
|
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$
|
|
1698
|
-
if (errorMessage.toLowerCase().includes("command not found") || errorMessage.includes("spawn lmnr ENOENT")) logger$
|
|
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$
|
|
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
|
|
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
|
}
|