oh-langfuse 0.1.72 → 0.1.74

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-langfuse",
3
- "version": "0.1.72",
3
+ "version": "0.1.74",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Use npm scripts to configure Claude Code / OpenCode / Codex with Langfuse tracing.",
@@ -38,8 +38,8 @@ function findLatestSession(sessionsDir) {
38
38
  return latest;
39
39
  }
40
40
 
41
- function commandOk(cmd, args) {
42
- const r = spawnSync(cmd, args, { encoding: "utf8" });
41
+ function commandOk(cmd, args, env = process.env) {
42
+ const r = spawnSync(cmd, args, { encoding: "utf8", env });
43
43
  return { ok: !r.error && r.status === 0, detail: (r.stdout || r.stderr || "").trim() };
44
44
  }
45
45
 
@@ -63,8 +63,22 @@ function readCodexShimTarget(codexHome) {
63
63
  function venvPython(codexHome) {
64
64
  return process.platform === "win32"
65
65
  ? path.join(codexHome, "langfuse-venv", "Scripts", "python.exe")
66
- : path.join(codexHome, "langfuse-venv", "bin", "python");
67
- }
66
+ : path.join(codexHome, "langfuse-venv", "bin", "python");
67
+ }
68
+
69
+ function fallbackPythonCommand() {
70
+ return process.platform === "win32" ? "python" : "python3";
71
+ }
72
+
73
+ function isolatedPythonPath(codexHome) {
74
+ return path.join(codexHome, "langfuse-python");
75
+ }
76
+
77
+ function langfuseImportOk(hookPython, fallbackTarget) {
78
+ if (fs.existsSync(hookPython)) return commandOk(hookPython, ["-c", "import langfuse; print('langfuse ok')"]);
79
+ const env = { ...process.env, PYTHONPATH: process.env.PYTHONPATH ? `${fallbackTarget}${path.delimiter}${process.env.PYTHONPATH}` : fallbackTarget };
80
+ return commandOk(fallbackPythonCommand(), ["-c", "import langfuse; print('langfuse ok')"], env);
81
+ }
68
82
 
69
83
  function configHasNotify(configText) {
70
84
  const firstSection = configText.search(/^\s*\[/m);
@@ -84,10 +98,11 @@ function main() {
84
98
  const sessionsDir = path.join(codexHome, "sessions");
85
99
  const latestSession = findLatestSession(sessionsDir);
86
100
  const langfuseConfig = readJsonIfExists(langfuseConfigPath);
87
- const configText = fs.existsSync(configPath) ? stripBom(fs.readFileSync(configPath, "utf8")) : "";
101
+ const configText = fs.existsSync(configPath) ? stripBom(fs.readFileSync(configPath, "utf8")) : "";
88
102
  const hookPython = venvPython(codexHome);
103
+ const fallbackTarget = isolatedPythonPath(codexHome);
89
104
  const python = commandOk(process.platform === "win32" ? "python" : "python3", ["--version"]);
90
- const langfuseImport = commandOk(hookPython, ["-c", "import langfuse; print('langfuse ok')"]);
105
+ const langfuseImport = langfuseImportOk(hookPython, fallbackTarget);
91
106
  const codexShimTarget = readCodexShimTarget(codexHome);
92
107
  const codexShimUsesWindowsApps = !!codexShimTarget && isWindowsAppsPath(codexShimTarget);
93
108
 
@@ -138,7 +153,13 @@ function main() {
138
153
  codexShimTarget || "not installed",
139
154
  "Run setup/update again with a fixed oh-langfuse version; WindowsApps Codex paths cannot be called directly."
140
155
  );
141
- addResult(results, "Langfuse venv Python", fs.existsSync(hookPython), hookPython, "Run setup again; on Linux install python3-venv if venv creation fails.");
156
+ addResult(
157
+ results,
158
+ "Langfuse hook Python",
159
+ fs.existsSync(hookPython) || fs.existsSync(fallbackTarget),
160
+ fs.existsSync(hookPython) ? hookPython : `${fallbackPythonCommand()} with PYTHONPATH=${fallbackTarget}`,
161
+ "Run setup again; on Linux the installer can fall back to an isolated PYTHONPATH target."
162
+ );
142
163
  addResult(
143
164
  results,
144
165
  "Python langfuse package",
@@ -68,46 +68,63 @@ function pythonExecutableInVenv(venvDir) {
68
68
  : path.join(venvDir, "bin", "python");
69
69
  }
70
70
 
71
- function pythonCanImport(pythonCmd, moduleName) {
72
- try {
73
- execFileSync(pythonCmd, ["-c", `import ${moduleName}`], { stdio: "ignore", encoding: "utf8", env: { ...process.env, PYTHONUTF8: "1" } });
74
- return true;
75
- } catch {
76
- return false;
77
- }
78
- }
79
-
80
- function runPipInstallWithFallback({ pythonCmd, pipIndexUrl }) {
81
- const attempts = [
82
- { command: pythonCmd, args: ["-m", "pip", "install", "--user", "-U", "langfuse", "-i", pipIndexUrl] },
83
- { command: "pip3", args: ["install", "--user", "-U", "langfuse", "-i", pipIndexUrl] },
84
- { command: "pip", args: ["install", "--user", "-U", "langfuse", "-i", pipIndexUrl] }
85
- ];
86
-
87
- const errors = [];
88
- for (const attempt of attempts) {
89
- try {
90
- console.log(`Trying Python package install: ${attempt.command} ${attempt.args.join(" ")}`);
91
- execFileSync(attempt.command, attempt.args, { stdio: "inherit", encoding: "utf8", env: { ...process.env, PYTHONUTF8: "1" } });
92
- return;
93
- } catch (error) {
94
- errors.push(`${attempt.command}: ${error?.message || error}`);
95
- }
96
- }
97
-
98
- throw new Error(
99
- `Failed to install langfuse with system Python/pip. Tried python -m pip, pip3, and pip. Last errors: ${errors.join(" | ")}`
100
- );
101
- }
102
-
103
- function installLangfuseWithSystemPython({ pythonCmd, pipIndexUrl }) {
104
- console.log("Python venv is unavailable; falling back to system Python user install for langfuse.");
105
- runPipInstallWithFallback({ pythonCmd, pipIndexUrl });
106
- if (!pythonCanImport(pythonCmd, "langfuse")) {
107
- throw new Error("langfuse was installed with pip, but python3 still cannot import it. Install python3-venv and rerun setup, for example: sudo apt install python3-venv");
108
- }
109
- return pythonCmd;
110
- }
71
+ function pythonEnv(extraPythonPath = "") {
72
+ const env = { ...process.env, PYTHONUTF8: "1" };
73
+ if (extraPythonPath) {
74
+ env.PYTHONPATH = process.env.PYTHONPATH ? `${extraPythonPath}${path.delimiter}${process.env.PYTHONPATH}` : extraPythonPath;
75
+ }
76
+ return env;
77
+ }
78
+
79
+ function pythonCanImport(pythonCmd, moduleName, extraPythonPath = "") {
80
+ try {
81
+ execFileSync(pythonCmd, ["-c", `import ${moduleName}`], { stdio: "ignore", encoding: "utf8", env: pythonEnv(extraPythonPath) });
82
+ return true;
83
+ } catch {
84
+ return false;
85
+ }
86
+ }
87
+
88
+ function installLangfuseIntoTargetDir({ pythonCmd, baseDir, pipIndexUrl }) {
89
+ const targetDir = path.join(baseDir, "langfuse-python");
90
+ ensureDir(targetDir);
91
+ console.log(`Installing Python package into isolated target: ${targetDir}`);
92
+ execFileSync(
93
+ pythonCmd,
94
+ ["-m", "pip", "install", "--target", targetDir, "--upgrade", "langfuse", "-i", pipIndexUrl],
95
+ { stdio: "inherit", encoding: "utf8", env: pythonEnv() }
96
+ );
97
+ if (!pythonCanImport(pythonCmd, "langfuse", targetDir)) {
98
+ throw new Error(`langfuse was installed into ${targetDir}, but ${pythonCmd} still cannot import it with PYTHONPATH.`);
99
+ }
100
+ return { python: pythonCmd, pythonPath: targetDir };
101
+ }
102
+
103
+ function runPipInstallWithFallback({ pythonCmd, baseDir, pipIndexUrl }) {
104
+ const attempts = [
105
+ () => installLangfuseIntoTargetDir({ pythonCmd, baseDir, pipIndexUrl }),
106
+ () => installLangfuseIntoTargetDir({ pythonCmd: "python3", baseDir, pipIndexUrl }),
107
+ () => installLangfuseIntoTargetDir({ pythonCmd: "python", baseDir, pipIndexUrl })
108
+ ];
109
+
110
+ const errors = [];
111
+ for (const attempt of attempts) {
112
+ try {
113
+ return attempt();
114
+ } catch (error) {
115
+ errors.push(error?.message || String(error));
116
+ }
117
+ }
118
+
119
+ throw new Error(
120
+ `Failed to install langfuse into isolated Python target. Last errors: ${errors.join(" | ")}`
121
+ );
122
+ }
123
+
124
+ function installLangfuseWithSystemPython({ pythonCmd, baseDir, pipIndexUrl }) {
125
+ console.log("Python venv is unavailable; falling back to isolated PYTHONPATH install for langfuse.");
126
+ return runPipInstallWithFallback({ pythonCmd, baseDir, pipIndexUrl });
127
+ }
111
128
 
112
129
  function shQuote(s) {
113
130
  return `'${String(s).replace(/'/g, "'\"'\"'")}'`;
@@ -348,13 +365,13 @@ function createOrUpdateLangfuseVenv({ baseDir, pipIndexUrl = "https://pypi.tuna.
348
365
  if (!fs.existsSync(venvPython)) {
349
366
  console.log(`Creating Python virtual environment: ${venvDir}`);
350
367
  try {
351
- execFileSync(pythonCmd, ["-m", "venv", venvDir], { stdio: "inherit", encoding: "utf8", env: { ...process.env, PYTHONUTF8: "1" } });
352
- } catch (e) {
353
- if (process.platform !== "win32") {
354
- return installLangfuseWithSystemPython({ pythonCmd, pipIndexUrl });
355
- }
356
- throw new Error("Failed to create Python venv. Please confirm Python venv support is available.");
357
- }
368
+ execFileSync(pythonCmd, ["-m", "venv", venvDir], { stdio: "inherit", encoding: "utf8", env: { ...process.env, PYTHONUTF8: "1" } });
369
+ } catch (e) {
370
+ if (process.platform !== "win32") {
371
+ return installLangfuseWithSystemPython({ pythonCmd, baseDir, pipIndexUrl });
372
+ }
373
+ throw new Error("Failed to create Python venv. Please confirm Python venv support is available.");
374
+ }
358
375
  }
359
376
 
360
377
  console.log("Installing/updating Python package in venv: langfuse");
@@ -363,16 +380,31 @@ function createOrUpdateLangfuseVenv({ baseDir, pipIndexUrl = "https://pypi.tuna.
363
380
  venvPython,
364
381
  ["-m", "pip", "install", "-U", "langfuse", "-i", pipIndexUrl],
365
382
  { stdio: "inherit", encoding: "utf8", env: { ...process.env, PYTHONUTF8: "1" } }
366
- );
367
- } catch (e) {
368
- if (process.platform !== "win32") {
369
- return installLangfuseWithSystemPython({ pythonCmd, pipIndexUrl });
370
- }
371
- throw new Error(`Failed to install langfuse in venv: ${venvPython} -m pip install -U langfuse -i ${pipIndexUrl}`);
372
- }
373
-
374
- return venvPython;
375
- }
383
+ );
384
+ } catch (e) {
385
+ if (process.platform !== "win32") {
386
+ return installLangfuseWithSystemPython({ pythonCmd, baseDir, pipIndexUrl });
387
+ }
388
+ throw new Error(`Failed to install langfuse in venv: ${venvPython} -m pip install -U langfuse -i ${pipIndexUrl}`);
389
+ }
390
+
391
+ if (!pythonCanImport(venvPython, "langfuse")) {
392
+ if (process.platform !== "win32") {
393
+ return installLangfuseWithSystemPython({ pythonCmd, baseDir, pipIndexUrl });
394
+ }
395
+ throw new Error(`langfuse was installed in venv, but cannot be imported by ${venvPython}`);
396
+ }
397
+
398
+ return { python: venvPython, pythonPath: "" };
399
+ }
400
+
401
+ function codexNotifyCommand({ python, pythonPath = "", hookPath }) {
402
+ if (pythonPath && process.platform !== "win32") {
403
+ const script = `if [ -n "\${PYTHONPATH:-}" ]; then export PYTHONPATH=${shQuote(pythonPath)}:"$PYTHONPATH"; else export PYTHONPATH=${shQuote(pythonPath)}; fi; exec ${shQuote(python)} ${shQuote(hookPath)}`;
404
+ return ["sh", "-c", script];
405
+ }
406
+ return [python, hookPath];
407
+ }
376
408
 
377
409
  function updateCodexNotify(configPath, notifyCommand) {
378
410
  let content = fs.existsSync(configPath) ? fs.readFileSync(configPath, "utf8") : "";
@@ -473,11 +505,11 @@ async function main() {
473
505
  userId,
474
506
  });
475
507
 
476
- const pythonCmd = process.platform === "win32" ? "python" : "python3";
477
- const notifyPython = args["skip-pip-install"]
478
- ? pythonCmd
479
- : createOrUpdateLangfuseVenv({ baseDir: codexHome, pipIndexUrl });
480
- updateCodexNotify(configPath, [notifyPython, destHook]);
508
+ const pythonCmd = process.platform === "win32" ? "python" : "python3";
509
+ const notifyRuntime = args["skip-pip-install"]
510
+ ? { python: pythonCmd, pythonPath: "" }
511
+ : createOrUpdateLangfuseVenv({ baseDir: codexHome, pipIndexUrl });
512
+ updateCodexNotify(configPath, codexNotifyCommand({ python: notifyRuntime.python, pythonPath: notifyRuntime.pythonPath, hookPath: destHook }));
481
513
  const codexShimDir = path.join(codexHome, "bin");
482
514
  const realCodexCli = resolveAgentCli({ target: "codex", preferred: args.cmd || "", shimDir: codexShimDir });
483
515
  const directShim = writeAgentCommandShim({
@@ -14,11 +14,19 @@ function addResult(results, item, ok, detail, fix = "") {
14
14
  results.push({ item, ok, detail, fix });
15
15
  }
16
16
 
17
- function pythonExecutableInVenv(claudeDir) {
18
- return process.platform === "win32"
19
- ? path.join(claudeDir, "langfuse-venv", "Scripts", "python.exe")
20
- : path.join(claudeDir, "langfuse-venv", "bin", "python");
21
- }
17
+ function pythonExecutableInVenv(claudeDir) {
18
+ return process.platform === "win32"
19
+ ? path.join(claudeDir, "langfuse-venv", "Scripts", "python.exe")
20
+ : path.join(claudeDir, "langfuse-venv", "bin", "python");
21
+ }
22
+
23
+ function fallbackPythonCommand() {
24
+ return process.platform === "win32" ? "python" : "python3";
25
+ }
26
+
27
+ function isolatedPythonPath(claudeDir) {
28
+ return path.join(claudeDir, "langfuse-python");
29
+ }
22
30
 
23
31
  function launcherPath(hooksDir) {
24
32
  return process.platform === "win32"
@@ -63,13 +71,18 @@ function pathExists(p) {
63
71
  return typeof p === "string" && p.length > 0 && fs.existsSync(p);
64
72
  }
65
73
 
66
- function checkPythonLangfuseImport(pythonPath) {
67
- if (!pathExists(pythonPath)) return { ok: false, detail: "python executable missing" };
68
- const result = spawnSync(pythonPath, ["-c", "import langfuse; print('OK')"], {
69
- encoding: "utf8",
70
- timeout: 15000,
71
- windowsHide: true
72
- });
74
+ function checkPythonLangfuseImport(pythonPath, extraPythonPath = "") {
75
+ if (!pathExists(pythonPath) && pythonPath !== "python3" && pythonPath !== "python") return { ok: false, detail: "python executable missing" };
76
+ const env = { ...process.env };
77
+ if (extraPythonPath) {
78
+ env.PYTHONPATH = process.env.PYTHONPATH ? `${extraPythonPath}${path.delimiter}${process.env.PYTHONPATH}` : extraPythonPath;
79
+ }
80
+ const result = spawnSync(pythonPath, ["-c", "import langfuse; print('OK')"], {
81
+ encoding: "utf8",
82
+ timeout: 15000,
83
+ windowsHide: true,
84
+ env
85
+ });
73
86
  if (result.status === 0) return { ok: true, detail: "OK" };
74
87
  const err = `${result.stderr || result.stdout || result.error?.message || "unknown error"}`.trim();
75
88
  return { ok: false, detail: err || "import failed" };
@@ -80,10 +93,11 @@ function main() {
80
93
  const claudeDir = path.join(userHome, ".claude");
81
94
  const hooksDir = path.join(claudeDir, "hooks");
82
95
  const settingsPath = path.join(claudeDir, "settings.json");
83
- const pyPath = path.join(hooksDir, "langfuse_hook.py");
84
- const expectedLauncher = launcherPath(hooksDir);
85
- const expectedPython = pythonExecutableInVenv(claudeDir);
86
- const logPath = path.join(claudeDir, "state", "langfuse_hook.log");
96
+ const pyPath = path.join(hooksDir, "langfuse_hook.py");
97
+ const expectedLauncher = launcherPath(hooksDir);
98
+ const expectedPython = pythonExecutableInVenv(claudeDir);
99
+ const fallbackTarget = isolatedPythonPath(claudeDir);
100
+ const logPath = path.join(claudeDir, "state", "langfuse_hook.log");
87
101
 
88
102
  const results = [];
89
103
 
@@ -138,22 +152,26 @@ function main() {
138
152
  "Run setup again. Claude Code should execute the generated Python hook command."
139
153
  );
140
154
 
141
- addResult(
142
- results,
143
- "hook python executable",
144
- pathExists(expectedPython),
145
- expectedPython,
146
- "Run setup again to recreate the Python venv."
147
- );
148
-
149
- const importCheck = checkPythonLangfuseImport(expectedPython);
150
- addResult(
151
- results,
152
- "python package langfuse",
153
- importCheck.ok,
154
- importCheck.detail,
155
- `Run: ${expectedPython} -m pip install -U langfuse`
156
- );
155
+ addResult(
156
+ results,
157
+ "hook python executable",
158
+ pathExists(expectedPython) || fs.existsSync(fallbackTarget),
159
+ pathExists(expectedPython) ? expectedPython : `${fallbackPythonCommand()} with PYTHONPATH=${fallbackTarget}`,
160
+ "Run setup again to recreate the Python venv."
161
+ );
162
+
163
+ const importCheck = pathExists(expectedPython)
164
+ ? checkPythonLangfuseImport(expectedPython)
165
+ : checkPythonLangfuseImport(fallbackPythonCommand(), fallbackTarget);
166
+ addResult(
167
+ results,
168
+ "python package langfuse",
169
+ importCheck.ok,
170
+ importCheck.detail,
171
+ pathExists(expectedPython)
172
+ ? `Run: ${expectedPython} -m pip install -U langfuse`
173
+ : "Run setup again to install langfuse into the isolated PYTHONPATH target."
174
+ );
157
175
 
158
176
  addResult(results, "hook log path", true, logPath);
159
177
 
@@ -340,68 +340,87 @@ function pythonExecutableInVenv(venvDir) {
340
340
  : path.join(venvDir, "bin", "python");
341
341
  }
342
342
 
343
- function pythonCanImport(pythonCmd, moduleName) {
344
- try {
345
- execFileSync(pythonCmd, ["-c", `import ${moduleName}`], { stdio: "ignore", encoding: "utf8", env: { ...process.env, PYTHONUTF8: "1" } });
346
- return true;
347
- } catch {
348
- return false;
349
- }
350
- }
351
-
352
- function runPipInstallWithFallback({ pythonCmd, pipIndexUrl }) {
353
- const attempts = [
354
- { command: pythonCmd, args: ["-m", "pip", "install", "--user", "-U", "langfuse", "-i", pipIndexUrl] },
355
- { command: "pip3", args: ["install", "--user", "-U", "langfuse", "-i", pipIndexUrl] },
356
- { command: "pip", args: ["install", "--user", "-U", "langfuse", "-i", pipIndexUrl] }
357
- ];
358
-
359
- const errors = [];
360
- for (const attempt of attempts) {
361
- try {
362
- console.log(`Trying Python package install: ${attempt.command} ${attempt.args.join(" ")}`);
363
- execFileSync(attempt.command, attempt.args, { stdio: "inherit", encoding: "utf8", env: { ...process.env, PYTHONUTF8: "1" } });
364
- return;
365
- } catch (error) {
366
- errors.push(`${attempt.command}: ${error?.message || error}`);
367
- }
368
- }
369
-
370
- throw new Error(
371
- `Failed to install langfuse with system Python/pip. Tried python -m pip, pip3, and pip. Last errors: ${errors.join(" | ")}`
372
- );
373
- }
374
-
375
- function installLangfuseWithSystemPython({ pythonCmd, pipIndexUrl }) {
376
- console.log("Python venv is unavailable; falling back to system Python user install for langfuse.");
377
- runPipInstallWithFallback({ pythonCmd, pipIndexUrl });
378
- if (!pythonCanImport(pythonCmd, "langfuse")) {
379
- throw new Error("langfuse was installed with pip, but python3 still cannot import it. Install python3-venv and rerun setup, for example: sudo apt install python3-venv");
380
- }
381
- return pythonCmd;
382
- }
383
-
384
- async function createHookLauncher({ hooksDir, hookPython, pyPath }) {
385
- if (process.platform === "win32") {
386
- const launcher = path.join(hooksDir, "run-langfuse-hook.cmd");
387
- const content = [
388
- "@echo off",
389
- `${cmdQuote(hookPython)} ${cmdQuote(pyPath)}`,
390
- ""
391
- ].join(os.EOL);
343
+ function pythonEnv(extraPythonPath = "") {
344
+ const env = { ...process.env, PYTHONUTF8: "1" };
345
+ if (extraPythonPath) {
346
+ env.PYTHONPATH = process.env.PYTHONPATH ? `${extraPythonPath}${path.delimiter}${process.env.PYTHONPATH}` : extraPythonPath;
347
+ }
348
+ return env;
349
+ }
350
+
351
+ function pythonCanImport(pythonCmd, moduleName, extraPythonPath = "") {
352
+ try {
353
+ execFileSync(pythonCmd, ["-c", `import ${moduleName}`], { stdio: "ignore", encoding: "utf8", env: pythonEnv(extraPythonPath) });
354
+ return true;
355
+ } catch {
356
+ return false;
357
+ }
358
+ }
359
+
360
+ function installLangfuseIntoTargetDir({ pythonCmd, baseDir, pipIndexUrl }) {
361
+ const targetDir = path.join(baseDir, "langfuse-python");
362
+ ensureDir(targetDir);
363
+ console.log(`Installing Python package into isolated target: ${targetDir}`);
364
+ execFileSync(
365
+ pythonCmd,
366
+ ["-m", "pip", "install", "--target", targetDir, "--upgrade", "langfuse", "-i", pipIndexUrl],
367
+ { stdio: "inherit", encoding: "utf8", env: pythonEnv() }
368
+ );
369
+ if (!pythonCanImport(pythonCmd, "langfuse", targetDir)) {
370
+ throw new Error(`langfuse was installed into ${targetDir}, but ${pythonCmd} still cannot import it with PYTHONPATH.`);
371
+ }
372
+ return { python: pythonCmd, pythonPath: targetDir };
373
+ }
374
+
375
+ function runPipInstallWithFallback({ pythonCmd, baseDir, pipIndexUrl }) {
376
+ const attempts = [
377
+ () => installLangfuseIntoTargetDir({ pythonCmd, baseDir, pipIndexUrl }),
378
+ () => installLangfuseIntoTargetDir({ pythonCmd: "python3", baseDir, pipIndexUrl }),
379
+ () => installLangfuseIntoTargetDir({ pythonCmd: "python", baseDir, pipIndexUrl })
380
+ ];
381
+
382
+ const errors = [];
383
+ for (const attempt of attempts) {
384
+ try {
385
+ return attempt();
386
+ } catch (error) {
387
+ errors.push(error?.message || String(error));
388
+ }
389
+ }
390
+
391
+ throw new Error(
392
+ `Failed to install langfuse into isolated Python target. Last errors: ${errors.join(" | ")}`
393
+ );
394
+ }
395
+
396
+ function installLangfuseWithSystemPython({ pythonCmd, baseDir, pipIndexUrl }) {
397
+ console.log("Python venv is unavailable; falling back to isolated PYTHONPATH install for langfuse.");
398
+ return runPipInstallWithFallback({ pythonCmd, baseDir, pipIndexUrl });
399
+ }
400
+
401
+ async function createHookLauncher({ hooksDir, hookPython, hookPythonPath = "", pyPath }) {
402
+ if (process.platform === "win32") {
403
+ const launcher = path.join(hooksDir, "run-langfuse-hook.cmd");
404
+ const content = [
405
+ "@echo off",
406
+ hookPythonPath ? `set PYTHONPATH=${hookPythonPath};%PYTHONPATH%` : null,
407
+ `${cmdQuote(hookPython)} ${cmdQuote(pyPath)}`,
408
+ ""
409
+ ].filter(Boolean).join(os.EOL);
392
410
  await fsp.writeFile(launcher, content, "utf8");
393
- return launcher;
394
- }
395
-
396
- const launcher = path.join(hooksDir, "run-langfuse-hook.sh");
397
- const content = [
398
- "#!/usr/bin/env sh",
399
- `exec ${shQuote(hookPython)} ${shQuote(pyPath)}`,
400
- ""
401
- ].join("\n");
402
- await fsp.writeFile(launcher, content, "utf8");
403
- fs.chmodSync(launcher, 0o755);
404
- return launcher;
411
+ return launcher;
412
+ }
413
+
414
+ const launcher = path.join(hooksDir, "run-langfuse-hook.sh");
415
+ const content = [
416
+ "#!/usr/bin/env sh",
417
+ hookPythonPath ? `if [ -n "\${PYTHONPATH:-}" ]; then export PYTHONPATH=${shQuote(hookPythonPath)}:"$PYTHONPATH"; else export PYTHONPATH=${shQuote(hookPythonPath)}; fi` : null,
418
+ `exec ${shQuote(hookPython)} ${shQuote(pyPath)}`,
419
+ ""
420
+ ].filter(Boolean).join("\n");
421
+ await fsp.writeFile(launcher, content, "utf8");
422
+ fs.chmodSync(launcher, 0o755);
423
+ return launcher;
405
424
  }
406
425
 
407
426
  function createOrUpdateLangfuseVenv({ baseDir, pipIndexUrl = "https://pypi.tuna.tsinghua.edu.cn/simple" }) {
@@ -412,13 +431,13 @@ function createOrUpdateLangfuseVenv({ baseDir, pipIndexUrl = "https://pypi.tuna.
412
431
  if (!fs.existsSync(venvPython)) {
413
432
  console.log(`Creating Python virtual environment: ${venvDir}`);
414
433
  try {
415
- execFileSync(pythonCmd, ["-m", "venv", venvDir], { stdio: "inherit", encoding: "utf8", env: { ...process.env, PYTHONUTF8: "1" } });
416
- } catch (e) {
417
- if (process.platform !== "win32") {
418
- return installLangfuseWithSystemPython({ pythonCmd, pipIndexUrl });
419
- }
420
- throw new Error("Failed to create Python venv. Please confirm Python venv support is available.");
421
- }
434
+ execFileSync(pythonCmd, ["-m", "venv", venvDir], { stdio: "inherit", encoding: "utf8", env: { ...process.env, PYTHONUTF8: "1" } });
435
+ } catch (e) {
436
+ if (process.platform !== "win32") {
437
+ return installLangfuseWithSystemPython({ pythonCmd, baseDir, pipIndexUrl });
438
+ }
439
+ throw new Error("Failed to create Python venv. Please confirm Python venv support is available.");
440
+ }
422
441
  }
423
442
 
424
443
  console.log("Installing/updating Python package in venv: langfuse");
@@ -427,16 +446,23 @@ function createOrUpdateLangfuseVenv({ baseDir, pipIndexUrl = "https://pypi.tuna.
427
446
  venvPython,
428
447
  ["-m", "pip", "install", "-U", "langfuse", "-i", pipIndexUrl],
429
448
  { stdio: "inherit", encoding: "utf8", env: { ...process.env, PYTHONUTF8: "1" } }
430
- );
431
- } catch (e) {
432
- if (process.platform !== "win32") {
433
- return installLangfuseWithSystemPython({ pythonCmd, pipIndexUrl });
434
- }
435
- throw new Error(`Failed to install langfuse in venv: ${venvPython} -m pip install -U langfuse -i ${pipIndexUrl}`);
436
- }
437
-
438
- return venvPython;
439
- }
449
+ );
450
+ } catch (e) {
451
+ if (process.platform !== "win32") {
452
+ return installLangfuseWithSystemPython({ pythonCmd, baseDir, pipIndexUrl });
453
+ }
454
+ throw new Error(`Failed to install langfuse in venv: ${venvPython} -m pip install -U langfuse -i ${pipIndexUrl}`);
455
+ }
456
+
457
+ if (!pythonCanImport(venvPython, "langfuse")) {
458
+ if (process.platform !== "win32") {
459
+ return installLangfuseWithSystemPython({ pythonCmd, baseDir, pipIndexUrl });
460
+ }
461
+ throw new Error(`langfuse was installed in venv, but cannot be imported by ${venvPython}`);
462
+ }
463
+
464
+ return { python: venvPython, pythonPath: "" };
465
+ }
440
466
 
441
467
  async function main() {
442
468
  const args = parseArgs(process.argv.slice(2));
@@ -517,8 +543,9 @@ async function main() {
517
543
  if (nextPyText !== pyText) {
518
544
  await fsp.writeFile(pyPath, nextPyText, "utf8");
519
545
  }
520
- const hookPython = createOrUpdateLangfuseVenv({ baseDir: claudeDir, pipIndexUrl });
521
- const hookLauncher = await createHookLauncher({ hooksDir, hookPython, pyPath });
546
+ const hookRuntime = createOrUpdateLangfuseVenv({ baseDir: claudeDir, pipIndexUrl });
547
+ const hookPython = hookRuntime.python;
548
+ const hookLauncher = await createHookLauncher({ hooksDir, hookPython, hookPythonPath: hookRuntime.pythonPath, pyPath });
522
549
  const claudeShimDir = path.join(claudeDir, "bin");
523
550
  const realClaudeCli = resolveAgentCli({ target: "claude", preferred: args.cmd || "", shimDir: claudeShimDir });
524
551
  const directShim = writeAgentCommandShim({
@@ -1007,7 +1007,8 @@ function writeOpencodeCommandShim(opencodeDir, { publicKey, secretKey, baseUrl,
1007
1007
  cmd.push(...windowsGitPathBootstrap());
1008
1008
  cmd.push(windowsAutoUpdateCommand("opencode"));
1009
1009
  cmd.push(`set "OH_LANGFUSE_REAL_OPENCODE=${cmdSetValue(realOpencodeCli)}"`);
1010
- cmd.push('if exist "%OH_LANGFUSE_REAL_OPENCODE%" (');
1010
+ cmd.push('echo "%OH_LANGFUSE_REAL_OPENCODE%" | findstr /I "\\node_modules\\opencode-ai\\bin\\opencode.exe" >nul');
1011
+ cmd.push('if errorlevel 1 if exist "%OH_LANGFUSE_REAL_OPENCODE%" (');
1011
1012
  cmd.push(' call "%OH_LANGFUSE_REAL_OPENCODE%" %*');
1012
1013
  cmd.push(" exit /b %ERRORLEVEL%");
1013
1014
  cmd.push(")");
@@ -1023,10 +1024,6 @@ function writeOpencodeCommandShim(opencodeDir, { publicKey, secretKey, baseUrl,
1023
1024
  cmd.push(' call "%APPDATA%\\npm\\opencode.cmd" %*');
1024
1025
  cmd.push(" exit /b %ERRORLEVEL%");
1025
1026
  cmd.push(")");
1026
- cmd.push('if exist "%APPDATA%\\npm\\node_modules\\opencode-ai\\bin\\opencode.exe" (');
1027
- cmd.push(' call "%APPDATA%\\npm\\node_modules\\opencode-ai\\bin\\opencode.exe" %*');
1028
- cmd.push(" exit /b %ERRORLEVEL%");
1029
- cmd.push(")");
1030
1027
  cmd.push('if exist "%APPDATA%\\npm\\node_modules\\opencode-ai\\node_modules\\opencode-windows-x64\\bin\\opencode.exe" (');
1031
1028
  cmd.push(' call "%APPDATA%\\npm\\node_modules\\opencode-ai\\node_modules\\opencode-windows-x64\\bin\\opencode.exe" %*');
1032
1029
  cmd.push(" exit /b %ERRORLEVEL%");
@@ -1045,8 +1042,11 @@ function writeOpencodeCommandShim(opencodeDir, { publicKey, secretKey, baseUrl,
1045
1042
  cmd.push(")");
1046
1043
  cmd.push('for /f "delims=" %%O in (\'where.exe opencode 2^>nul\') do (');
1047
1044
  cmd.push(' if /I not "%%~fO"=="%~f0" (');
1048
- cmd.push(' call "%%O" %*');
1049
- cmd.push(" exit /b %ERRORLEVEL%");
1045
+ cmd.push(' echo %%~fO | findstr /I "\\node_modules\\opencode-ai\\bin\\opencode.exe" >nul');
1046
+ cmd.push(" if errorlevel 1 (");
1047
+ cmd.push(' call "%%O" %*');
1048
+ cmd.push(" exit /b %ERRORLEVEL%");
1049
+ cmd.push(" )");
1050
1050
  cmd.push(" )");
1051
1051
  cmd.push(")");
1052
1052
  cmd.push("echo [ERROR] OpenCode CLI not found. Install OpenCode CLI first. 1>&2");
@@ -3,18 +3,28 @@ import path from "node:path";
3
3
  import os from "node:os";
4
4
  import { spawnSync } from "node:child_process";
5
5
  import { listSystemCliCandidates } from "./cli-detection-utils.mjs";
6
-
7
- /**
6
+
7
+ function isKnownBrokenWindowsOpencodeCandidate(candidate) {
8
+ if (process.platform !== "win32") return false;
9
+ const normalized = String(candidate || "").replace(/\//g, "\\").toLowerCase();
10
+ return normalized.endsWith("\\node_modules\\opencode-ai\\bin\\opencode.exe");
11
+ }
12
+
13
+ function usableCandidate(candidate) {
14
+ return candidate && !isKnownBrokenWindowsOpencodeCandidate(candidate) && fs.existsSync(candidate);
15
+ }
16
+
17
+ /**
8
18
  * OpenCode CLI 的官方安装脚本默认会把二进制放到 ~/.opencode/bin;
9
19
  * npm 全局安装则可能出现在 %AppData%\npm\opencode.cmd;PATH 也可能只认识 `opencode`。
10
20
  * @param {string|undefined} preferred --cmd=xxx 或可执行文件路径
11
21
  * @returns {string|null} 可执行的绝对路径,或仅在 PATH 中解析到则用 `opencode`
12
22
  */
13
23
  export function resolveOpencodeCli(preferred) {
14
- if (typeof preferred === "string" && preferred.trim()) {
15
- const p = preferred.trim();
16
- if (fs.existsSync(p)) return path.resolve(p);
17
- }
24
+ if (typeof preferred === "string" && preferred.trim()) {
25
+ const p = preferred.trim();
26
+ if (usableCandidate(p)) return path.resolve(p);
27
+ }
18
28
 
19
29
  const home = os.homedir();
20
30
 
@@ -30,7 +40,6 @@ export function resolveOpencodeCli(preferred) {
30
40
  if (process.env.APPDATA) {
31
41
  candidates.push(path.join(process.env.APPDATA, "npm", "opencode.cmd"));
32
42
  candidates.push(path.join(process.env.APPDATA, "npm", "opencode"));
33
- candidates.push(path.join(process.env.APPDATA, "npm", "node_modules", "opencode-ai", "bin", "opencode.exe"));
34
43
  candidates.push(path.join(process.env.APPDATA, "npm", "node_modules", "opencode-ai", "node_modules", "opencode-windows-x64", "bin", "opencode.exe"));
35
44
  candidates.push(path.join(process.env.APPDATA, "npm", "node_modules", "opencode-ai", "node_modules", "opencode-windows-x64-baseline", "bin", "opencode.exe"));
36
45
  candidates.push(path.join(process.env.APPDATA, "npm", "node_modules", "opencode-windows-x64", "bin", "opencode.exe"));
@@ -42,17 +51,17 @@ export function resolveOpencodeCli(preferred) {
42
51
  candidates.push(path.join(home, ".local", "bin", "opencode"));
43
52
  candidates.push(...listSystemCliCandidates("opencode"));
44
53
  }
45
-
46
- for (const c of candidates) {
47
- if (c && fs.existsSync(c)) return c;
48
- }
54
+
55
+ for (const c of candidates) {
56
+ if (usableCandidate(c)) return c;
57
+ }
49
58
 
50
59
  if (process.platform === "win32") {
51
60
  const r = spawnSync("where.exe", ["opencode"], { encoding: "utf8", windowsHide: true });
52
61
  if (r.status === 0 && r.stdout) {
53
- const line = r.stdout.trim().split(/\r?\n/)[0]?.trim();
54
- if (line && fs.existsSync(line)) return line;
55
- }
62
+ const line = r.stdout.trim().split(/\r?\n/)[0]?.trim();
63
+ if (usableCandidate(line)) return line;
64
+ }
56
65
  } else {
57
66
  const r = spawnSync("which", ["opencode"], { encoding: "utf8" });
58
67
  if (r.status === 0 && r.stdout) {