From c22d34f4ce04c8560fe22a0512b4fa66750dc292 Mon Sep 17 00:00:00 2001 From: Hari Harish Date: Wed, 15 Jul 2026 15:35:26 -0700 Subject: [PATCH 1/2] fix: surfaces outdated node version error --- README.md | 14 +++++++++ bundle/guard.js | 38 ++++++++++++++++++++++ esbuild.config.mjs | 1 + hooks.json | 16 +++++----- hooks/hooks.json | 16 +++++----- src/hooks/guard.ts | 56 +++++++++++++++++++++++++++++++++ src/utils/node-version.ts | 19 +++++++++++ test/utils/node-version.test.ts | 29 +++++++++++++++++ 8 files changed, 173 insertions(+), 16 deletions(-) create mode 100755 bundle/guard.js create mode 100644 src/hooks/guard.ts create mode 100644 src/utils/node-version.ts create mode 100644 test/utils/node-version.test.ts diff --git a/README.md b/README.md index 5dbbe64..feb10db 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,20 @@ Every run carries the shared [`coding-agent-v1`](https://github.com/langchain-ai `user_id`, `sandbox_type`, and `approval_policy` are omitted — Cursor's hooks expose no stable source for them. +## Troubleshooting + +**Nothing shows up in LangSmith / `turn_count` stays 0.** Cursor launches hooks from a GUI context, so the `node` it runs is *not* your shell's version-managed node (nvm/mise/asdf) — it's whatever is on the system `PATH`, which is often an older node (or none). The hooks need **Node ≥ 22.13** (for `node:sqlite`). + +The hooks run through a small version guard that fails loudly instead of silently. If your node is too old, you'll see a line in `~/.cursor/langsmith-hook.log` (and hook stderr) like: + +``` +[langsmith] Node 20.11.0 at /usr/local/bin/node is too old for tracing (need >= 22.13 for node:sqlite). This turn was NOT traced. ... +``` + +The path in that message is the exact node Cursor used. To fix, make a Node ≥ 22.13 available on the system `PATH` (note: upgrading your *shell's* nvm/mise node does **not** help a Dock/Finder-launched Cursor — that node isn't on the GUI `PATH`). Alternatives: install node ≥ 22.13 in a GUI-visible location (e.g. Homebrew on Intel symlinks into `/usr/local/bin`), or launch Cursor from a terminal (`cursor .`) so it inherits your shell environment. + +Tail the log to confirm activity: `tail -f ~/.cursor/langsmith-hook.log`. + ## Known limitations - **Subagent token usage** is not available — Cursor exposes no per-subagent usage breakdown via hooks or its local DB, so a subagent's `Task` run carries its tool calls but no token counts. diff --git a/bundle/guard.js b/bundle/guard.js new file mode 100755 index 0000000..a2d0c84 --- /dev/null +++ b/bundle/guard.js @@ -0,0 +1,38 @@ +#!/usr/bin/env node + +// dist/hooks/guard.js +import { appendFileSync } from "node:fs"; + +// dist/utils/node-version.js +var MIN_NODE = [22, 13]; +function nodeTooOld(version, min = MIN_NODE) { + const parts = version.split("."); + const major = Number.parseInt(parts[0] ?? "", 10); + const minor = Number.parseInt(parts[1] ?? "", 10); + if (!Number.isFinite(major)) + return false; + if (major !== min[0]) + return major < min[0]; + return (Number.isFinite(minor) ? minor : 0) < min[1]; +} + +// dist/hooks/guard.js +var hookName = process.argv[2]; +if (nodeTooOld(process.versions.node)) { + const msg = `[langsmith] Node ${process.versions.node} at ${process.execPath} is too old for tracing (need >= ${MIN_NODE[0]}.${MIN_NODE[1]} for node:sqlite). This turn was NOT traced. Cursor runs this node, not your shell's \u2014 install Node >= ${MIN_NODE[0]}.${MIN_NODE[1]} on the system PATH, or launch Cursor from a terminal. See README troubleshooting.`; + const logFile = process.env.LANGSMITH_CURSOR_LOG_FILE ?? `${process.env.HOME ?? ""}/.cursor/langsmith-hook.log`; + try { + appendFileSync(logFile, msg + "\n"); + } catch { + } + console.error(msg); + process.exit(0); +} +if (!hookName) { + console.error("[langsmith] guard: missing hook name argument"); + process.exit(0); +} +await import(new URL(`./${hookName}.js`, import.meta.url).href).catch((err) => { + console.error(`[langsmith] hook ${hookName} failed:`, err); + process.exit(0); +}); diff --git a/esbuild.config.mjs b/esbuild.config.mjs index 36782dd..1117537 100644 --- a/esbuild.config.mjs +++ b/esbuild.config.mjs @@ -14,6 +14,7 @@ const entryPoints = [ "dist/hooks/subagent-stop.js", "dist/hooks/stop.js", "dist/hooks/session-start.js", + "dist/hooks/guard.js", ]; await build({ diff --git a/hooks.json b/hooks.json index 6ffe812..080bb56 100644 --- a/hooks.json +++ b/hooks.json @@ -1,13 +1,13 @@ { "version": 1, "hooks": { - "beforeSubmitPrompt": [{ "command": "node BUNDLE_DIR/before-submit-prompt.js" }], - "afterAgentResponse": [{ "command": "node BUNDLE_DIR/after-agent-response.js" }], - "postToolUse": [{ "command": "node BUNDLE_DIR/post-tool-use.js" }], - "postToolUseFailure": [{ "command": "node BUNDLE_DIR/post-tool-use-failure.js" }], - "subagentStart": [{ "command": "node BUNDLE_DIR/subagent-start.js" }], - "subagentStop": [{ "command": "node BUNDLE_DIR/subagent-stop.js" }], - "stop": [{ "command": "node BUNDLE_DIR/stop.js" }], - "sessionStart": [{ "command": "node BUNDLE_DIR/session-start.js" }] + "beforeSubmitPrompt": [{ "command": "node BUNDLE_DIR/guard.js before-submit-prompt" }], + "afterAgentResponse": [{ "command": "node BUNDLE_DIR/guard.js after-agent-response" }], + "postToolUse": [{ "command": "node BUNDLE_DIR/guard.js post-tool-use" }], + "postToolUseFailure": [{ "command": "node BUNDLE_DIR/guard.js post-tool-use-failure" }], + "subagentStart": [{ "command": "node BUNDLE_DIR/guard.js subagent-start" }], + "subagentStop": [{ "command": "node BUNDLE_DIR/guard.js subagent-stop" }], + "stop": [{ "command": "node BUNDLE_DIR/guard.js stop" }], + "sessionStart": [{ "command": "node BUNDLE_DIR/guard.js session-start" }] } } diff --git a/hooks/hooks.json b/hooks/hooks.json index 802df6a..2811cc9 100644 --- a/hooks/hooks.json +++ b/hooks/hooks.json @@ -1,13 +1,13 @@ { "version": 1, "hooks": { - "beforeSubmitPrompt": [{ "command": "node ./bundle/before-submit-prompt.js" }], - "afterAgentResponse": [{ "command": "node ./bundle/after-agent-response.js" }], - "postToolUse": [{ "command": "node ./bundle/post-tool-use.js" }], - "postToolUseFailure": [{ "command": "node ./bundle/post-tool-use-failure.js" }], - "subagentStart": [{ "command": "node ./bundle/subagent-start.js" }], - "subagentStop": [{ "command": "node ./bundle/subagent-stop.js" }], - "stop": [{ "command": "node ./bundle/stop.js" }], - "sessionStart": [{ "command": "node ./bundle/session-start.js" }] + "beforeSubmitPrompt": [{ "command": "node ./bundle/guard.js before-submit-prompt" }], + "afterAgentResponse": [{ "command": "node ./bundle/guard.js after-agent-response" }], + "postToolUse": [{ "command": "node ./bundle/guard.js post-tool-use" }], + "postToolUseFailure": [{ "command": "node ./bundle/guard.js post-tool-use-failure" }], + "subagentStart": [{ "command": "node ./bundle/guard.js subagent-start" }], + "subagentStop": [{ "command": "node ./bundle/guard.js subagent-stop" }], + "stop": [{ "command": "node ./bundle/guard.js stop" }], + "sessionStart": [{ "command": "node ./bundle/guard.js session-start" }] } } diff --git a/src/hooks/guard.ts b/src/hooks/guard.ts new file mode 100644 index 0000000..8848a10 --- /dev/null +++ b/src/hooks/guard.ts @@ -0,0 +1,56 @@ +#!/usr/bin/env node +/** + * Version guard for the bundled hooks. + * + * Cursor launches hooks from a GUI context, where the `node` on PATH is often + * NOT the developer's version-managed node (nvm/mise/asdf): it can be an old + * system/Homebrew node, or missing entirely. The real hooks import + * `node:sqlite`, which needs Node >= 22.13; on older node that import throws + * ERR_UNKNOWN_BUILTIN_MODULE at module-load — *before* any of our code runs — + * so the failure is silent (no log line, no trace, turn_count stuck at 0). + * + * This guard imports nothing that touches node:sqlite. It checks the running + * Node version first, writes a clear message if it's too old, and only then + * dynamically imports the real hook — deferring the sqlite load until the + * check has passed. (A dynamic import runs after this module's own code; a + * static import would be hoisted and crash before the check.) + * + * Invoked as: node ./bundle/guard.js + */ +import { appendFileSync } from "node:fs"; +import { MIN_NODE, nodeTooOld } from "../utils/node-version.js"; + +const hookName = process.argv[2]; + +if (nodeTooOld(process.versions.node)) { + const msg = + `[langsmith] Node ${process.versions.node} at ${process.execPath} is too old for tracing ` + + `(need >= ${MIN_NODE[0]}.${MIN_NODE[1]} for node:sqlite). This turn was NOT traced. ` + + `Cursor runs this node, not your shell's — install Node >= ${MIN_NODE[0]}.${MIN_NODE[1]} on the ` + + `system PATH, or launch Cursor from a terminal. See README troubleshooting.`; + const logFile = + process.env.LANGSMITH_CURSOR_LOG_FILE ?? + `${process.env.HOME ?? ""}/.cursor/langsmith-hook.log`; + try { + appendFileSync(logFile, msg + "\n"); + } catch { + // best effort — logging must not itself throw + } + console.error(msg); + // Exit 0: a non-zero exit would make Cursor surface a hook failure every turn. + process.exit(0); +} + +if (!hookName) { + console.error("[langsmith] guard: missing hook name argument"); + process.exit(0); +} + +// Defer loading the real (sqlite-importing) hook until the version check passes. +// Resolve relative to this file so it works regardless of cwd. +await import(new URL(`./${hookName}.js`, import.meta.url).href).catch( + (err: unknown) => { + console.error(`[langsmith] hook ${hookName} failed:`, err); + process.exit(0); + }, +); diff --git a/src/utils/node-version.ts b/src/utils/node-version.ts new file mode 100644 index 0000000..2c407d4 --- /dev/null +++ b/src/utils/node-version.ts @@ -0,0 +1,19 @@ +/** Minimum Node that provides a usable built-in `node:sqlite` (DatabaseSync). */ +export const MIN_NODE: readonly [number, number] = [22, 13]; + +/** + * True if `version` (e.g. `process.versions.node` → "22.12.1") is older than + * `min` = [major, minor]. Pre-release / non-numeric suffixes are tolerated; an + * unparseable major returns false (don't block when we can't tell). + */ +export function nodeTooOld( + version: string, + min: readonly [number, number] = MIN_NODE, +): boolean { + const parts = version.split("."); + const major = Number.parseInt(parts[0] ?? "", 10); + const minor = Number.parseInt(parts[1] ?? "", 10); + if (!Number.isFinite(major)) return false; + if (major !== min[0]) return major < min[0]; + return (Number.isFinite(minor) ? minor : 0) < min[1]; +} diff --git a/test/utils/node-version.test.ts b/test/utils/node-version.test.ts new file mode 100644 index 0000000..d5c83db --- /dev/null +++ b/test/utils/node-version.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from "vitest"; +import { MIN_NODE, nodeTooOld } from "../../src/utils/node-version.js"; + +describe("nodeTooOld", () => { + it("flags versions below the minimum", () => { + expect(nodeTooOld("20.11.0")).toBe(true); + expect(nodeTooOld("22.12.0")).toBe(true); + expect(nodeTooOld("22.12.99")).toBe(true); + expect(nodeTooOld("18.0.0")).toBe(true); + }); + + it("accepts the minimum and above", () => { + expect(nodeTooOld(`${MIN_NODE[0]}.${MIN_NODE[1]}.0`)).toBe(false); + expect(nodeTooOld("22.13.1")).toBe(false); + expect(nodeTooOld("22.22.3")).toBe(false); + expect(nodeTooOld("24.0.0")).toBe(false); + }); + + it("tolerates pre-release / partial strings", () => { + expect(nodeTooOld("23.0.0-nightly")).toBe(false); + expect(nodeTooOld("22")).toBe(true); // 22.0 < 22.13 + expect(nodeTooOld("not-a-version")).toBe(false); // unparseable → don't block + }); + + it("honors a custom minimum", () => { + expect(nodeTooOld("20.0.0", [18, 0])).toBe(false); + expect(nodeTooOld("16.5.0", [18, 0])).toBe(true); + }); +}); From 06d6fce298245f35a02e475d0fa5e2354aef1a10 Mon Sep 17 00:00:00 2001 From: Hari Harish Date: Wed, 15 Jul 2026 18:19:07 -0700 Subject: [PATCH 2/2] release: 0.3.2 Bump version and rebuild bundle. Ships the Node version guard that surfaces outdated-node errors instead of failing silently. Also realigns .cursor-plugin/plugin.json (was 0.3.0) with package.json. Co-Authored-By: Claude Opus 4.8 (1M context) --- .cursor-plugin/plugin.json | 2 +- bundle/after-agent-response.js | 2 +- bundle/before-submit-prompt.js | 2 +- bundle/post-tool-use-failure.js | 2 +- bundle/post-tool-use.js | 2 +- bundle/session-start.js | 2 +- bundle/stop.js | 2 +- bundle/subagent-start.js | 2 +- bundle/subagent-stop.js | 2 +- package.json | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json index 8834787..2dccb77 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "langsmith-tracing", "displayName": "LangSmith Tracing for Cursor", - "version": "0.3.0", + "version": "0.3.2", "description": "Traces Cursor agent turns — prompts, model responses, tool calls, token usage, and subagents — to LangSmith, grouped into threads per conversation.", "license": "MIT", "keywords": ["langsmith", "langchain", "tracing", "observability", "cursor"], diff --git a/bundle/after-agent-response.js b/bundle/after-agent-response.js index cc5a24d..553c5ee 100755 --- a/bundle/after-agent-response.js +++ b/bundle/after-agent-response.js @@ -64,7 +64,7 @@ function debug(message) { var DEFAULT_PROJECT = "cursor"; // dist/config.js -var LS_INTEGRATION_VERSION = true ? "0.3.1" : process.env.LANGSMITH_CURSOR_INTEGRATION_VERSION || void 0; +var LS_INTEGRATION_VERSION = true ? "0.3.2" : process.env.LANGSMITH_CURSOR_INTEGRATION_VERSION || void 0; var PROVIDER_HOSTS = { github: "github.com", gitlab: "gitlab.com", diff --git a/bundle/before-submit-prompt.js b/bundle/before-submit-prompt.js index 9898043..a9efe65 100755 --- a/bundle/before-submit-prompt.js +++ b/bundle/before-submit-prompt.js @@ -64,7 +64,7 @@ function debug(message) { var DEFAULT_PROJECT = "cursor"; // dist/config.js -var LS_INTEGRATION_VERSION = true ? "0.3.1" : process.env.LANGSMITH_CURSOR_INTEGRATION_VERSION || void 0; +var LS_INTEGRATION_VERSION = true ? "0.3.2" : process.env.LANGSMITH_CURSOR_INTEGRATION_VERSION || void 0; var PROVIDER_HOSTS = { github: "github.com", gitlab: "gitlab.com", diff --git a/bundle/post-tool-use-failure.js b/bundle/post-tool-use-failure.js index ee5f732..7fb9503 100755 --- a/bundle/post-tool-use-failure.js +++ b/bundle/post-tool-use-failure.js @@ -64,7 +64,7 @@ function debug(message) { var DEFAULT_PROJECT = "cursor"; // dist/config.js -var LS_INTEGRATION_VERSION = true ? "0.3.1" : process.env.LANGSMITH_CURSOR_INTEGRATION_VERSION || void 0; +var LS_INTEGRATION_VERSION = true ? "0.3.2" : process.env.LANGSMITH_CURSOR_INTEGRATION_VERSION || void 0; var PROVIDER_HOSTS = { github: "github.com", gitlab: "gitlab.com", diff --git a/bundle/post-tool-use.js b/bundle/post-tool-use.js index 4fcb44f..615f822 100755 --- a/bundle/post-tool-use.js +++ b/bundle/post-tool-use.js @@ -64,7 +64,7 @@ function debug(message) { var DEFAULT_PROJECT = "cursor"; // dist/config.js -var LS_INTEGRATION_VERSION = true ? "0.3.1" : process.env.LANGSMITH_CURSOR_INTEGRATION_VERSION || void 0; +var LS_INTEGRATION_VERSION = true ? "0.3.2" : process.env.LANGSMITH_CURSOR_INTEGRATION_VERSION || void 0; var PROVIDER_HOSTS = { github: "github.com", gitlab: "gitlab.com", diff --git a/bundle/session-start.js b/bundle/session-start.js index bfa02bf..98e577d 100755 --- a/bundle/session-start.js +++ b/bundle/session-start.js @@ -64,7 +64,7 @@ function debug(message) { var DEFAULT_PROJECT = "cursor"; // dist/config.js -var LS_INTEGRATION_VERSION = true ? "0.3.1" : process.env.LANGSMITH_CURSOR_INTEGRATION_VERSION || void 0; +var LS_INTEGRATION_VERSION = true ? "0.3.2" : process.env.LANGSMITH_CURSOR_INTEGRATION_VERSION || void 0; var PROVIDER_HOSTS = { github: "github.com", gitlab: "gitlab.com", diff --git a/bundle/stop.js b/bundle/stop.js index 15a25a7..813b072 100755 --- a/bundle/stop.js +++ b/bundle/stop.js @@ -666,7 +666,7 @@ var DEFAULT_TAGS = ["cursor", "coding-agent"]; var DEFAULT_PROJECT = "cursor"; // dist/config.js -var LS_INTEGRATION_VERSION = true ? "0.3.1" : process.env.LANGSMITH_CURSOR_INTEGRATION_VERSION || void 0; +var LS_INTEGRATION_VERSION = true ? "0.3.2" : process.env.LANGSMITH_CURSOR_INTEGRATION_VERSION || void 0; var PROVIDER_HOSTS = { github: "github.com", gitlab: "gitlab.com", diff --git a/bundle/subagent-start.js b/bundle/subagent-start.js index 07d5c3b..dd17432 100755 --- a/bundle/subagent-start.js +++ b/bundle/subagent-start.js @@ -64,7 +64,7 @@ function debug(message) { var DEFAULT_PROJECT = "cursor"; // dist/config.js -var LS_INTEGRATION_VERSION = true ? "0.3.1" : process.env.LANGSMITH_CURSOR_INTEGRATION_VERSION || void 0; +var LS_INTEGRATION_VERSION = true ? "0.3.2" : process.env.LANGSMITH_CURSOR_INTEGRATION_VERSION || void 0; var PROVIDER_HOSTS = { github: "github.com", gitlab: "gitlab.com", diff --git a/bundle/subagent-stop.js b/bundle/subagent-stop.js index c53bbb5..401326c 100755 --- a/bundle/subagent-stop.js +++ b/bundle/subagent-stop.js @@ -64,7 +64,7 @@ function debug(message) { var DEFAULT_PROJECT = "cursor"; // dist/config.js -var LS_INTEGRATION_VERSION = true ? "0.3.1" : process.env.LANGSMITH_CURSOR_INTEGRATION_VERSION || void 0; +var LS_INTEGRATION_VERSION = true ? "0.3.2" : process.env.LANGSMITH_CURSOR_INTEGRATION_VERSION || void 0; var PROVIDER_HOSTS = { github: "github.com", gitlab: "gitlab.com", diff --git a/package.json b/package.json index 7f0a620..c2106c4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@langchain/langsmith-cursor-plugins", - "version": "0.3.1", + "version": "0.3.2", "description": "Cursor hooks integration for LangSmith tracing", "type": "module", "main": "dist/index.js",