lmnr-cli 0.1.12 → 0.1.14
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/README.md +12 -3
- package/dist/index.cjs +300 -38
- package/dist/index.cjs.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -128,11 +128,20 @@ lmnr-cli dataset create my-dataset data.jsonl -o out.jsonl
|
|
|
128
128
|
Record findings on a trace and review/name agent debug sessions.
|
|
129
129
|
|
|
130
130
|
```bash
|
|
131
|
-
lmnr-cli
|
|
132
|
-
lmnr-cli debug session
|
|
133
|
-
lmnr-cli
|
|
131
|
+
lmnr-cli debug session new # Mint a fresh debug session
|
|
132
|
+
lmnr-cli debug session open # Open the session in the browser
|
|
133
|
+
lmnr-cli trace append-note "note text" # Markdown note on the latest debug trace
|
|
134
|
+
lmnr-cli debug session set-name "title" # Rename the current debug session
|
|
135
|
+
lmnr-cli debug session summary # Every trace in the session + its note
|
|
134
136
|
```
|
|
135
137
|
|
|
138
|
+
These commands default to the session/trace recorded in
|
|
139
|
+
`.lmnr/debug-session.json` (written by `debug session new` and `LMNR_DEBUG=1`
|
|
140
|
+
runs; the nearest one walking up from the current directory, so subdirectories
|
|
141
|
+
of a project work too); target another one with an explicit id flag
|
|
142
|
+
(e.g. `lmnr-cli debug session summary --session-id <session-id>`,
|
|
143
|
+
`lmnr-cli trace append-note "note" --trace-id <trace-id>`).
|
|
144
|
+
|
|
136
145
|
`trace append-note` accumulates (each call appends a paragraph). See the Laminar
|
|
137
146
|
debugger docs: https://laminar.sh/docs/platform/debugger
|
|
138
147
|
|
package/dist/index.cjs
CHANGED
|
@@ -41,18 +41,24 @@ let fs_promises = require("fs/promises");
|
|
|
41
41
|
fs_promises = __toESM(fs_promises);
|
|
42
42
|
let cli_table3 = require("cli-table3");
|
|
43
43
|
cli_table3 = __toESM(cli_table3);
|
|
44
|
+
let node_crypto = require("node:crypto");
|
|
44
45
|
let open = require("open");
|
|
45
46
|
open = __toESM(open);
|
|
47
|
+
let node_fs = require("node:fs");
|
|
46
48
|
let picocolors = require("picocolors");
|
|
47
49
|
let node_readline_promises = require("node:readline/promises");
|
|
48
50
|
let node_child_process = require("node:child_process");
|
|
49
51
|
let node_util = require("node:util");
|
|
50
52
|
let giget = require("giget");
|
|
51
53
|
//#region ../types/dist/index.mjs
|
|
54
|
+
/** Directory the debug-session file lives in, relative to the working dir. */
|
|
55
|
+
const DEBUG_SESSION_DIR = ".lmnr";
|
|
56
|
+
/** Filename of the debug-session file inside {@link DEBUG_SESSION_DIR}. */
|
|
57
|
+
const DEBUG_SESSION_FILE = "debug-session.json";
|
|
52
58
|
const errorMessage = (error) => error instanceof Error ? error.message : String(error);
|
|
53
59
|
//#endregion
|
|
54
60
|
//#region package.json
|
|
55
|
-
var version$1 = "0.1.
|
|
61
|
+
var version$1 = "0.1.14";
|
|
56
62
|
//#endregion
|
|
57
63
|
//#region ../../node_modules/.pnpm/dotenv@17.4.2/node_modules/dotenv/lib/main.js
|
|
58
64
|
var require_main = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
@@ -323,6 +329,12 @@ var require_main = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
323
329
|
module.exports = DotenvModule;
|
|
324
330
|
}));
|
|
325
331
|
//#endregion
|
|
332
|
+
//#region ../../node_modules/.pnpm/uuid@14.0.0/node_modules/uuid/dist-node/rng.js
|
|
333
|
+
const rnds8 = new Uint8Array(16);
|
|
334
|
+
function rng() {
|
|
335
|
+
return crypto.getRandomValues(rnds8);
|
|
336
|
+
}
|
|
337
|
+
//#endregion
|
|
326
338
|
//#region ../../node_modules/.pnpm/uuid@14.0.0/node_modules/uuid/dist-node/stringify.js
|
|
327
339
|
const byteToHex = [];
|
|
328
340
|
for (let i = 0; i < 256; ++i) byteToHex.push((i + 256).toString(16).slice(1));
|
|
@@ -330,12 +342,6 @@ function unsafeStringify(arr, offset = 0) {
|
|
|
330
342
|
return (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + "-" + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + "-" + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + "-" + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + "-" + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase();
|
|
331
343
|
}
|
|
332
344
|
//#endregion
|
|
333
|
-
//#region ../../node_modules/.pnpm/uuid@14.0.0/node_modules/uuid/dist-node/rng.js
|
|
334
|
-
const rnds8 = new Uint8Array(16);
|
|
335
|
-
function rng() {
|
|
336
|
-
return crypto.getRandomValues(rnds8);
|
|
337
|
-
}
|
|
338
|
-
//#endregion
|
|
339
345
|
//#region ../../node_modules/.pnpm/uuid@14.0.0/node_modules/uuid/dist-node/v4.js
|
|
340
346
|
function v4(options, buf, offset) {
|
|
341
347
|
if (!buf && !options && crypto.randomUUID) return crypto.randomUUID();
|
|
@@ -358,7 +364,7 @@ function _v4(options, buf, offset) {
|
|
|
358
364
|
//#endregion
|
|
359
365
|
//#region ../client/dist/index.mjs
|
|
360
366
|
var import_main = require_main();
|
|
361
|
-
var version = "0.8.
|
|
367
|
+
var version = "0.8.32";
|
|
362
368
|
function getLangVersion() {
|
|
363
369
|
if (typeof process !== "undefined" && process.versions && process.versions.node) return `node-${process.versions.node}`;
|
|
364
370
|
if (typeof navigator !== "undefined" && navigator.userAgent) return `browser-${navigator.userAgent}`;
|
|
@@ -1210,19 +1216,19 @@ function outputJsonError(error, exitCode = 1) {
|
|
|
1210
1216
|
const DEFAULT_FRONTEND_URL$1 = "https://laminar.sh";
|
|
1211
1217
|
const DEFAULT_BASE_URL$1 = "https://api.lmnr.ai";
|
|
1212
1218
|
//#endregion
|
|
1213
|
-
//#region src/utils/project-
|
|
1214
|
-
const
|
|
1215
|
-
const
|
|
1219
|
+
//#region src/utils/local-project-file.ts
|
|
1220
|
+
const LOCAL_LMNR_DIR = ".lmnr";
|
|
1221
|
+
const LOCAL_LMNR_PROJECT_FILE = "project.json";
|
|
1216
1222
|
/**
|
|
1217
1223
|
* Find the nearest `.lmnr/project.json`, walking up from `startDir` to the
|
|
1218
1224
|
* filesystem root (so commands work from subdirectories of a linked project).
|
|
1219
1225
|
* Returns null if none is found.
|
|
1220
1226
|
*/
|
|
1221
|
-
async function
|
|
1227
|
+
async function readLocalProjectFile(startDir = process.cwd()) {
|
|
1222
1228
|
let dir = startDir;
|
|
1223
1229
|
const root = (0, node_path.parse)(dir).root;
|
|
1224
1230
|
while (true) {
|
|
1225
|
-
const candidate = (0, node_path.join)(dir,
|
|
1231
|
+
const candidate = (0, node_path.join)(dir, LOCAL_LMNR_DIR, LOCAL_LMNR_PROJECT_FILE);
|
|
1226
1232
|
try {
|
|
1227
1233
|
const parsed = JSON.parse(await (0, node_fs_promises.readFile)(candidate, "utf8"));
|
|
1228
1234
|
if (parsed && typeof parsed.projectId === "string" && parsed.projectId.length > 0) return parsed;
|
|
@@ -1234,10 +1240,10 @@ async function readProjectLink(startDir = process.cwd()) {
|
|
|
1234
1240
|
return null;
|
|
1235
1241
|
}
|
|
1236
1242
|
/** Write `.lmnr/project.json` under `dir` (default cwd). Returns the file path. */
|
|
1237
|
-
async function
|
|
1238
|
-
const linkDir = (0, node_path.join)(dir,
|
|
1243
|
+
async function writeLocalProjectFile(link, dir = process.cwd()) {
|
|
1244
|
+
const linkDir = (0, node_path.join)(dir, LOCAL_LMNR_DIR);
|
|
1239
1245
|
await (0, node_fs_promises.mkdir)(linkDir, { recursive: true });
|
|
1240
|
-
const path = (0, node_path.join)(linkDir,
|
|
1246
|
+
const path = (0, node_path.join)(linkDir, LOCAL_LMNR_PROJECT_FILE);
|
|
1241
1247
|
await (0, node_fs_promises.writeFile)(path, JSON.stringify(link, null, 2) + "\n", "utf8");
|
|
1242
1248
|
return path;
|
|
1243
1249
|
}
|
|
@@ -1495,7 +1501,7 @@ async function resolveAuth(opts) {
|
|
|
1495
1501
|
const creds = await readCredentials();
|
|
1496
1502
|
if (!creds) throw new Error("Not authenticated. Run `lmnr-cli login`.");
|
|
1497
1503
|
let projectId = opts.projectId;
|
|
1498
|
-
if (!projectId || projectId.length === 0) projectId = (await
|
|
1504
|
+
if (!projectId || projectId.length === 0) projectId = (await readLocalProjectFile())?.projectId;
|
|
1499
1505
|
if (!projectId || projectId.length === 0) throw new Error("No project for this directory. Run `lmnr-cli setup` here, or pass --project-id <id>.");
|
|
1500
1506
|
return {
|
|
1501
1507
|
bearer: (await refreshIfNeeded(creds)).accessToken,
|
|
@@ -1594,6 +1600,15 @@ function runWithEnvelope(work, opts, exitCodeFor) {
|
|
|
1594
1600
|
});
|
|
1595
1601
|
}
|
|
1596
1602
|
/**
|
|
1603
|
+
* Wrap a local-only command handler. No auth resolution / client build (the
|
|
1604
|
+
* command must not call the API), but the same positionals + options threading
|
|
1605
|
+
* and error envelope as the client wrappers, so handlers stay pure.
|
|
1606
|
+
*/
|
|
1607
|
+
const withLocalOpts = (action, exitCodeFor = defaultExitCode) => async (...cmdArgs) => {
|
|
1608
|
+
const { positionals, opts } = splitCommanderArgs(cmdArgs);
|
|
1609
|
+
await runWithEnvelope(() => action(...positionals, opts), opts, exitCodeFor);
|
|
1610
|
+
};
|
|
1611
|
+
/**
|
|
1597
1612
|
* Wrap a project-scoped command handler. Resolves a user-token
|
|
1598
1613
|
* {@link LaminarClient} (routes to `/v1/cli/*` with the resolved project),
|
|
1599
1614
|
* threads the commander positionals + options through, and owns the error
|
|
@@ -1853,8 +1868,7 @@ function fitColumnWidths(contentWidths, terminalWidth) {
|
|
|
1853
1868
|
const totalContentWidth = contentWidths.reduce((sum, w) => sum + w, 0);
|
|
1854
1869
|
if (totalContentWidth + overhead <= terminalWidth) return;
|
|
1855
1870
|
const availableContent = terminalWidth - overhead;
|
|
1856
|
-
|
|
1857
|
-
return contentWidths.map((w) => Math.max(minColWidth + PADDING_RIGHT, Math.floor(w / totalContentWidth * availableContent) + PADDING_RIGHT));
|
|
1871
|
+
return contentWidths.map((w) => Math.max(7, Math.floor(w / totalContentWidth * availableContent) + PADDING_RIGHT));
|
|
1858
1872
|
}
|
|
1859
1873
|
function truncate(str, maxLen) {
|
|
1860
1874
|
if (maxLen < 1) return str;
|
|
@@ -2006,6 +2020,99 @@ const handleDatasetsCreate = async (client, name, paths, opts) => {
|
|
|
2006
2020
|
else logger$2.info(`Successfully created dataset '${name}' and saved ${result.length} datapoints to ${opts.outputFile}`);
|
|
2007
2021
|
};
|
|
2008
2022
|
//#endregion
|
|
2023
|
+
//#region src/utils/debug-session-file.ts
|
|
2024
|
+
const str = (v) => typeof v === "string" && v.length > 0 ? v : null;
|
|
2025
|
+
/**
|
|
2026
|
+
* Read `${dir ?? cwd}/.lmnr/debug-session.json`. Best-effort: returns null on a
|
|
2027
|
+
* missing / unreadable / malformed file, or one with no usable `session_id`.
|
|
2028
|
+
* Mirrors the SDK's reader (`@lmnr-ai/lmnr` `src/debug/debug-session-file.ts`)
|
|
2029
|
+
* over the shared `DebugSessionFile` contract from `@lmnr-ai/types`.
|
|
2030
|
+
*/
|
|
2031
|
+
const readDebugSessionFile = (dir = process.cwd()) => {
|
|
2032
|
+
try {
|
|
2033
|
+
const raw = (0, node_fs.readFileSync)((0, node_path.join)(dir, DEBUG_SESSION_DIR, DEBUG_SESSION_FILE), "utf-8");
|
|
2034
|
+
const r = JSON.parse(raw);
|
|
2035
|
+
const session_id = str(r.session_id);
|
|
2036
|
+
if (!session_id) return null;
|
|
2037
|
+
return {
|
|
2038
|
+
session_id,
|
|
2039
|
+
trace_id: str(r.trace_id),
|
|
2040
|
+
replay_trace_id: str(r.replay_trace_id),
|
|
2041
|
+
cache_until: str(r.cache_until),
|
|
2042
|
+
debugger_url: str(r.debugger_url),
|
|
2043
|
+
started_at: str(r.started_at) ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
2044
|
+
};
|
|
2045
|
+
} catch {
|
|
2046
|
+
return null;
|
|
2047
|
+
}
|
|
2048
|
+
};
|
|
2049
|
+
/**
|
|
2050
|
+
* Find the nearest directory (walking up from `startDir` to the filesystem
|
|
2051
|
+
* root) whose `.lmnr/debug-session.json` holds a usable session record, so CLI
|
|
2052
|
+
* commands run from a subdirectory of a project act on the project's session.
|
|
2053
|
+
* Mirrors the SDK's `findDebugSessionDir`. Returns null when no ancestor
|
|
2054
|
+
* (including `startDir`) has one.
|
|
2055
|
+
*/
|
|
2056
|
+
const findDebugSessionDir = (startDir = process.cwd()) => {
|
|
2057
|
+
let dir = (0, node_path.resolve)(startDir);
|
|
2058
|
+
const root = (0, node_path.parse)(dir).root;
|
|
2059
|
+
while (true) {
|
|
2060
|
+
if (readDebugSessionFile(dir) !== null) return dir;
|
|
2061
|
+
const parent = (0, node_path.dirname)(dir);
|
|
2062
|
+
if (parent === dir || dir === root) return null;
|
|
2063
|
+
dir = parent;
|
|
2064
|
+
}
|
|
2065
|
+
};
|
|
2066
|
+
/**
|
|
2067
|
+
* The directory the debug-session file should be read from AND written to: the
|
|
2068
|
+
* nearest ancestor (incl. `startDir`) that already has one, else `startDir`
|
|
2069
|
+
* itself. Read and write MUST share this anchor — `debug session new` resets
|
|
2070
|
+
* the nearest existing file rather than shadowing it with a nested copy.
|
|
2071
|
+
*/
|
|
2072
|
+
const resolveDebugSessionDir = (startDir = process.cwd()) => findDebugSessionDir(startDir) ?? (0, node_path.resolve)(startDir);
|
|
2073
|
+
/**
|
|
2074
|
+
* Resolve the session id a debug command should act on: the explicit
|
|
2075
|
+
* `--session-id` value when given, else the `session_id` of the nearest
|
|
2076
|
+
* `.lmnr/debug-session.json` (walking up from `startDir`). Throws an
|
|
2077
|
+
* actionable error when neither exists; the command wrapper's error envelope
|
|
2078
|
+
* formats it.
|
|
2079
|
+
*/
|
|
2080
|
+
const resolveSessionId = (explicit, startDir) => {
|
|
2081
|
+
if (explicit) return explicit;
|
|
2082
|
+
const file = readDebugSessionFile(resolveDebugSessionDir(startDir));
|
|
2083
|
+
if (file?.session_id) return file.session_id;
|
|
2084
|
+
throw new Error("No session id given and no .lmnr/debug-session.json found in this directory. Pass --session-id <id> or run `lmnr-cli debug session new`.");
|
|
2085
|
+
};
|
|
2086
|
+
/**
|
|
2087
|
+
* Resolve the trace id a trace command should act on: the explicit `--trace-id`
|
|
2088
|
+
* value when given, else the `trace_id` of the nearest
|
|
2089
|
+
* `.lmnr/debug-session.json` (the root trace of the most recent debug run,
|
|
2090
|
+
* walking up from `startDir`). Throws an actionable error when neither exists;
|
|
2091
|
+
* the command wrapper's error envelope formats it.
|
|
2092
|
+
*/
|
|
2093
|
+
const resolveTraceId = (explicit, startDir) => {
|
|
2094
|
+
if (explicit) return explicit;
|
|
2095
|
+
const traceId = readDebugSessionFile(resolveDebugSessionDir(startDir))?.trace_id;
|
|
2096
|
+
if (traceId) return traceId;
|
|
2097
|
+
throw new Error("No trace id given and .lmnr/debug-session.json has no trace_id (no debug run has completed in this directory yet). Pass --trace-id <id> or run your program with LMNR_DEBUG=1 first.");
|
|
2098
|
+
};
|
|
2099
|
+
/**
|
|
2100
|
+
* Write `${dir ?? cwd}/.lmnr/debug-session.json` (mkdir -p first). Best-effort:
|
|
2101
|
+
* swallows IO errors. Shares the `DebugSessionFile` contract + filename consts
|
|
2102
|
+
* with the SDK (`@lmnr-ai/types`), so the SDK reads this same file at init to
|
|
2103
|
+
* join the session `debug session new` minted.
|
|
2104
|
+
*/
|
|
2105
|
+
const writeDebugSessionFile = (file, dir = process.cwd()) => {
|
|
2106
|
+
try {
|
|
2107
|
+
const directory = (0, node_path.join)(dir, DEBUG_SESSION_DIR);
|
|
2108
|
+
(0, node_fs.mkdirSync)(directory, { recursive: true });
|
|
2109
|
+
(0, node_fs.writeFileSync)((0, node_path.join)(directory, DEBUG_SESSION_FILE), JSON.stringify(file), "utf-8");
|
|
2110
|
+
return true;
|
|
2111
|
+
} catch {
|
|
2112
|
+
return false;
|
|
2113
|
+
}
|
|
2114
|
+
};
|
|
2115
|
+
//#endregion
|
|
2009
2116
|
//#region src/utils/trace-note.ts
|
|
2010
2117
|
const NOTE_METADATA_KEY = "rollout.note";
|
|
2011
2118
|
/**
|
|
@@ -2045,11 +2152,15 @@ const logger$1 = initializeLogger();
|
|
|
2045
2152
|
* Upsert the display name of a debug session. Update-only on the backend: a
|
|
2046
2153
|
* session id unknown to the project 404s rather than creating a ghost session.
|
|
2047
2154
|
*
|
|
2155
|
+
* The target session is the optional `--session-id` flag; when omitted the
|
|
2156
|
+
* session comes from `.lmnr/debug-session.json` (see `resolveSessionId`).
|
|
2157
|
+
*
|
|
2048
2158
|
* Pure handler: the command wrapper (`withProjectClient`) resolves a user-token
|
|
2049
2159
|
* {@link LaminarClient} (routes to `/v1/cli/*` with the resolved project) and
|
|
2050
2160
|
* owns the error envelope.
|
|
2051
2161
|
*/
|
|
2052
|
-
const handleDebugSessionSetName = async (client,
|
|
2162
|
+
const handleDebugSessionSetName = async (client, name, opts) => {
|
|
2163
|
+
const sessionId = resolveSessionId(opts.sessionId);
|
|
2053
2164
|
await client.rolloutSessions.setName({
|
|
2054
2165
|
sessionId,
|
|
2055
2166
|
name
|
|
@@ -2068,11 +2179,15 @@ const handleDebugSessionSetName = async (client, sessionId, name, opts) => {
|
|
|
2068
2179
|
* groups it to the session (`rollout.session_id`), oldest first, with the
|
|
2069
2180
|
* agent-authored note (`rollout.note`) attached to each.
|
|
2070
2181
|
*
|
|
2182
|
+
* The target session is the optional `--session-id` flag; when omitted the
|
|
2183
|
+
* session comes from `.lmnr/debug-session.json` (see `resolveSessionId`).
|
|
2184
|
+
*
|
|
2071
2185
|
* Pure handler: the command wrapper (`withProjectClient`) resolves a user-token
|
|
2072
2186
|
* {@link LaminarClient} (routes to `/v1/cli/*` with the resolved project) and
|
|
2073
2187
|
* owns the error envelope.
|
|
2074
2188
|
*/
|
|
2075
|
-
const handleDebugSessionSummary = async (client,
|
|
2189
|
+
const handleDebugSessionSummary = async (client, opts) => {
|
|
2190
|
+
const sessionId = resolveSessionId(opts.sessionId);
|
|
2076
2191
|
const traces = (await client.sql.query("SELECT id, formatDateTime(end_time, '%Y-%m-%dT%H:%i:%S.%fZ') AS end_time, metadata FROM traces WHERE simpleJSONExtractString(metadata, 'rollout.session_id') = {session_id:String} ORDER BY start_time", { session_id: sessionId })).map((row) => ({
|
|
2077
2192
|
note: readNoteFromMetadata(row.metadata),
|
|
2078
2193
|
traceId: String(row.id ?? ""),
|
|
@@ -2092,6 +2207,104 @@ const handleDebugSessionSummary = async (client, sessionId, opts) => {
|
|
|
2092
2207
|
});
|
|
2093
2208
|
console.log(blocks.join("\n\n"));
|
|
2094
2209
|
};
|
|
2210
|
+
/**
|
|
2211
|
+
* Build the frontend debugger-session URL. The frontend URL is its own env var
|
|
2212
|
+
* (LMNR_FRONTEND_URL) with a cloud default; self-host/local sets it explicitly.
|
|
2213
|
+
*/
|
|
2214
|
+
const buildDebuggerUrl = (projectId, sessionId) => {
|
|
2215
|
+
return `${process.env.LMNR_FRONTEND_URL?.trim().replace(/\/+$/, "") || "https://laminar.sh"}/project/${projectId}/debugger-sessions/${sessionId}`;
|
|
2216
|
+
};
|
|
2217
|
+
/**
|
|
2218
|
+
* Open a debug session's debugger page in the browser.
|
|
2219
|
+
*
|
|
2220
|
+
* The target session is the optional `--session-id` flag; when omitted the
|
|
2221
|
+
* session comes from `.lmnr/debug-session.json` (see `resolveSessionId`).
|
|
2222
|
+
* The URL is the file's stored `debugger_url` when it belongs to the resolved
|
|
2223
|
+
* session, else it is rebuilt from the resolved project (`--project-id` or the
|
|
2224
|
+
* linked `.lmnr/project.json`) + LMNR_FRONTEND_URL.
|
|
2225
|
+
*
|
|
2226
|
+
* Local-only handler (registered via `withLocalOpts`, NOT `withProjectClient`):
|
|
2227
|
+
* everything needed lives on disk, so no auth resolution / API call — `open`
|
|
2228
|
+
* works offline and before login.
|
|
2229
|
+
*/
|
|
2230
|
+
const handleDebugSessionOpen = async (opts) => {
|
|
2231
|
+
const sessionId = resolveSessionId(opts.sessionId);
|
|
2232
|
+
const file = readDebugSessionFile(resolveDebugSessionDir());
|
|
2233
|
+
let debuggerUrl = file?.session_id === sessionId ? file.debugger_url : null;
|
|
2234
|
+
if (!debuggerUrl) {
|
|
2235
|
+
const projectId = opts.projectId || (await readLocalProjectFile())?.projectId;
|
|
2236
|
+
if (!projectId) throw new Error("Cannot build the debugger URL: no project is linked to this directory. Pass --project-id or run `lmnr-cli setup`.");
|
|
2237
|
+
debuggerUrl = buildDebuggerUrl(projectId, sessionId);
|
|
2238
|
+
}
|
|
2239
|
+
if (opts.json) outputJson({
|
|
2240
|
+
sessionId,
|
|
2241
|
+
debuggerUrl
|
|
2242
|
+
});
|
|
2243
|
+
else console.log(debuggerUrl);
|
|
2244
|
+
try {
|
|
2245
|
+
await (0, open.default)(debuggerUrl);
|
|
2246
|
+
} catch (e) {
|
|
2247
|
+
logger$1.warn(`Could not open a browser (${errorMessage(e)}). URL: ${debuggerUrl}`);
|
|
2248
|
+
}
|
|
2249
|
+
};
|
|
2250
|
+
/**
|
|
2251
|
+
* Mint a fresh debug session and reset `.lmnr/debug-session.json` to it.
|
|
2252
|
+
*
|
|
2253
|
+
* The next `LMNR_DEBUG=1 <run>` in this directory reads that file and rejoins the
|
|
2254
|
+
* minted session silently (no browser) — "new session" is owned by this command;
|
|
2255
|
+
* the SDK only mints when no file exists.
|
|
2256
|
+
*
|
|
2257
|
+
* Ordering matters: the local file is written FIRST (so the minted session is
|
|
2258
|
+
* usable for `LMNR_DEBUG=1` continuation even if the network is down / the
|
|
2259
|
+
* backend endpoint isn't deployed yet), THEN the session is best-effort
|
|
2260
|
+
* registered with the backend. A registration failure WARNS but does not fail
|
|
2261
|
+
* the command (exit 0) — the file is already written.
|
|
2262
|
+
*
|
|
2263
|
+
* Pure handler: the command wrapper (`withProjectClient`) resolves a user-token
|
|
2264
|
+
* {@link LaminarClient} (routes `register()` to `POST /v1/cli/rollouts/{id}` with
|
|
2265
|
+
* the resolved project) and owns the error envelope.
|
|
2266
|
+
*/
|
|
2267
|
+
const handleDebugSessionNew = async (client, opts) => {
|
|
2268
|
+
const sessionId = (0, node_crypto.randomUUID)();
|
|
2269
|
+
const sessionDir = resolveDebugSessionDir();
|
|
2270
|
+
writeDebugSessionFile({
|
|
2271
|
+
session_id: sessionId,
|
|
2272
|
+
trace_id: null,
|
|
2273
|
+
replay_trace_id: null,
|
|
2274
|
+
cache_until: null,
|
|
2275
|
+
debugger_url: null,
|
|
2276
|
+
started_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
2277
|
+
}, sessionDir);
|
|
2278
|
+
let projectId = null;
|
|
2279
|
+
try {
|
|
2280
|
+
projectId = await client.rolloutSessions.register({ sessionId });
|
|
2281
|
+
} catch (e) {
|
|
2282
|
+
logger$1.warn(`Could not register the debug session with the backend (the local .lmnr/debug-session.json is still usable): ${errorMessage(e)}`);
|
|
2283
|
+
}
|
|
2284
|
+
const debuggerUrl = projectId ? buildDebuggerUrl(projectId, sessionId) : null;
|
|
2285
|
+
if (debuggerUrl) writeDebugSessionFile({
|
|
2286
|
+
session_id: sessionId,
|
|
2287
|
+
trace_id: null,
|
|
2288
|
+
replay_trace_id: null,
|
|
2289
|
+
cache_until: null,
|
|
2290
|
+
debugger_url: debuggerUrl,
|
|
2291
|
+
started_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
2292
|
+
}, sessionDir);
|
|
2293
|
+
if (opts.json) {
|
|
2294
|
+
outputJson({
|
|
2295
|
+
sessionId,
|
|
2296
|
+
projectId,
|
|
2297
|
+
debuggerUrl
|
|
2298
|
+
});
|
|
2299
|
+
return;
|
|
2300
|
+
}
|
|
2301
|
+
if (debuggerUrl) logger$1.info(`New debug session: ${debuggerUrl}`);
|
|
2302
|
+
else logger$1.info(`New debug session: ${sessionId}`);
|
|
2303
|
+
console.log(sessionId);
|
|
2304
|
+
if (opts.browser !== false && debuggerUrl) try {
|
|
2305
|
+
await (0, open.default)(debuggerUrl);
|
|
2306
|
+
} catch {}
|
|
2307
|
+
};
|
|
2095
2308
|
//#endregion
|
|
2096
2309
|
//#region src/utils/colors.ts
|
|
2097
2310
|
function enabledFor(stream) {
|
|
@@ -2195,7 +2408,7 @@ function trimSlash$2(url) {
|
|
|
2195
2408
|
*/
|
|
2196
2409
|
const handleProjectsList = async (client, opts) => {
|
|
2197
2410
|
const projects = await client.cli.listProjects();
|
|
2198
|
-
const linked = (await
|
|
2411
|
+
const linked = (await readLocalProjectFile())?.projectId;
|
|
2199
2412
|
if (opts.json) {
|
|
2200
2413
|
outputJson(projects.map((p) => ({
|
|
2201
2414
|
...p,
|
|
@@ -2565,7 +2778,7 @@ async function handleSetup(options) {
|
|
|
2565
2778
|
const cwd = process.cwd();
|
|
2566
2779
|
const existingKey = await findEnvKey(cwd);
|
|
2567
2780
|
let creds = await safeReadCredentials();
|
|
2568
|
-
let link = await
|
|
2781
|
+
let link = await readLocalProjectFile();
|
|
2569
2782
|
if (!creds) {
|
|
2570
2783
|
let login;
|
|
2571
2784
|
try {
|
|
@@ -2742,7 +2955,7 @@ async function resolveProjectViaCli(creds, userBaseUrl, issuer, isJson, options)
|
|
|
2742
2955
|
}
|
|
2743
2956
|
chosen = await promptProjectChoice(projects);
|
|
2744
2957
|
}
|
|
2745
|
-
const linkPath = await
|
|
2958
|
+
const linkPath = await writeLocalProjectFile({
|
|
2746
2959
|
projectId: chosen.id,
|
|
2747
2960
|
projectName: chosen.name,
|
|
2748
2961
|
workspaceId: chosen.workspaceId,
|
|
@@ -2772,7 +2985,7 @@ async function writeLink(issuer, userBaseUrl, projectId, isJson) {
|
|
|
2772
2985
|
}
|
|
2773
2986
|
} catch {}
|
|
2774
2987
|
try {
|
|
2775
|
-
const linkPath = await
|
|
2988
|
+
const linkPath = await writeLocalProjectFile(link);
|
|
2776
2989
|
if (!isJson) process.stderr.write(`${pc.green("✓")} Linked ${linkPath}\n`);
|
|
2777
2990
|
} catch (err) {
|
|
2778
2991
|
if (!isJson) process.stderr.write(`${pc.yellow("Warning")}: could not write .lmnr/project.json (${describeError(err)}). CLI commands will need --project-id ${projectId}.\n`);
|
|
@@ -2924,7 +3137,15 @@ Available tables:
|
|
|
2924
3137
|
|
|
2925
3138
|
signal_events
|
|
2926
3139
|
id (UUID), signal_id (UUID), trace_id (UUID), run_id (UUID),
|
|
2927
|
-
name (String), payload (String), timestamp (DateTime64)
|
|
3140
|
+
name (String), payload (String), timestamp (DateTime64),
|
|
3141
|
+
severity (UInt8: 0=INFO|1=WARNING|2=CRITICAL), summary (String),
|
|
3142
|
+
clusters (Array(UUID))
|
|
3143
|
+
|
|
3144
|
+
clusters
|
|
3145
|
+
id (UUID), signal_id (UUID), name (String),
|
|
3146
|
+
level (UInt8), parent_id (UUID), num_signal_events (UInt32),
|
|
3147
|
+
num_children_clusters (UInt16),
|
|
3148
|
+
created_at (DateTime64), updated_at (DateTime64)
|
|
2928
3149
|
|
|
2929
3150
|
signal_runs
|
|
2930
3151
|
signal_id (UUID), job_id (UUID), trigger_id (UUID), run_id (UUID),
|
|
@@ -2953,6 +3174,10 @@ const NOTE_SEPARATOR = "\n\n";
|
|
|
2953
3174
|
* as `existing + "\n\n" + note`. The note may contain markdown /
|
|
2954
3175
|
* span-reference links.
|
|
2955
3176
|
*
|
|
3177
|
+
* The target trace is the optional `--trace-id` flag; when omitted the trace
|
|
3178
|
+
* comes from `.lmnr/debug-session.json`'s `trace_id` — the root trace of the
|
|
3179
|
+
* most recent debug run in this directory (see `resolveTraceId`).
|
|
3180
|
+
*
|
|
2956
3181
|
* Pure handler: the command wrapper (`withProjectClient`) resolves a user-token
|
|
2957
3182
|
* {@link LaminarClient} (routes to `/v1/cli/*` with the resolved project) and
|
|
2958
3183
|
* owns the error envelope (`--json` → structured error + exit, else log + exit).
|
|
@@ -2967,8 +3192,8 @@ const NOTE_SEPARATOR = "\n\n";
|
|
|
2967
3192
|
* the metadata patch endpoint that concatenates within the Postgres UPDATE,
|
|
2968
3193
|
* which already serializes on the trace row lock).
|
|
2969
3194
|
*/
|
|
2970
|
-
const handleTraceAppendNote = async (client,
|
|
2971
|
-
const id = normalizeTraceId(traceId);
|
|
3195
|
+
const handleTraceAppendNote = async (client, note, opts) => {
|
|
3196
|
+
const id = normalizeTraceId(resolveTraceId(opts.traceId));
|
|
2972
3197
|
const rows = await client.sql.query("SELECT metadata FROM traces WHERE id = {trace_id:UUID} LIMIT 1", { trace_id: id });
|
|
2973
3198
|
if (rows.length === 0) throw new Error(`Trace ${id} not found. If the run just finished, the trace may not be flushed yet. Retry in a few seconds.`);
|
|
2974
3199
|
const existing = readNoteFromMetadata(rows[0].metadata);
|
|
@@ -3017,23 +3242,35 @@ Examples:
|
|
|
3017
3242
|
program.command("setup").description("One-shot onboarding: login, select a project, write its key to .env, link .lmnr, and install the Laminar agent skill").option("--write-env", "Write LMNR_PROJECT_API_KEY to ./.env (default)", true).option("--no-write-env", "Do not write to ./.env").option("--project-id <id>", "Project to link when you can access more than one (disambiguates the project_ambiguous case in --json mode)").option("--json", "Emit a machine-readable JSON line on stdout").option("--no-browser", "Do not auto-open the device-flow URL").option("--frontend-url <url>", "Frontend URL (issuer). Defaults to LMNR_FRONTEND_URL or https://laminar.sh").option("--base-url <url>", "Base URL for the Laminar API. Defaults to LMNR_BASE_URL or https://api.lmnr.ai").action(async (options) => {
|
|
3018
3243
|
await handleSetup(options);
|
|
3019
3244
|
});
|
|
3020
|
-
program.command("trace").description("Inspect and operate on traces").option("--project-id <id>", "Target project id. Defaults to the linked .lmnr/project.json. Run `lmnr-cli login` first.").option("--base-url <url>", "Base URL for the Laminar API. Defaults to https://api.lmnr.ai or LMNR_BASE_URL env variable").option("--port <port>", "Port for the Laminar API. Defaults to 443", (val) => parseInt(val, 10)).option("--json", "Output structured JSON to stdout").command("append-note").description("Append a free-text note to a trace (stored in trace metadata)").argument("<trace-id>", "Trace ID (UUID or 32-char OTel hex trace id)
|
|
3245
|
+
program.command("trace").description("Inspect and operate on traces").option("--project-id <id>", "Target project id. Defaults to the linked .lmnr/project.json. Run `lmnr-cli login` first.").option("--base-url <url>", "Base URL for the Laminar API. Defaults to https://api.lmnr.ai or LMNR_BASE_URL env variable").option("--port <port>", "Port for the Laminar API. Defaults to 443", (val) => parseInt(val, 10)).option("--json", "Output structured JSON to stdout").command("append-note").description("Append a free-text note to a trace (stored in trace metadata)").argument("<note>", "Note text (may contain markdown)").option("--trace-id <id>", "Trace ID (UUID or 32-char OTel hex trace id). Defaults to the trace_id of .lmnr/debug-session.json (the most recent debug run here)").action(withProjectClient(handleTraceAppendNote)).addHelpText("after", `
|
|
3021
3246
|
Notes accumulate: each call appends a new paragraph to the trace's existing
|
|
3022
3247
|
note rather than overwriting it.
|
|
3023
3248
|
|
|
3249
|
+
Without --trace-id, the note goes to the trace_id recorded in
|
|
3250
|
+
.lmnr/debug-session.json — the root trace of the most recent LMNR_DEBUG=1 run
|
|
3251
|
+
in this directory.
|
|
3252
|
+
|
|
3024
3253
|
Examples:
|
|
3025
|
-
$ lmnr-cli trace append-note
|
|
3254
|
+
$ lmnr-cli trace append-note "Reproduced the timeout on the search tool."
|
|
3255
|
+
$ lmnr-cli trace append-note "Reproduced the timeout." --trace-id <trace-id>
|
|
3026
3256
|
`);
|
|
3027
3257
|
const debugSessionCmd = program.command("debug").description("Operate on debug sessions").option("--project-id <id>", "Target project id. Defaults to the linked .lmnr/project.json. Run `lmnr-cli login` first.").option("--base-url <url>", "Base URL for the Laminar API. Defaults to https://api.lmnr.ai or LMNR_BASE_URL env variable").option("--port <port>", "Port for the Laminar API. Defaults to 443", (val) => parseInt(val, 10)).option("--json", "Output structured JSON to stdout").addHelpText("after", `
|
|
3028
3258
|
Learn more about debugging features at https://laminar.sh/docs/platform/debugger
|
|
3029
3259
|
`).command("session").description("Manage debug sessions").addHelpText("after", `
|
|
3030
3260
|
Learn more about debugging features at https://laminar.sh/docs/platform/debugger
|
|
3031
3261
|
`);
|
|
3032
|
-
debugSessionCmd.command("set-name").description("Set the display name of a debug session").argument("<
|
|
3262
|
+
debugSessionCmd.command("set-name").description("Set the display name of a debug session").argument("<name>", "Session display name").option("--session-id <id>", "Debug session ID. Defaults to the session in .lmnr/debug-session.json").action(withProjectClient(handleDebugSessionSetName)).addHelpText("after", `
|
|
3263
|
+
Without --session-id, the name applies to the session recorded in
|
|
3264
|
+
.lmnr/debug-session.json (written by \`debug session new\` / LMNR_DEBUG=1 runs).
|
|
3265
|
+
|
|
3033
3266
|
Examples:
|
|
3034
|
-
$ lmnr-cli debug session set-name
|
|
3267
|
+
$ lmnr-cli debug session set-name "Fix report length + search tool"
|
|
3268
|
+
$ lmnr-cli debug session set-name "Fix report length" --session-id <session-id>
|
|
3035
3269
|
`);
|
|
3036
|
-
debugSessionCmd.command("summary").description("Print every trace in a debug session with its note, oldest first").
|
|
3270
|
+
debugSessionCmd.command("summary").description("Print every trace in a debug session with its note, oldest first").option("--session-id <id>", "Debug session ID. Defaults to the session in .lmnr/debug-session.json").action(withProjectClient(handleDebugSessionSummary)).addHelpText("after", `
|
|
3271
|
+
Without --session-id, summarizes the session recorded in
|
|
3272
|
+
.lmnr/debug-session.json (written by \`debug session new\` / LMNR_DEBUG=1 runs).
|
|
3273
|
+
|
|
3037
3274
|
Output is one block per trace (oldest first), the trace's note followed by a
|
|
3038
3275
|
self-closing tag carrying the trace id and end time:
|
|
3039
3276
|
|
|
@@ -3043,8 +3280,31 @@ self-closing tag carrying the trace id and end time:
|
|
|
3043
3280
|
With --json, prints an array of {"note", "traceId", "endTime"} objects.
|
|
3044
3281
|
|
|
3045
3282
|
Examples:
|
|
3046
|
-
$ lmnr-cli debug session summary
|
|
3047
|
-
$ lmnr-cli debug session summary <session-id> --json
|
|
3283
|
+
$ lmnr-cli debug session summary
|
|
3284
|
+
$ lmnr-cli debug session summary --session-id <session-id> --json
|
|
3285
|
+
`);
|
|
3286
|
+
debugSessionCmd.command("open").description("Open a debug session's debugger page in the browser").option("--session-id <id>", "Debug session ID. Defaults to the session in .lmnr/debug-session.json").action(withLocalOpts(handleDebugSessionOpen)).addHelpText("after", `
|
|
3287
|
+
Without --session-id, opens the session recorded in .lmnr/debug-session.json
|
|
3288
|
+
(written by \`debug session new\` / LMNR_DEBUG=1 runs). The URL is also printed
|
|
3289
|
+
to stdout; in --json mode a {"sessionId","debuggerUrl"} object is printed
|
|
3290
|
+
instead. Local-only: no login or network needed.
|
|
3291
|
+
|
|
3292
|
+
Examples:
|
|
3293
|
+
$ lmnr-cli debug session open
|
|
3294
|
+
$ lmnr-cli debug session open --session-id <session-id>
|
|
3295
|
+
`);
|
|
3296
|
+
debugSessionCmd.command("new").description("Create a fresh debug session and reset .lmnr/debug-session.json").option("--no-browser", "Do not open the debugger session URL in a browser").action(withProjectClient(handleDebugSessionNew)).addHelpText("after", `
|
|
3297
|
+
Mints a new session id, writes it to .lmnr/debug-session.json (resetting any
|
|
3298
|
+
prior session), and registers it with the backend. The next \`LMNR_DEBUG=1 <run>\`
|
|
3299
|
+
in this directory rejoins this session silently (no browser).
|
|
3300
|
+
|
|
3301
|
+
The bare session id is printed to stdout; in --json mode a
|
|
3302
|
+
{"sessionId","projectId","debuggerUrl"} object is printed instead.
|
|
3303
|
+
|
|
3304
|
+
Examples:
|
|
3305
|
+
$ lmnr-cli debug session new
|
|
3306
|
+
$ lmnr-cli debug session new --json
|
|
3307
|
+
$ lmnr-cli debug session new --no-browser
|
|
3048
3308
|
`);
|
|
3049
3309
|
program.addHelpText("after", `
|
|
3050
3310
|
Authentication:
|
|
@@ -3064,9 +3324,11 @@ Examples:
|
|
|
3064
3324
|
lmnr-cli dataset pull output.jsonl -n my-dataset --json # Pull data from a dataset
|
|
3065
3325
|
lmnr-cli sql query "SELECT * FROM spans LIMIT 10" --json # Query spans
|
|
3066
3326
|
lmnr-cli sql schema # Show available tables
|
|
3067
|
-
lmnr-cli trace append-note
|
|
3068
|
-
lmnr-cli debug session
|
|
3069
|
-
lmnr-cli debug session
|
|
3327
|
+
lmnr-cli trace append-note "note text" # Note on the latest debug trace
|
|
3328
|
+
lmnr-cli debug session new # Mint a fresh debug session
|
|
3329
|
+
lmnr-cli debug session open # Open the session in the browser
|
|
3330
|
+
lmnr-cli debug session set-name "title" # Rename the current debug session
|
|
3331
|
+
lmnr-cli debug session summary # Notes for each trace in the session
|
|
3070
3332
|
|
|
3071
3333
|
For more information about the Laminar platfrom:
|
|
3072
3334
|
Documentation: https://laminar.sh/docs
|