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 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
@@ -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.12";
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.29";
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-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
@@ -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
- const minColWidth = 5;
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, sessionId, name, opts) => {
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, sessionId, opts) => {
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 readProjectLink())?.projectId;
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 readProjectLink();
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 writeProjectLink({
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 writeProjectLink(link);
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, traceId, note, opts) => {
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)").argument("<note>", "Note text (may contain markdown)").action(withProjectClient(handleTraceAppendNote)).addHelpText("after", `
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 <trace-id> "Reproduced the timeout on the search tool."
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("<session-id>", "Debug session ID").argument("<name>", "Session display name").action(withProjectClient(handleDebugSessionSetName)).addHelpText("after", `
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 <session-id> "Fix report length + search tool"
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").argument("<session-id>", "Debug session ID").action(withProjectClient(handleDebugSessionSummary)).addHelpText("after", `
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 <session-id>
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 <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
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