lmnr-cli 0.1.6 → 0.1.7
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 +612 -94
- package/dist/index.cjs.map +1 -1
- package/package.json +9 -5
package/dist/index.cjs
CHANGED
|
@@ -28,12 +28,18 @@ 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);
|
|
37
43
|
let chokidar = require("chokidar");
|
|
38
44
|
chokidar = __toESM(chokidar);
|
|
39
45
|
let http = require("http");
|
|
@@ -45,7 +51,7 @@ let readline = require("readline");
|
|
|
45
51
|
readline = __toESM(readline);
|
|
46
52
|
|
|
47
53
|
//#region package.json
|
|
48
|
-
var version$1 = "0.1.
|
|
54
|
+
var version$1 = "0.1.7";
|
|
49
55
|
|
|
50
56
|
//#endregion
|
|
51
57
|
//#region ../../node_modules/.pnpm/dotenv@17.2.3/node_modules/dotenv/package.json
|
|
@@ -113,7 +119,7 @@ var require_package = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
113
119
|
//#endregion
|
|
114
120
|
//#region ../../node_modules/.pnpm/dotenv@17.2.3/node_modules/dotenv/lib/main.js
|
|
115
121
|
var require_main = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
116
|
-
const fs = require("fs");
|
|
122
|
+
const fs$1 = require("fs");
|
|
117
123
|
const path$1 = require("path");
|
|
118
124
|
const os = require("os");
|
|
119
125
|
const crypto$1 = require("crypto");
|
|
@@ -250,10 +256,10 @@ var require_main = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
250
256
|
function _vaultPath(options) {
|
|
251
257
|
let possibleVaultPath = null;
|
|
252
258
|
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`;
|
|
259
|
+
for (const filepath of options.path) if (fs$1.existsSync(filepath)) possibleVaultPath = filepath.endsWith(".vault") ? filepath : `${filepath}.vault`;
|
|
254
260
|
} else possibleVaultPath = options.path.endsWith(".vault") ? options.path : `${options.path}.vault`;
|
|
255
261
|
else possibleVaultPath = path$1.resolve(process.cwd(), ".env.vault");
|
|
256
|
-
if (fs.existsSync(possibleVaultPath)) return possibleVaultPath;
|
|
262
|
+
if (fs$1.existsSync(possibleVaultPath)) return possibleVaultPath;
|
|
257
263
|
return null;
|
|
258
264
|
}
|
|
259
265
|
function _resolveHome(envPath) {
|
|
@@ -287,7 +293,7 @@ var require_main = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
287
293
|
let lastError;
|
|
288
294
|
const parsedAll = {};
|
|
289
295
|
for (const path$2 of optionPaths) try {
|
|
290
|
-
const parsed = DotenvModule.parse(fs.readFileSync(path$2, { encoding }));
|
|
296
|
+
const parsed = DotenvModule.parse(fs$1.readFileSync(path$2, { encoding }));
|
|
291
297
|
DotenvModule.populate(parsedAll, parsed, options);
|
|
292
298
|
} catch (e) {
|
|
293
299
|
if (debug) _debug(`Failed to load ${path$2} ${e.message}`);
|
|
@@ -392,7 +398,7 @@ var require_main = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
392
398
|
//#endregion
|
|
393
399
|
//#region ../client/dist/index.mjs
|
|
394
400
|
var import_main = require_main();
|
|
395
|
-
var version = "0.8.
|
|
401
|
+
var version = "0.8.15";
|
|
396
402
|
function getLangVersion() {
|
|
397
403
|
if (typeof process !== "undefined" && process.versions && process.versions.node) return `node-${process.versions.node}`;
|
|
398
404
|
if (typeof navigator !== "undefined" && navigator.userAgent) return `browser-${navigator.userAgent}`;
|
|
@@ -500,7 +506,7 @@ const loadEnv = (options) => {
|
|
|
500
506
|
};
|
|
501
507
|
const logger$1$1 = initializeLogger$1();
|
|
502
508
|
const DEFAULT_DATASET_PULL_LIMIT = 100;
|
|
503
|
-
const DEFAULT_DATASET_PUSH_BATCH_SIZE = 100;
|
|
509
|
+
const DEFAULT_DATASET_PUSH_BATCH_SIZE$1 = 100;
|
|
504
510
|
var DatasetsResource = class extends BaseResource {
|
|
505
511
|
constructor(baseHttpUrl, projectApiKey) {
|
|
506
512
|
super(baseHttpUrl, projectApiKey);
|
|
@@ -544,7 +550,7 @@ var DatasetsResource = class extends BaseResource {
|
|
|
544
550
|
* @param {boolean} [options.createDataset] - Whether to create the dataset if it doesn't exist
|
|
545
551
|
* @returns {Promise<PushDatapointsResponse | undefined>}
|
|
546
552
|
*/
|
|
547
|
-
async push({ points, name, id, batchSize = DEFAULT_DATASET_PUSH_BATCH_SIZE, createDataset = false }) {
|
|
553
|
+
async push({ points, name, id, batchSize = DEFAULT_DATASET_PUSH_BATCH_SIZE$1, createDataset = false }) {
|
|
548
554
|
if (!name && !id) throw new Error("Either name or id must be provided");
|
|
549
555
|
if (name && id) throw new Error("Only one of name or id must be provided");
|
|
550
556
|
if (createDataset && !name) throw new Error("Name must be provided when creating a new dataset");
|
|
@@ -601,7 +607,7 @@ var DatasetsResource = class extends BaseResource {
|
|
|
601
607
|
return response.json();
|
|
602
608
|
}
|
|
603
609
|
};
|
|
604
|
-
const logger$
|
|
610
|
+
const logger$6 = initializeLogger$1();
|
|
605
611
|
const INITIAL_EVALUATION_DATAPOINT_MAX_DATA_LENGTH = 16e6;
|
|
606
612
|
var EvalsResource = class extends BaseResource {
|
|
607
613
|
constructor(baseHttpUrl, projectApiKey) {
|
|
@@ -737,7 +743,7 @@ var EvalsResource = class extends BaseResource {
|
|
|
737
743
|
* @returns {Promise<GetDatapointsResponse>} Response from the datapoint retrieval
|
|
738
744
|
*/
|
|
739
745
|
async getDatapoints({ datasetName, offset, limit }) {
|
|
740
|
-
logger$
|
|
746
|
+
logger$6.warn("evals.getDatapoints() is deprecated. Use client.datasets.pull() instead.");
|
|
741
747
|
const params = new URLSearchParams({
|
|
742
748
|
name: datasetName,
|
|
743
749
|
offset: offset.toString(),
|
|
@@ -754,7 +760,7 @@ var EvalsResource = class extends BaseResource {
|
|
|
754
760
|
let length = initialLength;
|
|
755
761
|
let lastResponse = null;
|
|
756
762
|
for (let i = 0; i < maxRetries; i++) {
|
|
757
|
-
logger$
|
|
763
|
+
logger$6.debug(`Retrying save datapoints... ${i + 1} of ${maxRetries}, length: ${length}`);
|
|
758
764
|
const response = await fetch(this.baseHttpUrl + `/v1/evals/${evalId}/datapoints`, {
|
|
759
765
|
method: "POST",
|
|
760
766
|
headers: this.headers(),
|
|
@@ -1012,6 +1018,410 @@ var LaminarClient = class {
|
|
|
1012
1018
|
}
|
|
1013
1019
|
};
|
|
1014
1020
|
|
|
1021
|
+
//#endregion
|
|
1022
|
+
//#region src/utils/logger.ts
|
|
1023
|
+
function initializeLogger(options) {
|
|
1024
|
+
const colorize = options?.colorize ?? true;
|
|
1025
|
+
const level = options?.level ?? process.env.LMNR_LOG_LEVEL?.toLowerCase()?.trim() ?? "info";
|
|
1026
|
+
return (0, pino.default)({ level }, (0, pino_pretty.PinoPretty)({
|
|
1027
|
+
colorize,
|
|
1028
|
+
minimumLevel: level,
|
|
1029
|
+
destination: 2
|
|
1030
|
+
}));
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
//#endregion
|
|
1034
|
+
//#region src/utils/file.ts
|
|
1035
|
+
const logger$5 = initializeLogger();
|
|
1036
|
+
/**
|
|
1037
|
+
* Check if a file has a supported extension.
|
|
1038
|
+
*/
|
|
1039
|
+
const isSupportedFile = (file) => {
|
|
1040
|
+
const ext = path.extname(file).toLowerCase();
|
|
1041
|
+
return [
|
|
1042
|
+
".json",
|
|
1043
|
+
".csv",
|
|
1044
|
+
".jsonl"
|
|
1045
|
+
].includes(ext);
|
|
1046
|
+
};
|
|
1047
|
+
/**
|
|
1048
|
+
* Collect all supported files from the given paths.
|
|
1049
|
+
* Handles both files and directories.
|
|
1050
|
+
*/
|
|
1051
|
+
const collectFiles = async (paths, recursive = false) => {
|
|
1052
|
+
const collectedFiles = [];
|
|
1053
|
+
for (const filepath of paths) try {
|
|
1054
|
+
const stats = await fs_promises.stat(filepath);
|
|
1055
|
+
if (stats.isFile()) if (isSupportedFile(filepath)) collectedFiles.push(filepath);
|
|
1056
|
+
else logger$5.warn(`Skipping unsupported file type: ${filepath}`);
|
|
1057
|
+
else if (stats.isDirectory()) {
|
|
1058
|
+
const entries = await fs_promises.readdir(filepath);
|
|
1059
|
+
for (const entry of entries) {
|
|
1060
|
+
const fullPath = path.join(filepath, entry);
|
|
1061
|
+
const entryStats = await fs_promises.stat(fullPath);
|
|
1062
|
+
if (entryStats.isFile() && isSupportedFile(fullPath)) collectedFiles.push(fullPath);
|
|
1063
|
+
else if (recursive && entryStats.isDirectory()) {
|
|
1064
|
+
const subFiles = await collectFiles([fullPath], true);
|
|
1065
|
+
collectedFiles.push(...subFiles);
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
} catch (error) {
|
|
1070
|
+
logger$5.warn(`Path does not exist or is not accessible: ${filepath}. Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
1071
|
+
}
|
|
1072
|
+
return collectedFiles;
|
|
1073
|
+
};
|
|
1074
|
+
/**
|
|
1075
|
+
* Read a JSON file and return its contents.
|
|
1076
|
+
*/
|
|
1077
|
+
const readJsonFile = async (filepath) => {
|
|
1078
|
+
const content = await fs_promises.readFile(filepath, "utf-8");
|
|
1079
|
+
const parsed = JSON.parse(content);
|
|
1080
|
+
return Array.isArray(parsed) ? parsed : [parsed];
|
|
1081
|
+
};
|
|
1082
|
+
/**
|
|
1083
|
+
* Try to parse a string as JSON. If it fails, return the original string.
|
|
1084
|
+
*/
|
|
1085
|
+
const tryParseJson = (content) => {
|
|
1086
|
+
if (typeof content !== "string") return content;
|
|
1087
|
+
const trimmed = content.trim();
|
|
1088
|
+
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return content;
|
|
1089
|
+
try {
|
|
1090
|
+
return JSON.parse(content);
|
|
1091
|
+
} catch (error) {
|
|
1092
|
+
logger$5.debug(`Error parsing JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
1093
|
+
return content;
|
|
1094
|
+
}
|
|
1095
|
+
};
|
|
1096
|
+
/**
|
|
1097
|
+
* Parse each field in a CSV row, attempting to convert JSON strings back to objects.
|
|
1098
|
+
*/
|
|
1099
|
+
const parseCsvRow = (row) => {
|
|
1100
|
+
const parsed = {};
|
|
1101
|
+
for (const [key, value] of Object.entries(row)) parsed[key] = tryParseJson(value);
|
|
1102
|
+
return parsed;
|
|
1103
|
+
};
|
|
1104
|
+
/**
|
|
1105
|
+
* Read a CSV file and return its contents as an array of objects.
|
|
1106
|
+
*/
|
|
1107
|
+
const readCsvFile = async (filepath) => new Promise((resolve, reject) => {
|
|
1108
|
+
const results = [];
|
|
1109
|
+
(0, fs.createReadStream)(filepath).pipe((0, csv_parser.default)()).on("data", (data) => results.push(parseCsvRow(data))).on("end", () => resolve(results)).on("error", reject);
|
|
1110
|
+
});
|
|
1111
|
+
/**
|
|
1112
|
+
* Read a JSONL file and return its contents as an array of objects.
|
|
1113
|
+
*/
|
|
1114
|
+
async function readJsonlFile(filepath) {
|
|
1115
|
+
return (await fs_promises.readFile(filepath, "utf-8")).split("\n").filter((line) => line.trim()).map((line) => JSON.parse(line));
|
|
1116
|
+
}
|
|
1117
|
+
/**
|
|
1118
|
+
* Read a single file and return its contents.
|
|
1119
|
+
*/
|
|
1120
|
+
async function readFile(filepath) {
|
|
1121
|
+
const ext = path.extname(filepath).toLowerCase();
|
|
1122
|
+
if (ext === ".json") return readJsonFile(filepath);
|
|
1123
|
+
else if (ext === ".csv") return readCsvFile(filepath);
|
|
1124
|
+
else if (ext === ".jsonl") return readJsonlFile(filepath);
|
|
1125
|
+
else throw new Error(`Unsupported file type: ${ext}`);
|
|
1126
|
+
}
|
|
1127
|
+
/**
|
|
1128
|
+
* Load data from all files in the specified paths.
|
|
1129
|
+
*/
|
|
1130
|
+
const loadFromPaths = async (paths, recursive = false) => {
|
|
1131
|
+
const files = await collectFiles(paths, recursive);
|
|
1132
|
+
if (files.length === 0) {
|
|
1133
|
+
logger$5.warn("No supported files found in the specified paths");
|
|
1134
|
+
return [];
|
|
1135
|
+
}
|
|
1136
|
+
logger$5.info(`Found ${files.length} file(s) to read`);
|
|
1137
|
+
const result = [];
|
|
1138
|
+
for (const file of files) try {
|
|
1139
|
+
const data = await readFile(file);
|
|
1140
|
+
result.push(...data);
|
|
1141
|
+
logger$5.info(`Read ${data.length} record(s) from ${file}`);
|
|
1142
|
+
} catch (error) {
|
|
1143
|
+
logger$5.error(`Error reading file ${file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1144
|
+
throw error;
|
|
1145
|
+
}
|
|
1146
|
+
return result;
|
|
1147
|
+
};
|
|
1148
|
+
/**
|
|
1149
|
+
* Write data to a JSON file.
|
|
1150
|
+
*/
|
|
1151
|
+
const writeJsonFile = async (filepath, data) => {
|
|
1152
|
+
const content = JSON.stringify(data, null, 2);
|
|
1153
|
+
await fs_promises.writeFile(filepath, content, "utf-8");
|
|
1154
|
+
};
|
|
1155
|
+
/**
|
|
1156
|
+
* Format data as a CSV string.
|
|
1157
|
+
*/
|
|
1158
|
+
const formatCsv = (data) => {
|
|
1159
|
+
const formattedData = data.map((item) => Object.fromEntries(Object.entries(item).map(([key, value]) => [key, stringifyForCsv(value)])));
|
|
1160
|
+
return (0, export_to_csv.asString)((0, export_to_csv.generateCsv)((0, export_to_csv.mkConfig)({ useKeysAsHeaders: true }))(formattedData));
|
|
1161
|
+
};
|
|
1162
|
+
/**
|
|
1163
|
+
* Write data to a CSV file.
|
|
1164
|
+
*/
|
|
1165
|
+
const writeCsvFile = async (filepath, data) => {
|
|
1166
|
+
if (data.length === 0) throw new Error("No data to write to CSV");
|
|
1167
|
+
await fs_promises.writeFile(filepath, formatCsv(data), "utf-8");
|
|
1168
|
+
};
|
|
1169
|
+
/**
|
|
1170
|
+
* Write data to a JSONL file.
|
|
1171
|
+
*/
|
|
1172
|
+
const writeJsonlFile = async (filepath, data) => {
|
|
1173
|
+
const lines = data.map((item) => JSON.stringify(item)).join("\n");
|
|
1174
|
+
await fs_promises.writeFile(filepath, lines + "\n", "utf-8");
|
|
1175
|
+
};
|
|
1176
|
+
/**
|
|
1177
|
+
* Write data to a file based on the file extension.
|
|
1178
|
+
*/
|
|
1179
|
+
const writeToFile = async (filepath, data, format) => {
|
|
1180
|
+
const dir = path.dirname(filepath);
|
|
1181
|
+
await fs_promises.mkdir(dir, { recursive: true });
|
|
1182
|
+
const ext = format ?? path.extname(filepath).slice(1);
|
|
1183
|
+
if (format && format !== path.extname(filepath).slice(1)) logger$5.warn(`Output format ${format} does not match file extension ${path.extname(filepath).slice(1)}`);
|
|
1184
|
+
if (ext === "json") await writeJsonFile(filepath, data);
|
|
1185
|
+
else if (ext === "csv") await writeCsvFile(filepath, data);
|
|
1186
|
+
else if (ext === "jsonl") await writeJsonlFile(filepath, data);
|
|
1187
|
+
else throw new Error(`Unsupported output format: ${ext}`);
|
|
1188
|
+
};
|
|
1189
|
+
/**
|
|
1190
|
+
* Convert a value to a CSV-safe string.
|
|
1191
|
+
* - Strings and numbers pass through
|
|
1192
|
+
* - null/undefined become empty strings
|
|
1193
|
+
* - Objects and arrays are stringified to JSON
|
|
1194
|
+
*/
|
|
1195
|
+
const stringifyForCsv = (value) => {
|
|
1196
|
+
if (value === null || value === void 0) return "";
|
|
1197
|
+
if (typeof value === "string") return value;
|
|
1198
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
1199
|
+
return JSON.stringify(value);
|
|
1200
|
+
};
|
|
1201
|
+
/**
|
|
1202
|
+
* Print data to console in the specified format.
|
|
1203
|
+
*/
|
|
1204
|
+
const printToConsole = (data, format = "json") => {
|
|
1205
|
+
if (format === "json") console.log(JSON.stringify(data, null, 2));
|
|
1206
|
+
else if (format === "csv") {
|
|
1207
|
+
if (data.length === 0) {
|
|
1208
|
+
logger$5.error("No data to print");
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
console.log(formatCsv(data));
|
|
1212
|
+
} else if (format === "jsonl") data.forEach((item) => console.log(JSON.stringify(item)));
|
|
1213
|
+
else throw new Error(`Unsupported output format: ${String(format)}. (supported formats: json, csv, jsonl)`);
|
|
1214
|
+
};
|
|
1215
|
+
|
|
1216
|
+
//#endregion
|
|
1217
|
+
//#region src/utils/output.ts
|
|
1218
|
+
/**
|
|
1219
|
+
* Write structured JSON to stdout. Use this for machine-readable output
|
|
1220
|
+
* when --json is set.
|
|
1221
|
+
*/
|
|
1222
|
+
function outputJson(data) {
|
|
1223
|
+
console.log(JSON.stringify(data));
|
|
1224
|
+
}
|
|
1225
|
+
/**
|
|
1226
|
+
* Write a JSON error to stdout and exit with code 1.
|
|
1227
|
+
* Use this in --json mode so agents can parse the failure.
|
|
1228
|
+
*/
|
|
1229
|
+
function outputJsonError(error, exitCode = 1) {
|
|
1230
|
+
console.log(JSON.stringify({ error: error instanceof Error ? error.message : String(error) }));
|
|
1231
|
+
process.exit(exitCode);
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
//#endregion
|
|
1235
|
+
//#region src/commands/dataset/index.ts
|
|
1236
|
+
const logger$4 = initializeLogger();
|
|
1237
|
+
const DEFAULT_DATASET_PULL_BATCH_SIZE = 100;
|
|
1238
|
+
const DEFAULT_DATASET_PUSH_BATCH_SIZE = 100;
|
|
1239
|
+
/**
|
|
1240
|
+
* Pull all data from a dataset in batches.
|
|
1241
|
+
*/
|
|
1242
|
+
const pullAllData = async (client, identifier, batchSize = DEFAULT_DATASET_PULL_BATCH_SIZE, offset = 0, limit) => {
|
|
1243
|
+
let hasMore = true;
|
|
1244
|
+
let currentOffset = offset;
|
|
1245
|
+
const stopAt = limit !== void 0 ? offset + limit : void 0;
|
|
1246
|
+
const result = [];
|
|
1247
|
+
while (hasMore && (stopAt === void 0 || currentOffset < stopAt)) {
|
|
1248
|
+
const data = await client.datasets.pull({
|
|
1249
|
+
...identifier,
|
|
1250
|
+
offset: currentOffset,
|
|
1251
|
+
limit: batchSize
|
|
1252
|
+
});
|
|
1253
|
+
result.push(...data.items);
|
|
1254
|
+
if (data.items.length === 0 || data.items.length < batchSize) hasMore = false;
|
|
1255
|
+
else if (stopAt !== void 0 && currentOffset + batchSize >= stopAt) hasMore = false;
|
|
1256
|
+
else if (data.totalCount !== void 0 && currentOffset + batchSize >= data.totalCount) hasMore = false;
|
|
1257
|
+
currentOffset += batchSize;
|
|
1258
|
+
}
|
|
1259
|
+
if (limit !== void 0) return result.slice(0, limit);
|
|
1260
|
+
return result;
|
|
1261
|
+
};
|
|
1262
|
+
/**
|
|
1263
|
+
* Handle datasets list command.
|
|
1264
|
+
*/
|
|
1265
|
+
const handleDatasetsList = async (options) => {
|
|
1266
|
+
const client = new LaminarClient({
|
|
1267
|
+
projectApiKey: options.projectApiKey,
|
|
1268
|
+
baseUrl: options.baseUrl,
|
|
1269
|
+
port: options.port
|
|
1270
|
+
});
|
|
1271
|
+
try {
|
|
1272
|
+
const datasets = await client.datasets.listDatasets();
|
|
1273
|
+
if (options.json) {
|
|
1274
|
+
outputJson(datasets);
|
|
1275
|
+
return;
|
|
1276
|
+
}
|
|
1277
|
+
if (datasets.length === 0) {
|
|
1278
|
+
console.log("No datasets found.");
|
|
1279
|
+
return;
|
|
1280
|
+
}
|
|
1281
|
+
const idWidth = 36;
|
|
1282
|
+
const createdAtWidth = 19;
|
|
1283
|
+
console.log(`\n${"ID".padEnd(idWidth)} ${"Created At".padEnd(createdAtWidth)} Name`);
|
|
1284
|
+
console.log(`${"-".repeat(idWidth)} ${"-".repeat(createdAtWidth)} ${"-".repeat(20)}`);
|
|
1285
|
+
for (const dataset of datasets) {
|
|
1286
|
+
const createdAtStr = new Date(dataset.createdAt).toISOString().replace("T", " ").substring(0, 19);
|
|
1287
|
+
console.log(`${dataset.id.padEnd(idWidth)} ${createdAtStr.padEnd(createdAtWidth)} ${dataset.name}`);
|
|
1288
|
+
}
|
|
1289
|
+
console.log(`\nTotal: ${datasets.length} dataset(s)\n`);
|
|
1290
|
+
} catch (error) {
|
|
1291
|
+
if (options.json) outputJsonError(error);
|
|
1292
|
+
logger$4.error(`Failed to list datasets: ${error instanceof Error ? error.message : String(error)}`);
|
|
1293
|
+
process.exit(1);
|
|
1294
|
+
}
|
|
1295
|
+
};
|
|
1296
|
+
/**
|
|
1297
|
+
* Handle datasets push command.
|
|
1298
|
+
*/
|
|
1299
|
+
const handleDatasetsPush = async (paths, options) => {
|
|
1300
|
+
if (!options.name && !options.id) {
|
|
1301
|
+
if (options.json) outputJsonError("Either name or id must be provided");
|
|
1302
|
+
logger$4.error("Either name or id must be provided");
|
|
1303
|
+
process.exit(1);
|
|
1304
|
+
}
|
|
1305
|
+
if (options.name && options.id) {
|
|
1306
|
+
if (options.json) outputJsonError("Only one of name or id must be provided");
|
|
1307
|
+
logger$4.error("Only one of name or id must be provided");
|
|
1308
|
+
process.exit(1);
|
|
1309
|
+
}
|
|
1310
|
+
const client = new LaminarClient({
|
|
1311
|
+
projectApiKey: options.projectApiKey,
|
|
1312
|
+
baseUrl: options.baseUrl,
|
|
1313
|
+
port: options.port
|
|
1314
|
+
});
|
|
1315
|
+
try {
|
|
1316
|
+
const data = await loadFromPaths(paths, options.recursive);
|
|
1317
|
+
if (data.length === 0) {
|
|
1318
|
+
if (options.json) outputJsonError("No data to push");
|
|
1319
|
+
logger$4.error("No data to push. Skipping");
|
|
1320
|
+
process.exit(1);
|
|
1321
|
+
}
|
|
1322
|
+
const identifier = options.name ? { name: options.name } : { id: options.id };
|
|
1323
|
+
const result = await client.datasets.push({
|
|
1324
|
+
points: data,
|
|
1325
|
+
...identifier,
|
|
1326
|
+
batchSize: options.batchSize ?? DEFAULT_DATASET_PUSH_BATCH_SIZE
|
|
1327
|
+
});
|
|
1328
|
+
if (options.json) {
|
|
1329
|
+
outputJson({
|
|
1330
|
+
datasetId: result?.datasetId,
|
|
1331
|
+
count: data.length
|
|
1332
|
+
});
|
|
1333
|
+
return;
|
|
1334
|
+
}
|
|
1335
|
+
logger$4.info(`Pushed ${data.length} data points to dataset ${options.name || options.id}`);
|
|
1336
|
+
} catch (error) {
|
|
1337
|
+
if (options.json) outputJsonError(error);
|
|
1338
|
+
logger$4.error(`Failed to push dataset: ${error instanceof Error ? error.message : String(error)}`);
|
|
1339
|
+
process.exit(1);
|
|
1340
|
+
}
|
|
1341
|
+
};
|
|
1342
|
+
/**
|
|
1343
|
+
* Handle datasets pull command.
|
|
1344
|
+
*/
|
|
1345
|
+
const handleDatasetsPull = async (outputPath, options) => {
|
|
1346
|
+
if (!options.name && !options.id) {
|
|
1347
|
+
if (options.json) outputJsonError("Either name or id must be provided");
|
|
1348
|
+
logger$4.error("Either name or id must be provided");
|
|
1349
|
+
process.exit(1);
|
|
1350
|
+
}
|
|
1351
|
+
if (options.name && options.id) {
|
|
1352
|
+
if (options.json) outputJsonError("Only one of name or id must be provided");
|
|
1353
|
+
logger$4.error("Only one of name or id must be provided");
|
|
1354
|
+
process.exit(1);
|
|
1355
|
+
}
|
|
1356
|
+
const client = new LaminarClient({
|
|
1357
|
+
projectApiKey: options.projectApiKey,
|
|
1358
|
+
baseUrl: options.baseUrl,
|
|
1359
|
+
port: options.port
|
|
1360
|
+
});
|
|
1361
|
+
const identifier = options.name ? { name: options.name } : { id: options.id };
|
|
1362
|
+
try {
|
|
1363
|
+
const result = await pullAllData(client, identifier, options.batchSize ?? DEFAULT_DATASET_PULL_BATCH_SIZE, options.offset ?? 0, options.limit);
|
|
1364
|
+
if (outputPath) {
|
|
1365
|
+
await writeToFile(outputPath, result, options.outputFormat);
|
|
1366
|
+
if (options.json) outputJson({
|
|
1367
|
+
path: outputPath,
|
|
1368
|
+
count: result.length
|
|
1369
|
+
});
|
|
1370
|
+
else logger$4.info(`Successfully pulled ${result.length} data points to ${outputPath}`);
|
|
1371
|
+
} else if (options.json) outputJson(result);
|
|
1372
|
+
else printToConsole(result, options.outputFormat ?? "json");
|
|
1373
|
+
} catch (error) {
|
|
1374
|
+
if (options.json) outputJsonError(error);
|
|
1375
|
+
logger$4.error(`Failed to pull dataset: ${error instanceof Error ? error.message : String(error)}`);
|
|
1376
|
+
process.exit(1);
|
|
1377
|
+
}
|
|
1378
|
+
};
|
|
1379
|
+
/**
|
|
1380
|
+
* Handle datasets create command.
|
|
1381
|
+
*/
|
|
1382
|
+
const handleDatasetsCreate = async (name, paths, options) => {
|
|
1383
|
+
const client = new LaminarClient({
|
|
1384
|
+
projectApiKey: options.projectApiKey,
|
|
1385
|
+
baseUrl: options.baseUrl,
|
|
1386
|
+
port: options.port
|
|
1387
|
+
});
|
|
1388
|
+
try {
|
|
1389
|
+
const data = await loadFromPaths(paths, options.recursive);
|
|
1390
|
+
if (data.length === 0) {
|
|
1391
|
+
if (options.json) outputJsonError("No data to push");
|
|
1392
|
+
logger$4.error("No data to push. Skipping");
|
|
1393
|
+
process.exit(1);
|
|
1394
|
+
}
|
|
1395
|
+
logger$4.info(`Pushing ${data.length} data points to dataset '${name}'...`);
|
|
1396
|
+
await client.datasets.push({
|
|
1397
|
+
points: data,
|
|
1398
|
+
name,
|
|
1399
|
+
batchSize: options.batchSize ?? DEFAULT_DATASET_PUSH_BATCH_SIZE,
|
|
1400
|
+
createDataset: true
|
|
1401
|
+
});
|
|
1402
|
+
logger$4.info(`Successfully pushed ${data.length} data points to dataset '${name}'`);
|
|
1403
|
+
} catch (error) {
|
|
1404
|
+
if (options.json) outputJsonError(error);
|
|
1405
|
+
logger$4.error(`Failed to create dataset: ${error instanceof Error ? error.message : String(error)}`);
|
|
1406
|
+
process.exit(1);
|
|
1407
|
+
}
|
|
1408
|
+
logger$4.info(`Pulling data from dataset '${name}'...`);
|
|
1409
|
+
try {
|
|
1410
|
+
const result = await pullAllData(client, { name }, options.batchSize ?? DEFAULT_DATASET_PULL_BATCH_SIZE, 0, void 0);
|
|
1411
|
+
await writeToFile(options.outputFile, result, options.outputFormat);
|
|
1412
|
+
if (options.json) outputJson({
|
|
1413
|
+
name,
|
|
1414
|
+
path: options.outputFile,
|
|
1415
|
+
count: result.length
|
|
1416
|
+
});
|
|
1417
|
+
else logger$4.info(`Successfully created dataset '${name}' and saved ${result.length} datapoints to ${options.outputFile}`);
|
|
1418
|
+
} catch (error) {
|
|
1419
|
+
if (options.json) outputJsonError(error);
|
|
1420
|
+
logger$4.error(`Failed to pull dataset after creation: ${error instanceof Error ? error.message : String(error)}`);
|
|
1421
|
+
process.exit(1);
|
|
1422
|
+
}
|
|
1423
|
+
};
|
|
1424
|
+
|
|
1015
1425
|
//#endregion
|
|
1016
1426
|
//#region src/cache-server.ts
|
|
1017
1427
|
const DEFAULT_START_PORT = 35667;
|
|
@@ -1301,20 +1711,9 @@ function createSSEClient(options) {
|
|
|
1301
1711
|
*/
|
|
1302
1712
|
const WORKER_MESSAGE_PREFIX = "__LMNR_WORKER__:";
|
|
1303
1713
|
|
|
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
1714
|
//#endregion
|
|
1316
1715
|
//#region src/subprocess/executor.ts
|
|
1317
|
-
const logger$
|
|
1716
|
+
const logger$3 = initializeLogger();
|
|
1318
1717
|
/**
|
|
1319
1718
|
* Track and kill the currently running subprocess
|
|
1320
1719
|
*/
|
|
@@ -1345,16 +1744,16 @@ var SubprocessManager = class {
|
|
|
1345
1744
|
const message = JSON.parse(messageJson);
|
|
1346
1745
|
switch (message.type) {
|
|
1347
1746
|
case "log":
|
|
1348
|
-
logger$
|
|
1747
|
+
logger$3[message.level](message.message);
|
|
1349
1748
|
break;
|
|
1350
1749
|
case "error":
|
|
1351
1750
|
hasError = true;
|
|
1352
|
-
logger$
|
|
1353
|
-
if (message.stack) logger$
|
|
1751
|
+
logger$3.error(`Worker error: ${message.error}`);
|
|
1752
|
+
if (message.stack) logger$3.error(message.stack);
|
|
1354
1753
|
break;
|
|
1355
1754
|
}
|
|
1356
1755
|
} catch {
|
|
1357
|
-
logger$
|
|
1756
|
+
logger$3.debug("Failed to parse worker protocol message. Printing raw line");
|
|
1358
1757
|
console.log(line.substring(WORKER_MESSAGE_PREFIX.length));
|
|
1359
1758
|
}
|
|
1360
1759
|
else console.log(line);
|
|
@@ -1367,7 +1766,7 @@ var SubprocessManager = class {
|
|
|
1367
1766
|
if (signal) reject(/* @__PURE__ */ new Error(`Worker terminated by signal: ${signal}`));
|
|
1368
1767
|
else if (code === 0) resolve(result);
|
|
1369
1768
|
else {
|
|
1370
|
-
if (!hasError) logger$
|
|
1769
|
+
if (!hasError) logger$3.error(`Worker exited with code ${code}`);
|
|
1371
1770
|
reject(/* @__PURE__ */ new Error(`Worker exited with code ${code}`));
|
|
1372
1771
|
}
|
|
1373
1772
|
});
|
|
@@ -1389,7 +1788,7 @@ var SubprocessManager = class {
|
|
|
1389
1788
|
this.currentProcess.kill("SIGTERM");
|
|
1390
1789
|
setTimeout(() => {
|
|
1391
1790
|
if (processToKill && processToKill.exitCode === null) {
|
|
1392
|
-
logger$
|
|
1791
|
+
logger$3.warn("Child process did not terminate, using SIGKILL");
|
|
1393
1792
|
processToKill.kill("SIGKILL");
|
|
1394
1793
|
}
|
|
1395
1794
|
}, 5e3);
|
|
@@ -1484,7 +1883,7 @@ function getWorkerCommand(filePath, options) {
|
|
|
1484
1883
|
|
|
1485
1884
|
//#endregion
|
|
1486
1885
|
//#region src/commands/dev/metadata.ts
|
|
1487
|
-
const logger$
|
|
1886
|
+
const logger$2 = initializeLogger();
|
|
1488
1887
|
const TS_JS_EXTENSIONS = [
|
|
1489
1888
|
".ts",
|
|
1490
1889
|
".tsx",
|
|
@@ -1502,7 +1901,7 @@ const EXTENSIONS_TO_DISCOVER_METADATA = [...TS_JS_EXTENSIONS, ".py"];
|
|
|
1502
1901
|
*/
|
|
1503
1902
|
const METADATA_PROTOCOL_PREFIX = "LMNR_METADATA:";
|
|
1504
1903
|
const logLmnrPackageNotFoundAndExit = () => {
|
|
1505
|
-
logger$
|
|
1904
|
+
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
1905
|
process.exit(1);
|
|
1507
1906
|
};
|
|
1508
1907
|
/**
|
|
@@ -1532,20 +1931,20 @@ const discoverTypeScriptMetadata = async (filePath, options) => {
|
|
|
1532
1931
|
loadModule = buildModule.loadModule;
|
|
1533
1932
|
selectRolloutFunction = buildModule.selectRolloutFunction;
|
|
1534
1933
|
if (!extractRolloutFunctions || !buildFile || !loadModule || !selectRolloutFunction) {
|
|
1535
|
-
logger$
|
|
1934
|
+
logger$2.error("Missing exports from @lmnr-ai/lmnr modules. This may indicate an outdated package version.");
|
|
1536
1935
|
logLmnrPackageNotFoundAndExit();
|
|
1537
1936
|
}
|
|
1538
1937
|
} catch (error) {
|
|
1539
1938
|
if (error.code === "MODULE_NOT_FOUND") logLmnrPackageNotFoundAndExit();
|
|
1540
|
-
logger$
|
|
1939
|
+
logger$2.error(`Unexpected error loading @lmnr-ai/lmnr modules: ${error.message}`);
|
|
1541
1940
|
throw error;
|
|
1542
1941
|
}
|
|
1543
1942
|
let paramsMetadata;
|
|
1544
1943
|
try {
|
|
1545
1944
|
paramsMetadata = extractRolloutFunctions(filePath);
|
|
1546
|
-
logger$
|
|
1945
|
+
logger$2.debug(`Extracted TypeScript metadata for ${paramsMetadata?.size} functions`);
|
|
1547
1946
|
} catch (error) {
|
|
1548
|
-
logger$
|
|
1947
|
+
logger$2.warn("Failed to extract TypeScript metadata, falling back to runtime parsing: " + (error instanceof Error ? error.message : String(error)));
|
|
1549
1948
|
}
|
|
1550
1949
|
const moduleText = await buildFile(filePath, {
|
|
1551
1950
|
externalPackages: options.externalPackages,
|
|
@@ -1557,21 +1956,21 @@ const discoverTypeScriptMetadata = async (filePath, options) => {
|
|
|
1557
1956
|
});
|
|
1558
1957
|
const selectedFunction = selectRolloutFunction(options.function);
|
|
1559
1958
|
if (paramsMetadata) {
|
|
1560
|
-
logger$
|
|
1561
|
-
logger$
|
|
1959
|
+
logger$2.debug(`Available TS metadata keys: ${Array.from(paramsMetadata.keys()).join(", ")}`);
|
|
1960
|
+
logger$2.debug(`Looking for span name: ${selectedFunction.name} (runtime key: ${selectedFunction.exportName})`);
|
|
1562
1961
|
let foundMetadata = null;
|
|
1563
1962
|
for (const [exportName, metadata] of paramsMetadata.entries()) {
|
|
1564
|
-
logger$
|
|
1963
|
+
logger$2.debug(`Checking ${exportName}: span name = ${metadata.name}, export name = ${exportName}`);
|
|
1565
1964
|
if (metadata.name === selectedFunction.name) {
|
|
1566
1965
|
foundMetadata = metadata;
|
|
1567
|
-
logger$
|
|
1966
|
+
logger$2.debug(`Match. Export name: ${exportName}, span name: ${metadata.name}`);
|
|
1568
1967
|
break;
|
|
1569
1968
|
}
|
|
1570
1969
|
}
|
|
1571
1970
|
if (foundMetadata) {
|
|
1572
1971
|
selectedFunction.params = foundMetadata.params;
|
|
1573
|
-
logger$
|
|
1574
|
-
} else logger$
|
|
1972
|
+
logger$2.debug(`Using TypeScript metadata for span: ${selectedFunction.name}`);
|
|
1973
|
+
} else logger$2.info(`No TypeScript metadata found for span name: ${selectedFunction.name}`);
|
|
1575
1974
|
}
|
|
1576
1975
|
return {
|
|
1577
1976
|
functionName: selectedFunction.name,
|
|
@@ -1681,7 +2080,7 @@ const extractMetadataFromStdout = (stdout) => {
|
|
|
1681
2080
|
* Discovers function metadata for Python files/modules by calling the lmnr Python CLI
|
|
1682
2081
|
*/
|
|
1683
2082
|
const discoverPythonMetadata = async (filePathOrModule, options) => {
|
|
1684
|
-
logger$
|
|
2083
|
+
logger$2.debug(`Discovering Python metadata for ${filePathOrModule}`);
|
|
1685
2084
|
const args = ["discover"];
|
|
1686
2085
|
if (options.pythonModule) args.push("--module", options.pythonModule);
|
|
1687
2086
|
else args.push("--file", filePathOrModule);
|
|
@@ -1694,8 +2093,8 @@ const discoverPythonMetadata = async (filePathOrModule, options) => {
|
|
|
1694
2093
|
};
|
|
1695
2094
|
} catch (error) {
|
|
1696
2095
|
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$
|
|
2096
|
+
logger$2.error(`Error while loading Python file/module: ${errorMessage}`);
|
|
2097
|
+
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
2098
|
throw error;
|
|
1700
2099
|
}
|
|
1701
2100
|
};
|
|
@@ -1707,7 +2106,7 @@ const discoverFunctionMetadata = async (filePathOrModule, options) => {
|
|
|
1707
2106
|
const ext = path.extname(filePathOrModule);
|
|
1708
2107
|
if (TS_JS_EXTENSIONS.includes(ext)) return await discoverTypeScriptMetadata(filePathOrModule, options);
|
|
1709
2108
|
if (ext === ".py") return await discoverPythonMetadata(filePathOrModule, options);
|
|
1710
|
-
logger$
|
|
2109
|
+
logger$2.warn(`No metadata discovery available for ${ext} files`);
|
|
1711
2110
|
return {
|
|
1712
2111
|
functionName: options.function || path.basename(filePathOrModule, ext),
|
|
1713
2112
|
params: []
|
|
@@ -1716,7 +2115,7 @@ const discoverFunctionMetadata = async (filePathOrModule, options) => {
|
|
|
1716
2115
|
|
|
1717
2116
|
//#endregion
|
|
1718
2117
|
//#region src/commands/dev/index.ts
|
|
1719
|
-
const logger = initializeLogger();
|
|
2118
|
+
const logger$1 = initializeLogger();
|
|
1720
2119
|
function newUUID() {
|
|
1721
2120
|
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return crypto.randomUUID();
|
|
1722
2121
|
return (0, uuid.v4)();
|
|
@@ -1747,7 +2146,7 @@ const tryParseArg = (arg) => {
|
|
|
1747
2146
|
* Handles a run event from the backend
|
|
1748
2147
|
*/
|
|
1749
2148
|
const handleRunEvent = async (event, sessionId, filePathOrModule, client, cacheServerPort, cache, setMetadata, options, subprocessManager) => {
|
|
1750
|
-
logger.debug("Received run event");
|
|
2149
|
+
logger$1.debug("Received run event");
|
|
1751
2150
|
const { trace_id, path_to_count, args: rawArgs, overrides } = event.data;
|
|
1752
2151
|
const parsedArgs = Array.isArray(rawArgs) ? rawArgs.map(tryParseArg) : Object.fromEntries(Object.entries(rawArgs).map(([key, value]) => [key, tryParseArg(value)]));
|
|
1753
2152
|
cache.clear();
|
|
@@ -1756,10 +2155,10 @@ const handleRunEvent = async (event, sessionId, filePathOrModule, client, cacheS
|
|
|
1756
2155
|
overrides
|
|
1757
2156
|
});
|
|
1758
2157
|
try {
|
|
1759
|
-
if (!trace_id || trace_id.trim() === "") logger.info("No spans in cache, starting fresh");
|
|
2158
|
+
if (!trace_id || trace_id.trim() === "") logger$1.info("No spans in cache, starting fresh");
|
|
1760
2159
|
else {
|
|
1761
2160
|
const paths = Object.keys(path_to_count || {});
|
|
1762
|
-
if (paths.length === 0) logger.info("No spans to cache, starting fresh");
|
|
2161
|
+
if (paths.length === 0) logger$1.info("No spans to cache, starting fresh");
|
|
1763
2162
|
else {
|
|
1764
2163
|
const query = `
|
|
1765
2164
|
SELECT name, input, output, attributes, path
|
|
@@ -1768,12 +2167,12 @@ const handleRunEvent = async (event, sessionId, filePathOrModule, client, cacheS
|
|
|
1768
2167
|
AND path IN {paths:String[]}
|
|
1769
2168
|
ORDER BY start_time ASC
|
|
1770
2169
|
`;
|
|
1771
|
-
logger.debug(`Querying spans from trace ${trace_id}...`);
|
|
2170
|
+
logger$1.debug(`Querying spans from trace ${trace_id}...`);
|
|
1772
2171
|
const spans = await client.sql.query(query, {
|
|
1773
2172
|
traceId: trace_id,
|
|
1774
2173
|
paths
|
|
1775
2174
|
});
|
|
1776
|
-
logger.debug(`Received ${spans.length} spans from backend`);
|
|
2175
|
+
logger$1.debug(`Received ${spans.length} spans from backend`);
|
|
1777
2176
|
const spansByPath = {};
|
|
1778
2177
|
for (const span of spans) {
|
|
1779
2178
|
const path$2 = span.path;
|
|
@@ -1811,7 +2210,7 @@ const handleRunEvent = async (event, sessionId, filePathOrModule, client, cacheS
|
|
|
1811
2210
|
const cacheKey = `${index}:${path$2}`;
|
|
1812
2211
|
cache.set(cacheKey, cachedSpan);
|
|
1813
2212
|
});
|
|
1814
|
-
logger.info(`Cached ${spansToCache.length} spans for path: ${path$2}`);
|
|
2213
|
+
logger$1.info(`Cached ${spansToCache.length} spans for path: ${path$2}`);
|
|
1815
2214
|
}
|
|
1816
2215
|
setMetadata({
|
|
1817
2216
|
pathToCount: path_to_count || {},
|
|
@@ -1850,7 +2249,7 @@ const handleRunEvent = async (event, sessionId, filePathOrModule, client, cacheS
|
|
|
1850
2249
|
status: "RUNNING"
|
|
1851
2250
|
});
|
|
1852
2251
|
} catch (error) {
|
|
1853
|
-
logger.error(`Error setting debugger session status: ${error instanceof Error ? error.message : error}`);
|
|
2252
|
+
logger$1.error(`Error setting debugger session status: ${error instanceof Error ? error.message : error}`);
|
|
1854
2253
|
}
|
|
1855
2254
|
await subprocessManager.execute({
|
|
1856
2255
|
command: workerCommand.command,
|
|
@@ -1863,18 +2262,18 @@ const handleRunEvent = async (event, sessionId, filePathOrModule, client, cacheS
|
|
|
1863
2262
|
status: "FINISHED"
|
|
1864
2263
|
});
|
|
1865
2264
|
} catch (error) {
|
|
1866
|
-
logger.error(`Error setting debugger session status: ${error instanceof Error ? error.message : error}`);
|
|
2265
|
+
logger$1.error(`Error setting debugger session status: ${error instanceof Error ? error.message : error}`);
|
|
1867
2266
|
}
|
|
1868
2267
|
} 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);
|
|
2268
|
+
logger$1.error(`Error handling run event: ${error instanceof Error ? error.message : error}`);
|
|
2269
|
+
if (error instanceof Error && error.stack) logger$1.error(error.stack);
|
|
1871
2270
|
try {
|
|
1872
2271
|
await client.rolloutSessions.setStatus({
|
|
1873
2272
|
sessionId,
|
|
1874
2273
|
status: "FINISHED"
|
|
1875
2274
|
});
|
|
1876
2275
|
} catch (error$1) {
|
|
1877
|
-
logger.error(`Error setting debugger session status: ${error$1 instanceof Error ? error$1.message : error$1}`);
|
|
2276
|
+
logger$1.error(`Error setting debugger session status: ${error$1 instanceof Error ? error$1.message : error$1}`);
|
|
1878
2277
|
}
|
|
1879
2278
|
}
|
|
1880
2279
|
};
|
|
@@ -1891,30 +2290,30 @@ async function runDev(filePath, options = {}) {
|
|
|
1891
2290
|
projectApiKey: options.projectApiKey,
|
|
1892
2291
|
port: options.port
|
|
1893
2292
|
});
|
|
1894
|
-
logger.debug("Starting cache server...");
|
|
2293
|
+
logger$1.debug("Starting cache server...");
|
|
1895
2294
|
const { port: cacheServerPort, server: cacheServer, cache, setMetadata } = await startCacheServer();
|
|
1896
|
-
logger.debug(`Cache server started on port ${cacheServerPort}`);
|
|
2295
|
+
logger$1.debug(`Cache server started on port ${cacheServerPort}`);
|
|
1897
2296
|
const subprocessManager = new SubprocessManager();
|
|
1898
2297
|
let functionName = options.function;
|
|
1899
2298
|
let params = [];
|
|
1900
2299
|
try {
|
|
1901
2300
|
if (isPythonModule || filePath && EXTENSIONS_TO_DISCOVER_METADATA.includes(path.extname(filePath))) {
|
|
1902
|
-
logger.debug("Discovering entrypoint functions...");
|
|
2301
|
+
logger$1.debug("Discovering entrypoint functions...");
|
|
1903
2302
|
const metadata = await discoverFunctionMetadata(filePathOrModule, options);
|
|
1904
2303
|
functionName = metadata.functionName;
|
|
1905
2304
|
params = metadata.params;
|
|
1906
|
-
logger.info(`Serving function: ${functionName}`);
|
|
1907
|
-
logger.debug(`Function parameters: ${JSON.stringify(params, null, 2)}`);
|
|
2305
|
+
logger$1.info(`Serving function: ${functionName}`);
|
|
2306
|
+
logger$1.debug(`Function parameters: ${JSON.stringify(params, null, 2)}`);
|
|
1908
2307
|
} else if (filePath) {
|
|
1909
2308
|
functionName = options.function || path.basename(filePath, path.extname(filePath));
|
|
1910
|
-
logger.warn(`Metadata discovery not available for ${path.extname(filePath)} files`);
|
|
2309
|
+
logger$1.warn(`Metadata discovery not available for ${path.extname(filePath)} files`);
|
|
1911
2310
|
}
|
|
1912
2311
|
} catch (error) {
|
|
1913
|
-
logger.error("Failed to discover entrypoint functions: " + (error instanceof Error ? error.message : String(error)));
|
|
2312
|
+
logger$1.error("Failed to discover entrypoint functions: " + (error instanceof Error ? error.message : String(error)));
|
|
1914
2313
|
cacheServer.close();
|
|
1915
2314
|
throw error;
|
|
1916
2315
|
}
|
|
1917
|
-
logger.debug("Setting up file watcher...");
|
|
2316
|
+
logger$1.debug("Setting up file watcher...");
|
|
1918
2317
|
const watcher = chokidar.default.watch(".", {
|
|
1919
2318
|
ignored: (path$2) => {
|
|
1920
2319
|
const ignoredDirs = [
|
|
@@ -1949,7 +2348,7 @@ async function runDev(filePath, options = {}) {
|
|
|
1949
2348
|
pollInterval: 100
|
|
1950
2349
|
}
|
|
1951
2350
|
});
|
|
1952
|
-
logger.debug("Setting up SSE client...");
|
|
2351
|
+
logger$1.debug("Setting up SSE client...");
|
|
1953
2352
|
let sseClient = null;
|
|
1954
2353
|
try {
|
|
1955
2354
|
sseClient = createSSEClient({
|
|
@@ -1961,45 +2360,45 @@ async function runDev(filePath, options = {}) {
|
|
|
1961
2360
|
let currentRunPromise = null;
|
|
1962
2361
|
let stopRequested = false;
|
|
1963
2362
|
sseClient.on("heartbeat", () => {
|
|
1964
|
-
logger.debug("Heartbeat received");
|
|
2363
|
+
logger$1.debug("Heartbeat received");
|
|
1965
2364
|
});
|
|
1966
2365
|
sseClient.on("run", (event) => {
|
|
1967
2366
|
if (currentRunPromise !== null) {
|
|
1968
|
-
logger.warn("Already processing a run event, skipping new run");
|
|
2367
|
+
logger$1.warn("Already processing a run event, skipping new run");
|
|
1969
2368
|
return;
|
|
1970
2369
|
}
|
|
1971
2370
|
currentRunPromise = (async () => {
|
|
1972
2371
|
try {
|
|
1973
2372
|
stopRequested = false;
|
|
1974
2373
|
if (reloadScheduled) {
|
|
1975
|
-
logger.info("Reloading function metadata before run...");
|
|
2374
|
+
logger$1.info("Reloading function metadata before run...");
|
|
1976
2375
|
if (isPythonModule || filePath && EXTENSIONS_TO_DISCOVER_METADATA.includes(path.extname(filePath))) try {
|
|
1977
2376
|
const metadata = await discoverFunctionMetadata(filePathOrModule, options);
|
|
1978
2377
|
if (stopRequested) {
|
|
1979
|
-
logger.info("Run cancelled during metadata discovery");
|
|
2378
|
+
logger$1.info("Run cancelled during metadata discovery");
|
|
1980
2379
|
return;
|
|
1981
2380
|
}
|
|
1982
|
-
logger.debug(`Updated function metadata: ${metadata.functionName}`);
|
|
1983
|
-
logger.debug(`Updated parameters: ${JSON.stringify(metadata.params, null, 2)}`);
|
|
2381
|
+
logger$1.debug(`Updated function metadata: ${metadata.functionName}`);
|
|
2382
|
+
logger$1.debug(`Updated parameters: ${JSON.stringify(metadata.params, null, 2)}`);
|
|
1984
2383
|
if (sseClient) {
|
|
1985
2384
|
sseClient.updateMetadata(metadata.params, metadata.functionName);
|
|
1986
|
-
logger.debug("Notified backend of metadata changes");
|
|
2385
|
+
logger$1.debug("Notified backend of metadata changes");
|
|
1987
2386
|
}
|
|
1988
2387
|
reloadScheduled = false;
|
|
1989
2388
|
} 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}`);
|
|
2389
|
+
logger$1.error("Failed to update function metadata: " + (error instanceof Error ? error.message : String(error)));
|
|
2390
|
+
if (error instanceof Error && error.stack) logger$1.debug(`Stack trace: ${error.stack}`);
|
|
1992
2391
|
return;
|
|
1993
2392
|
}
|
|
1994
2393
|
else reloadScheduled = false;
|
|
1995
2394
|
}
|
|
1996
2395
|
if (stopRequested) {
|
|
1997
|
-
logger.info("Run cancelled before execution");
|
|
2396
|
+
logger$1.info("Run cancelled before execution");
|
|
1998
2397
|
return;
|
|
1999
2398
|
}
|
|
2000
2399
|
await handleRunEvent(event, sessionId, filePathOrModule, client, cacheServerPort, cache, setMetadata, options, subprocessManager);
|
|
2001
2400
|
} catch (error) {
|
|
2002
|
-
logger.error("Unhandled error in run event handler: " + (error instanceof Error ? error.message : String(error)));
|
|
2401
|
+
logger$1.error("Unhandled error in run event handler: " + (error instanceof Error ? error.message : String(error)));
|
|
2003
2402
|
} finally {
|
|
2004
2403
|
currentRunPromise = null;
|
|
2005
2404
|
}
|
|
@@ -2009,65 +2408,65 @@ async function runDev(filePath, options = {}) {
|
|
|
2009
2408
|
const projectId = event.data.project_id;
|
|
2010
2409
|
const sessionId$1 = event.data.session_id;
|
|
2011
2410
|
const frontendUrl = getFrontendUrl(options.baseUrl, options.frontendPort);
|
|
2012
|
-
if (!didLogHandshake) logger.info(`View your session at ${frontendUrl}/project/${projectId}/debugger-sessions/${sessionId$1}`);
|
|
2411
|
+
if (!didLogHandshake) logger$1.info(`View your session at ${frontendUrl}/project/${projectId}/debugger-sessions/${sessionId$1}`);
|
|
2013
2412
|
didLogHandshake = true;
|
|
2014
2413
|
});
|
|
2015
2414
|
sseClient.on("error", (error) => {
|
|
2016
|
-
logger.warn(`Error connecting to backend: ${error.message}`);
|
|
2415
|
+
logger$1.warn(`Error connecting to backend: ${error.message}`);
|
|
2017
2416
|
});
|
|
2018
2417
|
sseClient.on("reconnecting", () => {
|
|
2019
|
-
logger.info("Reconnecting to backend...");
|
|
2418
|
+
logger$1.info("Reconnecting to backend...");
|
|
2020
2419
|
});
|
|
2021
2420
|
sseClient.on("heartbeat_timeout", () => {
|
|
2022
|
-
logger.debug("Heartbeat timeout, reconnecting...");
|
|
2421
|
+
logger$1.debug("Heartbeat timeout, reconnecting...");
|
|
2023
2422
|
});
|
|
2024
2423
|
sseClient.on("stop", () => {
|
|
2025
|
-
logger.debug("Stop event received");
|
|
2424
|
+
logger$1.debug("Stop event received");
|
|
2026
2425
|
stopRequested = true;
|
|
2027
|
-
if (subprocessManager.kill()) logger.info("Current run cancelled");
|
|
2426
|
+
if (subprocessManager.kill()) logger$1.info("Current run cancelled");
|
|
2028
2427
|
});
|
|
2029
2428
|
let reloadTimeout = null;
|
|
2030
2429
|
let reloadScheduled = false;
|
|
2031
2430
|
watcher.on("change", (changedPath) => {
|
|
2032
|
-
logger.info(`File changed: ${changedPath}, scheduling reload...`);
|
|
2431
|
+
logger$1.info(`File changed: ${changedPath}, scheduling reload...`);
|
|
2033
2432
|
if (reloadTimeout) clearTimeout(reloadTimeout);
|
|
2034
2433
|
reloadTimeout = setTimeout(() => {
|
|
2035
|
-
logger.debug("Marking reload as scheduled for next run...");
|
|
2434
|
+
logger$1.debug("Marking reload as scheduled for next run...");
|
|
2036
2435
|
reloadTimeout = null;
|
|
2037
2436
|
reloadScheduled = true;
|
|
2038
2437
|
}, 100);
|
|
2039
2438
|
});
|
|
2040
2439
|
const shutdown = () => {
|
|
2041
|
-
logger.debug("Shutting down...");
|
|
2440
|
+
logger$1.debug("Shutting down...");
|
|
2042
2441
|
if (reloadTimeout) {
|
|
2043
2442
|
clearTimeout(reloadTimeout);
|
|
2044
2443
|
reloadTimeout = null;
|
|
2045
2444
|
}
|
|
2046
2445
|
reloadScheduled = false;
|
|
2047
|
-
logger.debug("Closing file watcher...");
|
|
2446
|
+
logger$1.debug("Closing file watcher...");
|
|
2048
2447
|
watcher.close().catch((error) => {
|
|
2049
|
-
logger.error(`Failed to close file watcher: ${error instanceof Error ? error.message : error}`);
|
|
2448
|
+
logger$1.error(`Failed to close file watcher: ${error instanceof Error ? error.message : error}`);
|
|
2050
2449
|
});
|
|
2051
2450
|
subprocessManager.kill();
|
|
2052
|
-
logger.debug("Deleting debugger session...");
|
|
2451
|
+
logger$1.debug("Deleting debugger session...");
|
|
2053
2452
|
client.rolloutSessions.delete({ sessionId }).then(() => {
|
|
2054
2453
|
if (sseClient) sseClient.shutdown();
|
|
2055
2454
|
cacheServer.close(() => {
|
|
2056
|
-
logger.debug("Cache server closed");
|
|
2455
|
+
logger$1.debug("Cache server closed");
|
|
2057
2456
|
});
|
|
2058
2457
|
process.exit(0);
|
|
2059
2458
|
}).catch((error) => {
|
|
2060
|
-
logger.warn(`Failed to delete debugger session: ${error instanceof Error ? error.message : error}`);
|
|
2459
|
+
logger$1.warn(`Failed to delete debugger session: ${error instanceof Error ? error.message : error}`);
|
|
2061
2460
|
process.exit(1);
|
|
2062
2461
|
});
|
|
2063
2462
|
};
|
|
2064
2463
|
process.on("SIGINT", shutdown);
|
|
2065
2464
|
process.on("SIGTERM", shutdown);
|
|
2066
2465
|
process.stdin.resume();
|
|
2067
|
-
logger.debug("Connecting to backend...");
|
|
2466
|
+
logger$1.debug("Connecting to backend...");
|
|
2068
2467
|
await sseClient.connectAndListen();
|
|
2069
2468
|
} catch (error) {
|
|
2070
|
-
logger.error("Failed to start dev command: " + (error instanceof Error ? error.message : String(error)));
|
|
2469
|
+
logger$1.error("Failed to start dev command: " + (error instanceof Error ? error.message : String(error)));
|
|
2071
2470
|
try {
|
|
2072
2471
|
await client.rolloutSessions.delete({ sessionId });
|
|
2073
2472
|
} catch {}
|
|
@@ -2078,11 +2477,92 @@ async function runDev(filePath, options = {}) {
|
|
|
2078
2477
|
}
|
|
2079
2478
|
}
|
|
2080
2479
|
|
|
2480
|
+
//#endregion
|
|
2481
|
+
//#region src/commands/sql/index.ts
|
|
2482
|
+
const logger = initializeLogger();
|
|
2483
|
+
const handleSqlQuery = async (query, options) => {
|
|
2484
|
+
const client = new LaminarClient({
|
|
2485
|
+
projectApiKey: options.projectApiKey,
|
|
2486
|
+
baseUrl: options.baseUrl,
|
|
2487
|
+
port: options.port
|
|
2488
|
+
});
|
|
2489
|
+
try {
|
|
2490
|
+
const rows = await client.sql.query(query);
|
|
2491
|
+
if (options.json) {
|
|
2492
|
+
outputJson(rows);
|
|
2493
|
+
return;
|
|
2494
|
+
}
|
|
2495
|
+
if (rows.length === 0) {
|
|
2496
|
+
console.log("No rows returned.");
|
|
2497
|
+
return;
|
|
2498
|
+
}
|
|
2499
|
+
const columns = Object.keys(rows[0]);
|
|
2500
|
+
const widths = columns.map((col) => rows.reduce((max, r) => Math.max(max, String(r[col] ?? "").length), col.length));
|
|
2501
|
+
const header = columns.map((col, i) => col.padEnd(widths[i])).join(" ");
|
|
2502
|
+
const divider = widths.map((w) => "-".repeat(w)).join(" ");
|
|
2503
|
+
console.log(`\n${header}`);
|
|
2504
|
+
console.log(divider);
|
|
2505
|
+
for (const row of rows) console.log(columns.map((col, i) => String(row[col] ?? "").padEnd(widths[i])).join(" "));
|
|
2506
|
+
console.log(`\n${rows.length} row(s)\n`);
|
|
2507
|
+
} catch (error) {
|
|
2508
|
+
if (options.json) outputJsonError(error);
|
|
2509
|
+
logger.error(`Query failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
2510
|
+
process.exit(1);
|
|
2511
|
+
}
|
|
2512
|
+
};
|
|
2513
|
+
|
|
2514
|
+
//#endregion
|
|
2515
|
+
//#region src/commands/sql/schema.ts
|
|
2516
|
+
const SQL_SCHEMA_HELP = `
|
|
2517
|
+
Available tables:
|
|
2518
|
+
spans
|
|
2519
|
+
span_id (UUID), name (String), span_type (String: DEFAULT|LLM|TOOL),
|
|
2520
|
+
start_time (DateTime64), end_time (DateTime64), duration (Float64),
|
|
2521
|
+
input_cost (Float64), output_cost (Float64), total_cost (Float64),
|
|
2522
|
+
input_tokens (Int64), output_tokens (Int64), total_tokens (Int64),
|
|
2523
|
+
request_model (String), response_model (String), model (String),
|
|
2524
|
+
trace_id (UUID), provider (String), path (String),
|
|
2525
|
+
input (String), output (String), status (String),
|
|
2526
|
+
parent_span_id (UUID), attributes (String), tags (Array(String))
|
|
2527
|
+
|
|
2528
|
+
traces
|
|
2529
|
+
id (UUID), start_time (DateTime64), end_time (DateTime64),
|
|
2530
|
+
input_tokens (Int64), output_tokens (Int64), total_tokens (Int64),
|
|
2531
|
+
input_cost (Float64), output_cost (Float64), total_cost (Float64),
|
|
2532
|
+
duration (Float64), metadata (String), session_id (String),
|
|
2533
|
+
user_id (String), status (String), top_span_id (UUID),
|
|
2534
|
+
top_span_name (String), top_span_type (String), trace_type (String),
|
|
2535
|
+
tags (Array(String)), has_browser_session (Bool)
|
|
2536
|
+
|
|
2537
|
+
events
|
|
2538
|
+
id (UUID), type (String), name (String), span_id (UUID),
|
|
2539
|
+
timestamp (DateTime64), attributes (String)
|
|
2540
|
+
|
|
2541
|
+
signal_events
|
|
2542
|
+
id (UUID), signal_id (UUID), trace_id (UUID), run_id (UUID),
|
|
2543
|
+
name (String), payload (String), timestamp (DateTime64)
|
|
2544
|
+
|
|
2545
|
+
signal_runs
|
|
2546
|
+
signal_id (UUID), job_id (UUID), trigger_id (UUID), run_id (UUID),
|
|
2547
|
+
trace_id (UUID), status (String), event_id (UUID), updated_at (DateTime64)
|
|
2548
|
+
|
|
2549
|
+
evaluation_datapoints
|
|
2550
|
+
id (UUID), evaluation_id (UUID), data (String), target (String),
|
|
2551
|
+
metadata (String), executor_output (String), index (UInt64),
|
|
2552
|
+
trace_id (UUID), group_id (String), scores (String),
|
|
2553
|
+
created_at (DateTime64), dataset_id (UUID),
|
|
2554
|
+
dataset_datapoint_id (UUID), dataset_datapoint_created_at (DateTime64)
|
|
2555
|
+
|
|
2556
|
+
dataset_datapoints
|
|
2557
|
+
id (UUID), created_at (DateTime64), dataset_id (UUID),
|
|
2558
|
+
data (String), target (String), metadata (String)
|
|
2559
|
+
`;
|
|
2560
|
+
|
|
2081
2561
|
//#endregion
|
|
2082
2562
|
//#region src/index.ts
|
|
2083
2563
|
async function main() {
|
|
2084
2564
|
const program = new commander.Command();
|
|
2085
|
-
program.name("lmnr-cli").description("CLI for Laminar agent
|
|
2565
|
+
program.name("lmnr-cli").description("CLI for the Laminar agent observability platform").version(version$1, "-v, --version", "display version number");
|
|
2086
2566
|
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
2567
|
if (!file && !options.pythonModule) {
|
|
2088
2568
|
console.error("Error: Must provide either a file path or --python-module (-m) flag");
|
|
@@ -2099,6 +2579,44 @@ Examples:
|
|
|
2099
2579
|
$ lmnr-cli dev agent.py # Python file (script mode)
|
|
2100
2580
|
$ lmnr-cli dev -m src.agent # Python module (module mode)
|
|
2101
2581
|
$ lmnr-cli dev agent.ts --function myAgent # Specific function
|
|
2582
|
+
`);
|
|
2583
|
+
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");
|
|
2584
|
+
datasetsCmd.command("list").description("List all datasets").action(async (_options, cmd) => {
|
|
2585
|
+
await handleDatasetsList(cmd.optsWithGlobals());
|
|
2586
|
+
});
|
|
2587
|
+
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) => {
|
|
2588
|
+
await handleDatasetsPush(paths, cmd.optsWithGlobals());
|
|
2589
|
+
});
|
|
2590
|
+
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) => {
|
|
2591
|
+
await handleDatasetsPull(outputPath, cmd.optsWithGlobals());
|
|
2592
|
+
});
|
|
2593
|
+
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) => {
|
|
2594
|
+
await handleDatasetsCreate(name, paths, cmd.optsWithGlobals());
|
|
2595
|
+
});
|
|
2596
|
+
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");
|
|
2597
|
+
sqlCmd.command("query").description("Execute a SQL query").argument("<query>", "SQL query string").action(async (query, _options, cmd) => {
|
|
2598
|
+
await handleSqlQuery(query, cmd.optsWithGlobals());
|
|
2599
|
+
}).addHelpText("after", SQL_SCHEMA_HELP + `
|
|
2600
|
+
Examples:
|
|
2601
|
+
$ lmnr-cli sql query "SELECT * FROM spans LIMIT 10"
|
|
2602
|
+
$ lmnr-cli sql query "SELECT id, total_cost, status FROM traces LIMIT 20"
|
|
2603
|
+
$ lmnr-cli sql query "SELECT * FROM spans LIMIT 10" --json
|
|
2604
|
+
`);
|
|
2605
|
+
sqlCmd.command("schema").description("Show available tables and their columns").action(() => {
|
|
2606
|
+
process.stdout.write(SQL_SCHEMA_HELP);
|
|
2607
|
+
});
|
|
2608
|
+
program.addHelpText("after", `
|
|
2609
|
+
Examples:
|
|
2610
|
+
lmnr-cli dev agent.ts # Debugger TypeScript entrypoint
|
|
2611
|
+
lmnr-cli dev agent.py # Debugger Python script mode
|
|
2612
|
+
lmnr-cli dev -m src.agent # Debuger Python module mode
|
|
2613
|
+
lmnr-cli dataset list --json # List all datasets
|
|
2614
|
+
lmnr-cli dataset push data.jsonl -n my-dataset --json # Push data to a dataset
|
|
2615
|
+
lmnr-cli dataset pull output.jsonl -n my-dataset --json # Pull data from a dataset
|
|
2616
|
+
lmnr-cli sql query "SELECT * FROM spans LIMIT 10" --json # Query spans
|
|
2617
|
+
lmnr-cli sql query "SELECT t.id, s.name FROM traces t \
|
|
2618
|
+
JOIN spans s ON t.id = s.trace_id LIMIT 20" --json
|
|
2619
|
+
lmnr-cli sql schema # Show available tables
|
|
2102
2620
|
`);
|
|
2103
2621
|
await program.parseAsync();
|
|
2104
2622
|
}
|