lmnr-cli 0.1.12 → 0.1.13

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 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 trace append-note <trace-id> "note text" # Append a markdown note to a trace
132
- lmnr-cli debug session set-name <session-id> "title" # Rename a debug session
133
- lmnr-cli debug session summary <session-id> # Every trace in a session + its note
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
@@ -31,6 +31,7 @@ let pino = require("pino");
31
31
  let pino$3 = __toESM(pino, 1);
32
32
  pino = __toESM(pino);
33
33
  let pino_pretty = require("pino-pretty");
34
+ let node_crypto = require("node:crypto");
34
35
  let node_fs_promises = require("node:fs/promises");
35
36
  let node_path = require("node:path");
36
37
  let node_os = require("node:os");
@@ -43,16 +44,21 @@ let cli_table3 = require("cli-table3");
43
44
  cli_table3 = __toESM(cli_table3);
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.12";
61
+ var version$1 = "0.1.13";
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) => {
@@ -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.29";
367
+ var version = "0.8.31";
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-link.ts
1214
- const LINK_DIR = ".lmnr";
1215
- const LINK_FILE = "project.json";
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 readProjectLink(startDir = process.cwd()) {
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, LINK_DIR, LINK_FILE);
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 writeProjectLink(link, dir = process.cwd()) {
1238
- const linkDir = (0, node_path.join)(dir, LINK_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, LINK_FILE);
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 readProjectLink())?.projectId;
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
@@ -2006,6 +2021,99 @@ const handleDatasetsCreate = async (client, name, paths, opts) => {
2006
2021
  else logger$2.info(`Successfully created dataset '${name}' and saved ${result.length} datapoints to ${opts.outputFile}`);
2007
2022
  };
2008
2023
  //#endregion
2024
+ //#region src/utils/debug-session-file.ts
2025
+ const str = (v) => typeof v === "string" && v.length > 0 ? v : null;
2026
+ /**
2027
+ * Read `${dir ?? cwd}/.lmnr/debug-session.json`. Best-effort: returns null on a
2028
+ * missing / unreadable / malformed file, or one with no usable `session_id`.
2029
+ * Mirrors the SDK's reader (`@lmnr-ai/lmnr` `src/debug/debug-session-file.ts`)
2030
+ * over the shared `DebugSessionFile` contract from `@lmnr-ai/types`.
2031
+ */
2032
+ const readDebugSessionFile = (dir = process.cwd()) => {
2033
+ try {
2034
+ const raw = (0, node_fs.readFileSync)((0, node_path.join)(dir, DEBUG_SESSION_DIR, DEBUG_SESSION_FILE), "utf-8");
2035
+ const r = JSON.parse(raw);
2036
+ const session_id = str(r.session_id);
2037
+ if (!session_id) return null;
2038
+ return {
2039
+ session_id,
2040
+ trace_id: str(r.trace_id),
2041
+ replay_trace_id: str(r.replay_trace_id),
2042
+ cache_until: str(r.cache_until),
2043
+ debugger_url: str(r.debugger_url),
2044
+ started_at: str(r.started_at) ?? (/* @__PURE__ */ new Date()).toISOString()
2045
+ };
2046
+ } catch {
2047
+ return null;
2048
+ }
2049
+ };
2050
+ /**
2051
+ * Find the nearest directory (walking up from `startDir` to the filesystem
2052
+ * root) whose `.lmnr/debug-session.json` holds a usable session record, so CLI
2053
+ * commands run from a subdirectory of a project act on the project's session.
2054
+ * Mirrors the SDK's `findDebugSessionDir`. Returns null when no ancestor
2055
+ * (including `startDir`) has one.
2056
+ */
2057
+ const findDebugSessionDir = (startDir = process.cwd()) => {
2058
+ let dir = (0, node_path.resolve)(startDir);
2059
+ const root = (0, node_path.parse)(dir).root;
2060
+ while (true) {
2061
+ if (readDebugSessionFile(dir) !== null) return dir;
2062
+ const parent = (0, node_path.dirname)(dir);
2063
+ if (parent === dir || dir === root) return null;
2064
+ dir = parent;
2065
+ }
2066
+ };
2067
+ /**
2068
+ * The directory the debug-session file should be read from AND written to: the
2069
+ * nearest ancestor (incl. `startDir`) that already has one, else `startDir`
2070
+ * itself. Read and write MUST share this anchor — `debug session new` resets
2071
+ * the nearest existing file rather than shadowing it with a nested copy.
2072
+ */
2073
+ const resolveDebugSessionDir = (startDir = process.cwd()) => findDebugSessionDir(startDir) ?? (0, node_path.resolve)(startDir);
2074
+ /**
2075
+ * Resolve the session id a debug command should act on: the explicit
2076
+ * `--session-id` value when given, else the `session_id` of the nearest
2077
+ * `.lmnr/debug-session.json` (walking up from `startDir`). Throws an
2078
+ * actionable error when neither exists; the command wrapper's error envelope
2079
+ * formats it.
2080
+ */
2081
+ const resolveSessionId = (explicit, startDir) => {
2082
+ if (explicit) return explicit;
2083
+ const file = readDebugSessionFile(resolveDebugSessionDir(startDir));
2084
+ if (file?.session_id) return file.session_id;
2085
+ 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`.");
2086
+ };
2087
+ /**
2088
+ * Resolve the trace id a trace command should act on: the explicit `--trace-id`
2089
+ * value when given, else the `trace_id` of the nearest
2090
+ * `.lmnr/debug-session.json` (the root trace of the most recent debug run,
2091
+ * walking up from `startDir`). Throws an actionable error when neither exists;
2092
+ * the command wrapper's error envelope formats it.
2093
+ */
2094
+ const resolveTraceId = (explicit, startDir) => {
2095
+ if (explicit) return explicit;
2096
+ const traceId = readDebugSessionFile(resolveDebugSessionDir(startDir))?.trace_id;
2097
+ if (traceId) return traceId;
2098
+ 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.");
2099
+ };
2100
+ /**
2101
+ * Write `${dir ?? cwd}/.lmnr/debug-session.json` (mkdir -p first). Best-effort:
2102
+ * swallows IO errors. Shares the `DebugSessionFile` contract + filename consts
2103
+ * with the SDK (`@lmnr-ai/types`), so the SDK reads this same file at init to
2104
+ * join the session `debug session new` minted.
2105
+ */
2106
+ const writeDebugSessionFile = (file, dir = process.cwd()) => {
2107
+ try {
2108
+ const directory = (0, node_path.join)(dir, DEBUG_SESSION_DIR);
2109
+ (0, node_fs.mkdirSync)(directory, { recursive: true });
2110
+ (0, node_fs.writeFileSync)((0, node_path.join)(directory, DEBUG_SESSION_FILE), JSON.stringify(file), "utf-8");
2111
+ return true;
2112
+ } catch {
2113
+ return false;
2114
+ }
2115
+ };
2116
+ //#endregion
2009
2117
  //#region src/utils/trace-note.ts
2010
2118
  const NOTE_METADATA_KEY = "rollout.note";
2011
2119
  /**
@@ -2045,11 +2153,15 @@ const logger$1 = initializeLogger();
2045
2153
  * Upsert the display name of a debug session. Update-only on the backend: a
2046
2154
  * session id unknown to the project 404s rather than creating a ghost session.
2047
2155
  *
2156
+ * The target session is the optional `--session-id` flag; when omitted the
2157
+ * session comes from `.lmnr/debug-session.json` (see `resolveSessionId`).
2158
+ *
2048
2159
  * Pure handler: the command wrapper (`withProjectClient`) resolves a user-token
2049
2160
  * {@link LaminarClient} (routes to `/v1/cli/*` with the resolved project) and
2050
2161
  * owns the error envelope.
2051
2162
  */
2052
- const handleDebugSessionSetName = async (client, sessionId, name, opts) => {
2163
+ const handleDebugSessionSetName = async (client, name, opts) => {
2164
+ const sessionId = resolveSessionId(opts.sessionId);
2053
2165
  await client.rolloutSessions.setName({
2054
2166
  sessionId,
2055
2167
  name
@@ -2068,11 +2180,15 @@ const handleDebugSessionSetName = async (client, sessionId, name, opts) => {
2068
2180
  * groups it to the session (`rollout.session_id`), oldest first, with the
2069
2181
  * agent-authored note (`rollout.note`) attached to each.
2070
2182
  *
2183
+ * The target session is the optional `--session-id` flag; when omitted the
2184
+ * session comes from `.lmnr/debug-session.json` (see `resolveSessionId`).
2185
+ *
2071
2186
  * Pure handler: the command wrapper (`withProjectClient`) resolves a user-token
2072
2187
  * {@link LaminarClient} (routes to `/v1/cli/*` with the resolved project) and
2073
2188
  * owns the error envelope.
2074
2189
  */
2075
- const handleDebugSessionSummary = async (client, sessionId, opts) => {
2190
+ const handleDebugSessionSummary = async (client, opts) => {
2191
+ const sessionId = resolveSessionId(opts.sessionId);
2076
2192
  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
2193
  note: readNoteFromMetadata(row.metadata),
2078
2194
  traceId: String(row.id ?? ""),
@@ -2092,6 +2208,104 @@ const handleDebugSessionSummary = async (client, sessionId, opts) => {
2092
2208
  });
2093
2209
  console.log(blocks.join("\n\n"));
2094
2210
  };
2211
+ /**
2212
+ * Build the frontend debugger-session URL. The frontend URL is its own env var
2213
+ * (LMNR_FRONTEND_URL) with a cloud default; self-host/local sets it explicitly.
2214
+ */
2215
+ const buildDebuggerUrl = (projectId, sessionId) => {
2216
+ return `${process.env.LMNR_FRONTEND_URL?.trim().replace(/\/+$/, "") || "https://laminar.sh"}/project/${projectId}/debugger-sessions/${sessionId}`;
2217
+ };
2218
+ /**
2219
+ * Open a debug session's debugger page in the browser.
2220
+ *
2221
+ * The target session is the optional `--session-id` flag; when omitted the
2222
+ * session comes from `.lmnr/debug-session.json` (see `resolveSessionId`).
2223
+ * The URL is the file's stored `debugger_url` when it belongs to the resolved
2224
+ * session, else it is rebuilt from the resolved project (`--project-id` or the
2225
+ * linked `.lmnr/project.json`) + LMNR_FRONTEND_URL.
2226
+ *
2227
+ * Local-only handler (registered via `withLocalOpts`, NOT `withProjectClient`):
2228
+ * everything needed lives on disk, so no auth resolution / API call — `open`
2229
+ * works offline and before login.
2230
+ */
2231
+ const handleDebugSessionOpen = async (opts) => {
2232
+ const sessionId = resolveSessionId(opts.sessionId);
2233
+ const file = readDebugSessionFile(resolveDebugSessionDir());
2234
+ let debuggerUrl = file?.session_id === sessionId ? file.debugger_url : null;
2235
+ if (!debuggerUrl) {
2236
+ const projectId = opts.projectId || (await readLocalProjectFile())?.projectId;
2237
+ 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`.");
2238
+ debuggerUrl = buildDebuggerUrl(projectId, sessionId);
2239
+ }
2240
+ if (opts.json) outputJson({
2241
+ sessionId,
2242
+ debuggerUrl
2243
+ });
2244
+ else console.log(debuggerUrl);
2245
+ try {
2246
+ await (0, open.default)(debuggerUrl);
2247
+ } catch (e) {
2248
+ logger$1.warn(`Could not open a browser (${errorMessage(e)}). URL: ${debuggerUrl}`);
2249
+ }
2250
+ };
2251
+ /**
2252
+ * Mint a fresh debug session and reset `.lmnr/debug-session.json` to it.
2253
+ *
2254
+ * The next `LMNR_DEBUG=1 <run>` in this directory reads that file and rejoins the
2255
+ * minted session silently (no browser) — "new session" is owned by this command;
2256
+ * the SDK only mints when no file exists.
2257
+ *
2258
+ * Ordering matters: the local file is written FIRST (so the minted session is
2259
+ * usable for `LMNR_DEBUG=1` continuation even if the network is down / the
2260
+ * backend endpoint isn't deployed yet), THEN the session is best-effort
2261
+ * registered with the backend. A registration failure WARNS but does not fail
2262
+ * the command (exit 0) — the file is already written.
2263
+ *
2264
+ * Pure handler: the command wrapper (`withProjectClient`) resolves a user-token
2265
+ * {@link LaminarClient} (routes `register()` to `POST /v1/cli/rollouts/{id}` with
2266
+ * the resolved project) and owns the error envelope.
2267
+ */
2268
+ const handleDebugSessionNew = async (client, opts) => {
2269
+ const sessionId = (0, node_crypto.randomUUID)();
2270
+ const sessionDir = resolveDebugSessionDir();
2271
+ writeDebugSessionFile({
2272
+ session_id: sessionId,
2273
+ trace_id: null,
2274
+ replay_trace_id: null,
2275
+ cache_until: null,
2276
+ debugger_url: null,
2277
+ started_at: (/* @__PURE__ */ new Date()).toISOString()
2278
+ }, sessionDir);
2279
+ let projectId = null;
2280
+ try {
2281
+ projectId = await client.rolloutSessions.register({ sessionId });
2282
+ } catch (e) {
2283
+ logger$1.warn(`Could not register the debug session with the backend (the local .lmnr/debug-session.json is still usable): ${errorMessage(e)}`);
2284
+ }
2285
+ const debuggerUrl = projectId ? buildDebuggerUrl(projectId, sessionId) : null;
2286
+ if (debuggerUrl) writeDebugSessionFile({
2287
+ session_id: sessionId,
2288
+ trace_id: null,
2289
+ replay_trace_id: null,
2290
+ cache_until: null,
2291
+ debugger_url: debuggerUrl,
2292
+ started_at: (/* @__PURE__ */ new Date()).toISOString()
2293
+ }, sessionDir);
2294
+ if (opts.json) {
2295
+ outputJson({
2296
+ sessionId,
2297
+ projectId,
2298
+ debuggerUrl
2299
+ });
2300
+ return;
2301
+ }
2302
+ if (debuggerUrl) logger$1.info(`New debug session: ${debuggerUrl}`);
2303
+ else logger$1.info(`New debug session: ${sessionId}`);
2304
+ console.log(sessionId);
2305
+ if (opts.browser !== false && debuggerUrl) try {
2306
+ await (0, open.default)(debuggerUrl);
2307
+ } catch {}
2308
+ };
2095
2309
  //#endregion
2096
2310
  //#region src/utils/colors.ts
2097
2311
  function enabledFor(stream) {
@@ -2195,7 +2409,7 @@ function trimSlash$2(url) {
2195
2409
  */
2196
2410
  const handleProjectsList = async (client, opts) => {
2197
2411
  const projects = await client.cli.listProjects();
2198
- const linked = (await readProjectLink())?.projectId;
2412
+ const linked = (await readLocalProjectFile())?.projectId;
2199
2413
  if (opts.json) {
2200
2414
  outputJson(projects.map((p) => ({
2201
2415
  ...p,
@@ -2565,7 +2779,7 @@ async function handleSetup(options) {
2565
2779
  const cwd = process.cwd();
2566
2780
  const existingKey = await findEnvKey(cwd);
2567
2781
  let creds = await safeReadCredentials();
2568
- let link = await readProjectLink();
2782
+ let link = await readLocalProjectFile();
2569
2783
  if (!creds) {
2570
2784
  let login;
2571
2785
  try {
@@ -2742,7 +2956,7 @@ async function resolveProjectViaCli(creds, userBaseUrl, issuer, isJson, options)
2742
2956
  }
2743
2957
  chosen = await promptProjectChoice(projects);
2744
2958
  }
2745
- const linkPath = await writeProjectLink({
2959
+ const linkPath = await writeLocalProjectFile({
2746
2960
  projectId: chosen.id,
2747
2961
  projectName: chosen.name,
2748
2962
  workspaceId: chosen.workspaceId,
@@ -2772,7 +2986,7 @@ async function writeLink(issuer, userBaseUrl, projectId, isJson) {
2772
2986
  }
2773
2987
  } catch {}
2774
2988
  try {
2775
- const linkPath = await writeProjectLink(link);
2989
+ const linkPath = await writeLocalProjectFile(link);
2776
2990
  if (!isJson) process.stderr.write(`${pc.green("✓")} Linked ${linkPath}\n`);
2777
2991
  } catch (err) {
2778
2992
  if (!isJson) process.stderr.write(`${pc.yellow("Warning")}: could not write .lmnr/project.json (${describeError(err)}). CLI commands will need --project-id ${projectId}.\n`);
@@ -2953,6 +3167,10 @@ const NOTE_SEPARATOR = "\n\n";
2953
3167
  * as `existing + "\n\n" + note`. The note may contain markdown /
2954
3168
  * span-reference links.
2955
3169
  *
3170
+ * The target trace is the optional `--trace-id` flag; when omitted the trace
3171
+ * comes from `.lmnr/debug-session.json`'s `trace_id` — the root trace of the
3172
+ * most recent debug run in this directory (see `resolveTraceId`).
3173
+ *
2956
3174
  * Pure handler: the command wrapper (`withProjectClient`) resolves a user-token
2957
3175
  * {@link LaminarClient} (routes to `/v1/cli/*` with the resolved project) and
2958
3176
  * owns the error envelope (`--json` → structured error + exit, else log + exit).
@@ -2967,8 +3185,8 @@ const NOTE_SEPARATOR = "\n\n";
2967
3185
  * the metadata patch endpoint that concatenates within the Postgres UPDATE,
2968
3186
  * which already serializes on the trace row lock).
2969
3187
  */
2970
- const handleTraceAppendNote = async (client, traceId, note, opts) => {
2971
- const id = normalizeTraceId(traceId);
3188
+ const handleTraceAppendNote = async (client, note, opts) => {
3189
+ const id = normalizeTraceId(resolveTraceId(opts.traceId));
2972
3190
  const rows = await client.sql.query("SELECT metadata FROM traces WHERE id = {trace_id:UUID} LIMIT 1", { trace_id: id });
2973
3191
  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
3192
  const existing = readNoteFromMetadata(rows[0].metadata);
@@ -3017,23 +3235,35 @@ Examples:
3017
3235
  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
3236
  await handleSetup(options);
3019
3237
  });
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)").argument("<note>", "Note text (may contain markdown)").action(withProjectClient(handleTraceAppendNote)).addHelpText("after", `
3238
+ 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
3239
  Notes accumulate: each call appends a new paragraph to the trace's existing
3022
3240
  note rather than overwriting it.
3023
3241
 
3242
+ Without --trace-id, the note goes to the trace_id recorded in
3243
+ .lmnr/debug-session.json — the root trace of the most recent LMNR_DEBUG=1 run
3244
+ in this directory.
3245
+
3024
3246
  Examples:
3025
- $ lmnr-cli trace append-note <trace-id> "Reproduced the timeout on the search tool."
3247
+ $ lmnr-cli trace append-note "Reproduced the timeout on the search tool."
3248
+ $ lmnr-cli trace append-note "Reproduced the timeout." --trace-id <trace-id>
3026
3249
  `);
3027
3250
  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
3251
  Learn more about debugging features at https://laminar.sh/docs/platform/debugger
3029
3252
  `).command("session").description("Manage debug sessions").addHelpText("after", `
3030
3253
  Learn more about debugging features at https://laminar.sh/docs/platform/debugger
3031
3254
  `);
3032
- debugSessionCmd.command("set-name").description("Set the display name of a debug session").argument("<session-id>", "Debug session ID").argument("<name>", "Session display name").action(withProjectClient(handleDebugSessionSetName)).addHelpText("after", `
3255
+ 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", `
3256
+ Without --session-id, the name applies to the session recorded in
3257
+ .lmnr/debug-session.json (written by \`debug session new\` / LMNR_DEBUG=1 runs).
3258
+
3033
3259
  Examples:
3034
- $ lmnr-cli debug session set-name <session-id> "Fix report length + search tool"
3260
+ $ lmnr-cli debug session set-name "Fix report length + search tool"
3261
+ $ lmnr-cli debug session set-name "Fix report length" --session-id <session-id>
3035
3262
  `);
3036
- debugSessionCmd.command("summary").description("Print every trace in a debug session with its note, oldest first").argument("<session-id>", "Debug session ID").action(withProjectClient(handleDebugSessionSummary)).addHelpText("after", `
3263
+ 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", `
3264
+ Without --session-id, summarizes the session recorded in
3265
+ .lmnr/debug-session.json (written by \`debug session new\` / LMNR_DEBUG=1 runs).
3266
+
3037
3267
  Output is one block per trace (oldest first), the trace's note followed by a
3038
3268
  self-closing tag carrying the trace id and end time:
3039
3269
 
@@ -3043,8 +3273,31 @@ self-closing tag carrying the trace id and end time:
3043
3273
  With --json, prints an array of {"note", "traceId", "endTime"} objects.
3044
3274
 
3045
3275
  Examples:
3046
- $ lmnr-cli debug session summary <session-id>
3047
- $ lmnr-cli debug session summary <session-id> --json
3276
+ $ lmnr-cli debug session summary
3277
+ $ lmnr-cli debug session summary --session-id <session-id> --json
3278
+ `);
3279
+ 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", `
3280
+ Without --session-id, opens the session recorded in .lmnr/debug-session.json
3281
+ (written by \`debug session new\` / LMNR_DEBUG=1 runs). The URL is also printed
3282
+ to stdout; in --json mode a {"sessionId","debuggerUrl"} object is printed
3283
+ instead. Local-only: no login or network needed.
3284
+
3285
+ Examples:
3286
+ $ lmnr-cli debug session open
3287
+ $ lmnr-cli debug session open --session-id <session-id>
3288
+ `);
3289
+ 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", `
3290
+ Mints a new session id, writes it to .lmnr/debug-session.json (resetting any
3291
+ prior session), and registers it with the backend. The next \`LMNR_DEBUG=1 <run>\`
3292
+ in this directory rejoins this session silently (no browser).
3293
+
3294
+ The bare session id is printed to stdout; in --json mode a
3295
+ {"sessionId","projectId","debuggerUrl"} object is printed instead.
3296
+
3297
+ Examples:
3298
+ $ lmnr-cli debug session new
3299
+ $ lmnr-cli debug session new --json
3300
+ $ lmnr-cli debug session new --no-browser
3048
3301
  `);
3049
3302
  program.addHelpText("after", `
3050
3303
  Authentication:
@@ -3064,9 +3317,11 @@ Examples:
3064
3317
  lmnr-cli dataset pull output.jsonl -n my-dataset --json # Pull data from a dataset
3065
3318
  lmnr-cli sql query "SELECT * FROM spans LIMIT 10" --json # Query spans
3066
3319
  lmnr-cli sql schema # Show available tables
3067
- lmnr-cli trace append-note <trace-id> "note text" # Append a note to a trace
3068
- lmnr-cli debug session set-name <session-id> "title" # Rename a debug session
3069
- lmnr-cli debug session summary <session-id> # Notes for each trace in a session
3320
+ lmnr-cli trace append-note "note text" # Note on the latest debug trace
3321
+ lmnr-cli debug session new # Mint a fresh debug session
3322
+ lmnr-cli debug session open # Open the session in the browser
3323
+ lmnr-cli debug session set-name "title" # Rename the current debug session
3324
+ lmnr-cli debug session summary # Notes for each trace in the session
3070
3325
 
3071
3326
  For more information about the Laminar platfrom:
3072
3327
  Documentation: https://laminar.sh/docs