From e068b5f879b305fd165484e17b0d9c486d6f8eb7 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 17 May 2026 15:10:54 +0000 Subject: [PATCH 1/4] Add AGENTS.md with Cursor Cloud development instructions Co-authored-by: Mohamed --- AGENTS.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..d57b69d --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,30 @@ +# AGENTS.md + +## Cursor Cloud specific instructions + +### Overview + +This is `langfuse-cli` — a CLI tool for interacting with the Langfuse API. It wraps the [`specli`](https://www.npmjs.com/package/specli) library which auto-generates CLI commands from a bundled OpenAPI specification (`openapi.yml`). + +### Prerequisites + +- **Bun** — required for build scripts (`bun run build`, `bun run patch-openapi`). +- **Node.js >= 20** — runtime for the built CLI (`bin/langfuse.mjs`). + +### Key commands + +| Task | Command | +|---|---| +| Install deps | `bun install` | +| Build | `bun run build` (fetches latest OpenAPI spec + bundles `src/cli.ts` → `dist/cli.js`) | +| Run CLI | `node bin/langfuse.mjs` | +| Patch OpenAPI only | `bun run patch-openapi` | +| Refetch OpenAPI | `bun run refetch-openapi` | + +### Development notes + +- There are no automated tests, linter, or formatter configured in this project. +- The build step (`bun run build`) fetches the OpenAPI spec from `https://cloud.langfuse.com/generated/api/openapi.yml` — it requires network access. +- To test the CLI against the Langfuse API you need `LANGFUSE_PUBLIC_KEY` and `LANGFUSE_SECRET_KEY` env vars (or pass `--public-key` / `--secret-key` flags). See `.env.default` for local dev defaults. +- The `--curl` flag on any API command previews the curl command without executing it, useful for verifying command construction without credentials. +- The project has no lockfile committed; `bun install` generates `bun.lock` locally. From 3e72def228338a8b07e995db84df8dd4d31bf0a3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 19 May 2026 02:48:25 +0000 Subject: [PATCH 2/4] Add Langfuse tracing instrumentation to CLI Agent-Logs-Url: https://github.com/garlobrian52/langfuse-cli/sessions/d0f021b5-77e7-46ef-ab91-d541c59eebcd Co-authored-by: garlobrian52 <134189886+garlobrian52@users.noreply.github.com> --- README.md | 12 ++++ package.json | 7 ++ src/cli.ts | 187 +++++++++++++++++++++++++++++++++++++++++++++------ 3 files changed, 185 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index ec03a91..e2e3b0e 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,12 @@ langfuse api score-v2s get-scores --limit 20 ## Agent Usage +Install the official Langfuse AI skill: + +```sh +npx skills add langfuse/skills --skill "langfuse" +``` + A skill file is included for teaching AI agents how to use the CLI. Print it with: ```sh @@ -85,6 +91,12 @@ langfuse get-skill Pipe it into an agent's context or include it in a system prompt. +## Tracing + +This CLI is instrumented with Langfuse tracing when credentials are configured (`LANGFUSE_PUBLIC_KEY`, `LANGFUSE_SECRET_KEY`, and optional `LANGFUSE_HOST`). + +It creates traces for each command and nested spans for API spec loading/help/execution, while only recording safe command metadata (not secret flags). + ## API Reference See the full [Langfuse API Reference](https://api.reference.langfuse.com/). diff --git a/package.json b/package.json index 215c920..0281500 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,13 @@ "prepublishOnly": "rm -rf dist && bun run build" }, "dependencies": { + "@langfuse/otel": "^5.3.0", + "@langfuse/tracing": "^5.3.0", + "@opentelemetry/api": "^1.9.1", + "@opentelemetry/core": "^2.7.1", + "@opentelemetry/exporter-trace-otlp-http": "^0.218.0", + "@opentelemetry/sdk-trace-base": "^2.7.1", + "@opentelemetry/sdk-trace-node": "^2.7.1", "specli": "^0.0.39" }, "engines": { diff --git a/src/cli.ts b/src/cli.ts index 3b6b677..635a1bb 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -13,6 +13,11 @@ const LANGFUSE_FLAGS = new Set([ ]); const LANGFUSE_BOOL_FLAGS = new Set(["--refetch-api-spec"]); +type TracingRuntime = { + startActiveObservation: typeof import("@langfuse/tracing").startActiveObservation; + updateActiveObservation: typeof import("@langfuse/tracing").updateActiveObservation; +}; + function loadEnvFile(filePath: string): void { const content = readFileSync(filePath, "utf-8"); for (const line of content.split("\n")) { @@ -64,6 +69,99 @@ async function getSpecText(params: { return readFileSync(specPath, "utf-8"); } +function getErrorMessage(error: unknown): string { + if (error instanceof Error) return error.message; + return String(error); +} + +async function runTracedStep(params: { + tracing: TracingRuntime | null; + name: string; + input?: unknown; + fn: () => Promise; +}): Promise { + const { tracing, name, input, fn } = params; + if (!tracing) return fn(); + + return tracing.startActiveObservation(name, async () => { + if (input !== undefined) { + tracing.updateActiveObservation({ input }); + } + try { + const result = await fn(); + tracing.updateActiveObservation({ output: { status: "ok" } }); + return result; + } catch (error) { + tracing.updateActiveObservation({ + level: "ERROR", + statusMessage: getErrorMessage(error), + output: { status: "error" }, + }); + throw error; + } + }); +} + +async function withLangfuseTracing(params: { + publicKey: string | undefined; + secretKey: string | undefined; + host: string; + command: string; + fn: (tracing: TracingRuntime | null) => Promise; +}): Promise { + const { publicKey, secretKey, host, command, fn } = params; + if (!publicKey || !secretKey) { + return fn(null); + } + + const [{ LangfuseSpanProcessor }, tracingModule, { NodeTracerProvider }] = + await Promise.all([ + import("@langfuse/otel"), + import("@langfuse/tracing"), + import("@opentelemetry/sdk-trace-node"), + ]); + + const provider = new NodeTracerProvider({ + spanProcessors: [ + new LangfuseSpanProcessor({ + publicKey, + secretKey, + baseUrl: host, + }), + ], + }); + provider.register(); + tracingModule.setLangfuseTracerProvider(provider); + + const tracing: TracingRuntime = { + startActiveObservation: tracingModule.startActiveObservation, + updateActiveObservation: tracingModule.updateActiveObservation, + }; + + try { + return await tracingModule.propagateAttributes( + { + traceName: `langfuse-cli.${command}`, + metadata: { + command, + runtime: "langfuse-cli", + }, + tags: ["langfuse-cli", "cli"], + }, + () => + runTracedStep({ + tracing, + name: "langfuse-cli.command", + input: { command }, + fn: () => fn(tracing), + }), + ); + } finally { + await provider.shutdown(); + tracingModule.setLangfuseTracerProvider(null); + } +} + export async function run(argv: string[]): Promise { const extracted: Record = {}; const boolFlags: Record = {}; @@ -102,20 +200,36 @@ export async function run(argv: string[]): Promise { // First positional arg determines the subcommand const subcommand = passthrough[2]; + const command = subcommand ?? "help"; - if (subcommand === "api") { - passthrough.splice(2, 1); - return runApi({ passthrough, boolFlags, publicKey, secretKey, host }); - } + await withLangfuseTracing({ + publicKey, + secretKey, + host, + command, + fn: async (tracing) => { + if (subcommand === "api") { + passthrough.splice(2, 1); + return runApi({ + passthrough, + boolFlags, + publicKey, + secretKey, + host, + tracing, + }); + } - if (subcommand === "get-skill") { - const skillPath = join(__dirname, "..", "skill", "langfuse-cli.md"); - process.stdout.write(readFileSync(skillPath, "utf-8")); - return; - } + if (subcommand === "get-skill") { + const skillPath = join(__dirname, "..", "skill", "langfuse-cli.md"); + process.stdout.write(readFileSync(skillPath, "utf-8")); + return; + } - // Show help for anything else (no args, --help, -h, unknown command) - printHelp(); + // Show help for anything else (no args, --help, -h, unknown command) + printHelp(); + }, + }); } function printHelp(): void { @@ -196,12 +310,23 @@ async function runApi(params: { publicKey: string | undefined; secretKey: string | undefined; host: string; + tracing: TracingRuntime | null; }): Promise { - const { passthrough, boolFlags, publicKey, secretKey, host } = params; + const { passthrough, boolFlags, publicKey, secretKey, host, tracing } = params; + const apiResource = passthrough[2] ?? "__unknown__"; + const apiAction = passthrough[3] ?? "__help__"; - const specText = await getSpecText({ - refetch: boolFlags["refetch-api-spec"] ?? false, - host, + const specText = await runTracedStep({ + tracing, + name: "langfuse-cli.api.load-spec", + input: { + refetchApiSpec: boolFlags["refetch-api-spec"] ?? false, + }, + fn: () => + getSpecText({ + refetch: boolFlags["refetch-api-spec"] ?? false, + host, + }), }); // Intercept help: no args, --help, or -h @@ -210,7 +335,17 @@ async function runApi(params: { args.length === 0 || (args.length === 1 && (args[0] === "--help" || args[0] === "-h")) ) { - printApiHelp(await getResources(specText)); + await runTracedStep({ + tracing, + name: "langfuse-cli.api.help", + input: { + resource: apiResource, + action: apiAction, + }, + fn: async () => { + printApiHelp(await getResources(specText)); + }, + }); return; } @@ -220,10 +355,20 @@ async function runApi(params: { if (secretKey) inject.push("--password", secretKey); specliArgv.splice(2, 0, ...inject); - const main = await loadMain(); - await main(specliArgv, { - cliName: "langfuse api", - auth: "BasicAuth", - embeddedSpecText: specText, + await runTracedStep({ + tracing, + name: "langfuse-cli.api.execute", + input: { + resource: apiResource, + action: apiAction, + }, + fn: async () => { + const main = await loadMain(); + await main(specliArgv, { + cliName: "langfuse api", + auth: "BasicAuth", + embeddedSpecText: specText, + }); + }, }); } From 21690109450d02e54209d78c4ebdd95d87bdd923 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 19 May 2026 02:53:13 +0000 Subject: [PATCH 3/4] Refine tracing spans and docs for Langfuse best practices Agent-Logs-Url: https://github.com/garlobrian52/langfuse-cli/sessions/d0f021b5-77e7-46ef-ab91-d541c59eebcd Co-authored-by: garlobrian52 <134189886+garlobrian52@users.noreply.github.com> --- package.json | 2 -- src/cli.ts | 28 ++++++++++++++++++++-------- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 0281500..ed41f2f 100644 --- a/package.json +++ b/package.json @@ -37,9 +37,7 @@ "@langfuse/otel": "^5.3.0", "@langfuse/tracing": "^5.3.0", "@opentelemetry/api": "^1.9.1", - "@opentelemetry/core": "^2.7.1", "@opentelemetry/exporter-trace-otlp-http": "^0.218.0", - "@opentelemetry/sdk-trace-base": "^2.7.1", "@opentelemetry/sdk-trace-node": "^2.7.1", "specli": "^0.0.39" }, diff --git a/src/cli.ts b/src/cli.ts index 635a1bb..8ded6d7 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -84,18 +84,27 @@ async function runTracedStep(params: { if (!tracing) return fn(); return tracing.startActiveObservation(name, async () => { + const startedAt = Date.now(); if (input !== undefined) { tracing.updateActiveObservation({ input }); } try { const result = await fn(); - tracing.updateActiveObservation({ output: { status: "ok" } }); + tracing.updateActiveObservation({ + output: { + status: "ok", + durationMs: Date.now() - startedAt, + }, + }); return result; } catch (error) { tracing.updateActiveObservation({ level: "ERROR", statusMessage: getErrorMessage(error), - output: { status: "error" }, + output: { + status: "error", + durationMs: Date.now() - startedAt, + }, }); throw error; } @@ -157,8 +166,11 @@ async function withLangfuseTracing(params: { }), ); } finally { - await provider.shutdown(); - tracingModule.setLangfuseTracerProvider(null); + try { + await provider.shutdown(); + } finally { + tracingModule.setLangfuseTracerProvider(null); + } } } @@ -200,13 +212,13 @@ export async function run(argv: string[]): Promise { // First positional arg determines the subcommand const subcommand = passthrough[2]; - const command = subcommand ?? "help"; + const commandForTracing = subcommand ?? "help"; await withLangfuseTracing({ publicKey, secretKey, host, - command, + command: commandForTracing, fn: async (tracing) => { if (subcommand === "api") { passthrough.splice(2, 1); @@ -313,8 +325,8 @@ async function runApi(params: { tracing: TracingRuntime | null; }): Promise { const { passthrough, boolFlags, publicKey, secretKey, host, tracing } = params; - const apiResource = passthrough[2] ?? "__unknown__"; - const apiAction = passthrough[3] ?? "__help__"; + const apiResource = passthrough[2] ?? "unknown-resource"; + const apiAction = passthrough[3] ?? "help-action"; const specText = await runTracedStep({ tracing, From d706bc6bb7c1f4c737d212e4ca84e466268c4b18 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 19 May 2026 15:55:49 +0000 Subject: [PATCH 4/4] fix: add langfuse-cli bin alias Agent-Logs-Url: https://github.com/garlobrian52/langfuse-cli/sessions/58f445e8-a3d8-4a68-8d80-f576f756aef0 Co-authored-by: garlobrian52 <134189886+garlobrian52@users.noreply.github.com> --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index ed41f2f..e8246d1 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,8 @@ ], "type": "module", "bin": { - "langfuse": "./bin/langfuse.mjs" + "langfuse": "./bin/langfuse.mjs", + "langfuse-cli": "./bin/langfuse.mjs" }, "files": [ "bin",