oh-langfuse 0.1.78 → 0.1.80

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.
@@ -2,15 +2,25 @@ import fs from "node:fs";
2
2
  import fsp from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import os from "node:os";
5
- import { execFileSync, spawnSync } from "node:child_process";
6
- import { fileURLToPath } from "node:url";
7
- import { defaultWindowsNpmCliCandidates, listSystemCliCandidates } from "./cli-detection-utils.mjs";
8
- import { writeRuntimeInstallRecord } from "./runtime-state-utils.mjs";
5
+ import { execFileSync, spawnSync } from "node:child_process";
6
+ import { fileURLToPath } from "node:url";
7
+ import { defaultWindowsNpmCliCandidates, listSystemCliCandidates } from "./cli-detection-utils.mjs";
8
+ import { writeRuntimeInstallRecord } from "./runtime-state-utils.mjs";
9
9
 
10
10
  const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
11
+ const DEFAULT_LANGFUSE_BASE_URL = "https://metrics.openharmonyinsight.cn";
12
+ const LEGACY_LANGFUSE_BASE_URLS = new Set([
13
+ "http://120.46.221.227:3000",
14
+ "https://120.46.221.227:3000",
15
+ ]);
11
16
  const USER_ID_PATTERN = /^[a-z](?:\d{8}|wx\d{7})$/;
12
17
  const USER_ID_PATTERN_TEXT = "^[a-z](?:\\d{8}|wx\\d{7})$";
13
18
 
19
+ function normalizeLegacyBaseUrl(baseUrl) {
20
+ const value = String(baseUrl || "").trim().replace(/\/+$/, "");
21
+ return LEGACY_LANGFUSE_BASE_URLS.has(value) ? DEFAULT_LANGFUSE_BASE_URL : baseUrl;
22
+ }
23
+
14
24
  function normalizeUserId(v) {
15
25
  return String(v || "").trim();
16
26
  }
@@ -68,63 +78,63 @@ function pythonExecutableInVenv(venvDir) {
68
78
  : path.join(venvDir, "bin", "python");
69
79
  }
70
80
 
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
- }
81
+ function pythonEnv(extraPythonPath = "") {
82
+ const env = { ...process.env, PYTHONUTF8: "1" };
83
+ if (extraPythonPath) {
84
+ env.PYTHONPATH = process.env.PYTHONPATH ? `${extraPythonPath}${path.delimiter}${process.env.PYTHONPATH}` : extraPythonPath;
85
+ }
86
+ return env;
87
+ }
88
+
89
+ function pythonCanImport(pythonCmd, moduleName, extraPythonPath = "") {
90
+ try {
91
+ execFileSync(pythonCmd, ["-c", `import ${moduleName}`], { stdio: "ignore", encoding: "utf8", env: pythonEnv(extraPythonPath) });
92
+ return true;
93
+ } catch {
94
+ return false;
95
+ }
96
+ }
97
+
98
+ function installLangfuseIntoTargetDir({ pythonCmd, baseDir, pipIndexUrl }) {
99
+ const targetDir = path.join(baseDir, "langfuse-python");
100
+ ensureDir(targetDir);
101
+ console.log(`Installing Python package into isolated target: ${targetDir}`);
102
+ execFileSync(
103
+ pythonCmd,
104
+ ["-m", "pip", "install", "--target", targetDir, "--upgrade", "langfuse", "-i", pipIndexUrl],
105
+ { stdio: "inherit", encoding: "utf8", env: pythonEnv() }
106
+ );
107
+ if (!pythonCanImport(pythonCmd, "langfuse", targetDir)) {
108
+ throw new Error(`langfuse was installed into ${targetDir}, but ${pythonCmd} still cannot import it with PYTHONPATH.`);
109
+ }
110
+ return { python: pythonCmd, pythonPath: targetDir };
111
+ }
112
+
113
+ function runPipInstallWithFallback({ pythonCmd, baseDir, pipIndexUrl }) {
114
+ const attempts = [
115
+ () => installLangfuseIntoTargetDir({ pythonCmd, baseDir, pipIndexUrl }),
116
+ () => installLangfuseIntoTargetDir({ pythonCmd: "python3", baseDir, pipIndexUrl }),
117
+ () => installLangfuseIntoTargetDir({ pythonCmd: "python", baseDir, pipIndexUrl })
118
+ ];
119
+
120
+ const errors = [];
121
+ for (const attempt of attempts) {
122
+ try {
123
+ return attempt();
124
+ } catch (error) {
125
+ errors.push(error?.message || String(error));
126
+ }
127
+ }
128
+
129
+ throw new Error(
130
+ `Failed to install langfuse into isolated Python target. Last errors: ${errors.join(" | ")}`
131
+ );
132
+ }
133
+
134
+ function installLangfuseWithSystemPython({ pythonCmd, baseDir, pipIndexUrl }) {
135
+ console.log("Python venv is unavailable; falling back to isolated PYTHONPATH install for langfuse.");
136
+ return runPipInstallWithFallback({ pythonCmd, baseDir, pipIndexUrl });
137
+ }
128
138
 
129
139
  function shQuote(s) {
130
140
  return `'${String(s).replace(/'/g, "'\"'\"'")}'`;
@@ -150,7 +160,7 @@ function writeAutoUpdateHelper(target) {
150
160
  const helperDir = autoUpdateHelperDir();
151
161
  ensureDir(helperDir);
152
162
  const runtimePath = autoUpdateRuntimePath();
153
- const fallbackArgs = ["-y", "--registry=https://registry.npmjs.org/", "oh-langfuse@latest", "auto-update", target, "--skip-check", "--startup-status"];
163
+ const fallbackArgs = ["-y", "--registry=https://registry.npmjs.org/", "oh-langfuse@latest", "auto-update", target, "--skip-check", "--startup-status"];
154
164
 
155
165
  if (process.platform === "win32") {
156
166
  const helper = path.join(helperDir, `oh-langfuse-auto-update-${target}.cmd`);
@@ -165,7 +175,7 @@ function writeAutoUpdateHelper(target) {
165
175
  "exit /b 0",
166
176
  ""
167
177
  ];
168
- fs.writeFileSync(helper, lines.join(os.EOL), "utf8");
178
+ fs.writeFileSync(helper, lines.join(os.EOL), "utf8");
169
179
  return helper;
170
180
  }
171
181
 
@@ -235,11 +245,11 @@ function listLocalCodexCliCandidates() {
235
245
  return candidates;
236
246
  }
237
247
 
238
- function listCliCandidatesFromPath(target) {
239
- return listSystemCliCandidates(target);
240
- }
241
-
242
- function sortWindowsCliCandidates(candidates) {
248
+ function listCliCandidatesFromPath(target) {
249
+ return listSystemCliCandidates(target);
250
+ }
251
+
252
+ function sortWindowsCliCandidates(candidates) {
243
253
  const extPriority = (candidate) => {
244
254
  const ext = path.extname(String(candidate || "")).toLowerCase();
245
255
  if (ext === ".cmd" || ext === ".bat" || ext === ".exe") return 0;
@@ -248,13 +258,13 @@ function sortWindowsCliCandidates(candidates) {
248
258
  return [...candidates].sort((a, b) => extPriority(a) - extPriority(b));
249
259
  }
250
260
 
251
- function resolveAgentCli({ target, preferred = "", shimDir = "" }) {
252
- const candidates = [
253
- preferred,
254
- ...(target === "codex" ? listLocalCodexCliCandidates() : []),
255
- ...defaultWindowsNpmCliCandidates(target),
256
- ...listCliCandidatesFromPath(target)
257
- ];
261
+ function resolveAgentCli({ target, preferred = "", shimDir = "" }) {
262
+ const candidates = [
263
+ preferred,
264
+ ...(target === "codex" ? listLocalCodexCliCandidates() : []),
265
+ ...defaultWindowsNpmCliCandidates(target),
266
+ ...listCliCandidatesFromPath(target)
267
+ ];
258
268
  for (const candidate of candidates) {
259
269
  const found = existingCliCandidate(candidate, shimDir);
260
270
  if (found) return found;
@@ -308,7 +318,7 @@ function writeAgentCommandShim({ baseDir, target, executable, realCli, publicKey
308
318
  "exit /b %ERRORLEVEL%",
309
319
  ""
310
320
  ].filter(Boolean).join(os.EOL);
311
- fs.writeFileSync(shim, content, "utf8");
321
+ fs.writeFileSync(shim, content, "utf8");
312
322
  return { shim, shimDir };
313
323
  }
314
324
 
@@ -340,7 +350,7 @@ function createAgentLauncher({ baseDir, target, executable }) {
340
350
  `${executable} %*`,
341
351
  ""
342
352
  ].join(os.EOL);
343
- fs.writeFileSync(launcher, content, "utf8");
353
+ fs.writeFileSync(launcher, content, "utf8");
344
354
  return launcher;
345
355
  }
346
356
 
@@ -365,13 +375,13 @@ function createOrUpdateLangfuseVenv({ baseDir, pipIndexUrl = "https://pypi.tuna.
365
375
  if (!fs.existsSync(venvPython)) {
366
376
  console.log(`Creating Python virtual environment: ${venvDir}`);
367
377
  try {
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
- }
378
+ execFileSync(pythonCmd, ["-m", "venv", venvDir], { stdio: "inherit", encoding: "utf8", env: { ...process.env, PYTHONUTF8: "1" } });
379
+ } catch (e) {
380
+ if (process.platform !== "win32") {
381
+ return installLangfuseWithSystemPython({ pythonCmd, baseDir, pipIndexUrl });
382
+ }
383
+ throw new Error("Failed to create Python venv. Please confirm Python venv support is available.");
384
+ }
375
385
  }
376
386
 
377
387
  console.log("Installing/updating Python package in venv: langfuse");
@@ -380,31 +390,31 @@ function createOrUpdateLangfuseVenv({ baseDir, pipIndexUrl = "https://pypi.tuna.
380
390
  venvPython,
381
391
  ["-m", "pip", "install", "-U", "langfuse", "-i", pipIndexUrl],
382
392
  { stdio: "inherit", encoding: "utf8", env: { ...process.env, PYTHONUTF8: "1" } }
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
- }
393
+ );
394
+ } catch (e) {
395
+ if (process.platform !== "win32") {
396
+ return installLangfuseWithSystemPython({ pythonCmd, baseDir, pipIndexUrl });
397
+ }
398
+ throw new Error(`Failed to install langfuse in venv: ${venvPython} -m pip install -U langfuse -i ${pipIndexUrl}`);
399
+ }
400
+
401
+ if (!pythonCanImport(venvPython, "langfuse")) {
402
+ if (process.platform !== "win32") {
403
+ return installLangfuseWithSystemPython({ pythonCmd, baseDir, pipIndexUrl });
404
+ }
405
+ throw new Error(`langfuse was installed in venv, but cannot be imported by ${venvPython}`);
406
+ }
407
+
408
+ return { python: venvPython, pythonPath: "" };
409
+ }
410
+
411
+ function codexNotifyCommand({ python, pythonPath = "", hookPath }) {
412
+ if (pythonPath && process.platform !== "win32") {
413
+ const script = `if [ -n "\${PYTHONPATH:-}" ]; then export PYTHONPATH=${shQuote(pythonPath)}:"$PYTHONPATH"; else export PYTHONPATH=${shQuote(pythonPath)}; fi; exec ${shQuote(python)} ${shQuote(hookPath)}`;
414
+ return ["sh", "-c", script];
415
+ }
416
+ return [python, hookPath];
417
+ }
408
418
 
409
419
  function updateCodexNotify(configPath, notifyCommand) {
410
420
  let content = fs.existsSync(configPath) ? fs.readFileSync(configPath, "utf8") : "";
@@ -463,13 +473,14 @@ async function main() {
463
473
  args.publicKey || process.env.LANGFUSE_PUBLIC_KEY || "pk-lf-da0c90a7-6e93-4eb7-bb86-c1047c8d187d";
464
474
  const secretKey =
465
475
  args.secretKey || process.env.LANGFUSE_SECRET_KEY || "sk-lf-0269b85d-bfdc-442c-bfa3-e737954e3315";
466
- const baseUrl =
476
+ const baseUrl = normalizeLegacyBaseUrl(
467
477
  args.langfuseBaseUrl ||
468
478
  args.langfuseHost ||
469
479
  args.host ||
470
480
  process.env.LANGFUSE_BASEURL ||
471
481
  process.env.LANGFUSE_HOST ||
472
- "http://120.46.221.227:3000";
482
+ DEFAULT_LANGFUSE_BASE_URL
483
+ );
473
484
  const userId = normalizeUserId(args.userId || args.userid || "");
474
485
  if (!userId || typeof userId !== "string") {
475
486
  throw new Error("缺少参数:--userId=你的工号");
@@ -505,11 +516,11 @@ async function main() {
505
516
  userId,
506
517
  });
507
518
 
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 }));
519
+ const pythonCmd = process.platform === "win32" ? "python" : "python3";
520
+ const notifyRuntime = args["skip-pip-install"]
521
+ ? { python: pythonCmd, pythonPath: "" }
522
+ : createOrUpdateLangfuseVenv({ baseDir: codexHome, pipIndexUrl });
523
+ updateCodexNotify(configPath, codexNotifyCommand({ python: notifyRuntime.python, pythonPath: notifyRuntime.pythonPath, hookPath: destHook }));
513
524
  const codexShimDir = path.join(codexHome, "bin");
514
525
  const realCodexCli = resolveAgentCli({ target: "codex", preferred: args.cmd || "", shimDir: codexShimDir });
515
526
  const directShim = writeAgentCommandShim({
@@ -1,99 +1,99 @@
1
- export function stripBom(s) {
2
- if (typeof s !== "string" || s.length === 0) return s;
3
- return s.charCodeAt(0) === 0xfeff ? s.slice(1) : s;
4
- }
5
-
6
- function stripJsonComments(text) {
7
- let out = "";
8
- let inString = false;
9
- let escaped = false;
10
-
11
- for (let i = 0; i < text.length; i += 1) {
12
- const c = text[i];
13
- const n = text[i + 1];
14
-
15
- if (inString) {
16
- out += c;
17
- if (escaped) escaped = false;
18
- else if (c === "\\") escaped = true;
19
- else if (c === '"') inString = false;
20
- continue;
21
- }
22
-
23
- if (c === '"') {
24
- inString = true;
25
- out += c;
26
- continue;
27
- }
28
-
29
- if (c === "/" && n === "/") {
30
- while (i < text.length && text[i] !== "\n" && text[i] !== "\r") i += 1;
31
- if (i < text.length) out += text[i];
32
- continue;
33
- }
34
-
35
- if (c === "/" && n === "*") {
36
- i += 2;
37
- while (i < text.length - 1 && !(text[i] === "*" && text[i + 1] === "/")) {
38
- out += text[i] === "\n" || text[i] === "\r" ? text[i] : " ";
39
- i += 1;
40
- }
41
- i += 1;
42
- continue;
43
- }
44
-
45
- out += c;
46
- }
47
-
48
- return out;
49
- }
50
-
51
- function removeTrailingCommas(text) {
52
- let out = "";
53
- let inString = false;
54
- let escaped = false;
55
-
56
- for (let i = 0; i < text.length; i += 1) {
57
- const c = text[i];
58
-
59
- if (inString) {
60
- out += c;
61
- if (escaped) escaped = false;
62
- else if (c === "\\") escaped = true;
63
- else if (c === '"') inString = false;
64
- continue;
65
- }
66
-
67
- if (c === '"') {
68
- inString = true;
69
- out += c;
70
- continue;
71
- }
72
-
73
- if (c === ",") {
74
- let j = i + 1;
75
- while (j < text.length && /\s/.test(text[j])) j += 1;
76
- if (text[j] === "}" || text[j] === "]") continue;
77
- }
78
-
79
- out += c;
80
- }
81
-
82
- return out;
83
- }
84
-
85
- export function parseJsonRelaxed(text, filePath = "JSON file") {
86
- const raw = stripBom(text);
87
- try {
88
- return JSON.parse(raw);
89
- } catch (strictError) {
90
- const relaxed = removeTrailingCommas(stripJsonComments(raw));
91
- try {
92
- return JSON.parse(relaxed);
93
- } catch (relaxedError) {
94
- throw new Error(
95
- `Cannot parse ${filePath}: ${relaxedError.message}. Original JSON.parse error: ${strictError.message}`
96
- );
97
- }
98
- }
99
- }
1
+ export function stripBom(s) {
2
+ if (typeof s !== "string" || s.length === 0) return s;
3
+ return s.charCodeAt(0) === 0xfeff ? s.slice(1) : s;
4
+ }
5
+
6
+ function stripJsonComments(text) {
7
+ let out = "";
8
+ let inString = false;
9
+ let escaped = false;
10
+
11
+ for (let i = 0; i < text.length; i += 1) {
12
+ const c = text[i];
13
+ const n = text[i + 1];
14
+
15
+ if (inString) {
16
+ out += c;
17
+ if (escaped) escaped = false;
18
+ else if (c === "\\") escaped = true;
19
+ else if (c === '"') inString = false;
20
+ continue;
21
+ }
22
+
23
+ if (c === '"') {
24
+ inString = true;
25
+ out += c;
26
+ continue;
27
+ }
28
+
29
+ if (c === "/" && n === "/") {
30
+ while (i < text.length && text[i] !== "\n" && text[i] !== "\r") i += 1;
31
+ if (i < text.length) out += text[i];
32
+ continue;
33
+ }
34
+
35
+ if (c === "/" && n === "*") {
36
+ i += 2;
37
+ while (i < text.length - 1 && !(text[i] === "*" && text[i + 1] === "/")) {
38
+ out += text[i] === "\n" || text[i] === "\r" ? text[i] : " ";
39
+ i += 1;
40
+ }
41
+ i += 1;
42
+ continue;
43
+ }
44
+
45
+ out += c;
46
+ }
47
+
48
+ return out;
49
+ }
50
+
51
+ function removeTrailingCommas(text) {
52
+ let out = "";
53
+ let inString = false;
54
+ let escaped = false;
55
+
56
+ for (let i = 0; i < text.length; i += 1) {
57
+ const c = text[i];
58
+
59
+ if (inString) {
60
+ out += c;
61
+ if (escaped) escaped = false;
62
+ else if (c === "\\") escaped = true;
63
+ else if (c === '"') inString = false;
64
+ continue;
65
+ }
66
+
67
+ if (c === '"') {
68
+ inString = true;
69
+ out += c;
70
+ continue;
71
+ }
72
+
73
+ if (c === ",") {
74
+ let j = i + 1;
75
+ while (j < text.length && /\s/.test(text[j])) j += 1;
76
+ if (text[j] === "}" || text[j] === "]") continue;
77
+ }
78
+
79
+ out += c;
80
+ }
81
+
82
+ return out;
83
+ }
84
+
85
+ export function parseJsonRelaxed(text, filePath = "JSON file") {
86
+ const raw = stripBom(text);
87
+ try {
88
+ return JSON.parse(raw);
89
+ } catch (strictError) {
90
+ const relaxed = removeTrailingCommas(stripJsonComments(raw));
91
+ try {
92
+ return JSON.parse(relaxed);
93
+ } catch (relaxedError) {
94
+ throw new Error(
95
+ `Cannot parse ${filePath}: ${relaxedError.message}. Original JSON.parse error: ${strictError.message}`
96
+ );
97
+ }
98
+ }
99
+ }
@@ -14,19 +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
- }
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
- }
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
+ }
30
30
 
31
31
  function launcherPath(hooksDir) {
32
32
  return process.platform === "win32"
@@ -71,18 +71,18 @@ function pathExists(p) {
71
71
  return typeof p === "string" && p.length > 0 && fs.existsSync(p);
72
72
  }
73
73
 
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
- });
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
+ });
86
86
  if (result.status === 0) return { ok: true, detail: "OK" };
87
87
  const err = `${result.stderr || result.stdout || result.error?.message || "unknown error"}`.trim();
88
88
  return { ok: false, detail: err || "import failed" };
@@ -93,11 +93,11 @@ function main() {
93
93
  const claudeDir = path.join(userHome, ".claude");
94
94
  const hooksDir = path.join(claudeDir, "hooks");
95
95
  const settingsPath = path.join(claudeDir, "settings.json");
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");
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");
101
101
 
102
102
  const results = [];
103
103
 
@@ -152,26 +152,26 @@ function main() {
152
152
  "Run setup again. Claude Code should execute the generated Python hook command."
153
153
  );
154
154
 
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
- );
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
+ );
175
175
 
176
176
  addResult(results, "hook log path", true, logPath);
177
177