lmnr-cli 0.1.11 → 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 +13 -4
- package/dist/index.cjs +288 -33
- package/dist/index.cjs.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -97,7 +97,7 @@ is the data API and carries **no port** — pass the port separately with `--por
|
|
|
97
97
|
lmnr-cli sql schema --base-url http://localhost --port 8000
|
|
98
98
|
```
|
|
99
99
|
|
|
100
|
-
`LMNR_FRONTEND_URL` (default `https://
|
|
100
|
+
`LMNR_FRONTEND_URL` (default `https://laminar.sh`), `LMNR_BASE_URL`
|
|
101
101
|
(default `https://api.lmnr.ai`), and `LMNR_HTTP_PORT` (default `443`) are also
|
|
102
102
|
honored, and are auto-loaded from a `.env` / `.env.local` in the working directory.
|
|
103
103
|
|
|
@@ -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
|
@@ -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.
|
|
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.
|
|
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}`;
|
|
@@ -1207,22 +1213,22 @@ function outputJsonError(error, exitCode = 1) {
|
|
|
1207
1213
|
}
|
|
1208
1214
|
//#endregion
|
|
1209
1215
|
//#region src/constants.ts
|
|
1210
|
-
const DEFAULT_FRONTEND_URL$1 = "https://
|
|
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
|
|
@@ -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,
|
|
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,
|
|
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
|
|
2412
|
+
const linked = (await readLocalProjectFile())?.projectId;
|
|
2199
2413
|
if (opts.json) {
|
|
2200
2414
|
outputJson(projects.map((p) => ({
|
|
2201
2415
|
...p,
|
|
@@ -2531,7 +2745,7 @@ function describeError$1(err) {
|
|
|
2531
2745
|
}
|
|
2532
2746
|
//#endregion
|
|
2533
2747
|
//#region src/commands/setup/index.ts
|
|
2534
|
-
const DEFAULT_FRONTEND_URL = "https://
|
|
2748
|
+
const DEFAULT_FRONTEND_URL = "https://laminar.sh";
|
|
2535
2749
|
const DEFAULT_BASE_URL = "https://api.lmnr.ai";
|
|
2536
2750
|
const EXIT_NO_ACCESS = 4;
|
|
2537
2751
|
const EXIT_LOGIN_FAILED = 6;
|
|
@@ -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
|
|
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
|
|
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
|
|
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,
|
|
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);
|
|
@@ -3005,7 +3223,7 @@ Examples:
|
|
|
3005
3223
|
process.stdout.write(SQL_SCHEMA_HELP);
|
|
3006
3224
|
});
|
|
3007
3225
|
program.command("project").description("Work with Laminar projects").command("list").description("List the projects you can access (● = linked to this directory)").option("--base-url <url>", "Base URL for the Laminar API. Defaults to the logged-in session or LMNR_BASE_URL").option("--port <port>", "Port for the Laminar API. Defaults to 443", (val) => parseInt(val, 10)).option("--json", "Output structured JSON to stdout").action(withUserToken(handleProjectsList));
|
|
3008
|
-
program.command("login").description("Authenticate the CLI via OAuth Device Flow").option("--frontend-url <url>", "Frontend URL (issuer). Defaults to https://
|
|
3226
|
+
program.command("login").description("Authenticate the CLI via OAuth Device Flow").option("--frontend-url <url>", "Frontend URL (issuer). Defaults to https://laminar.sh or LMNR_FRONTEND_URL env variable").option("--no-browser", "Do not open the verification URL in a browser").action(async (options) => {
|
|
3009
3227
|
const result = await handleLogin(options);
|
|
3010
3228
|
process.stderr.write(`${pc.green("✓")} Logged in as ${result.userEmail ?? "<unknown>"}.\n`);
|
|
3011
3229
|
process.stderr.write(pc.dim("Client: lmnr-cli. Tokens stored at ~/.config/lmnr/credentials.json (mode 0600).\n"));
|
|
@@ -3014,26 +3232,38 @@ Examples:
|
|
|
3014
3232
|
program.command("logout").description("Log out and remove the stored credentials").action(async () => {
|
|
3015
3233
|
await handleLogout();
|
|
3016
3234
|
});
|
|
3017
|
-
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://
|
|
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)
|
|
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
|
|
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("<
|
|
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
|
|
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").
|
|
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
|
|
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
|
|
3068
|
-
lmnr-cli debug session
|
|
3069
|
-
lmnr-cli debug 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
|