oh-langfuse 0.1.72 → 0.1.73
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 +1 -1
- package/scripts/codex-langfuse-check.mjs +28 -7
- package/scripts/codex-langfuse-setup.mjs +94 -62
- package/scripts/langfuse-check.mjs +50 -32
- package/scripts/langfuse-setup.mjs +107 -80
package/package.json
CHANGED
|
@@ -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 =
|
|
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(
|
|
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
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
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
|
|
478
|
-
? pythonCmd
|
|
479
|
-
: createOrUpdateLangfuseVenv({ baseDir: codexHome, pipIndexUrl });
|
|
480
|
-
updateCodexNotify(configPath,
|
|
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
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
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 =
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
function
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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
|
-
`
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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
|
-
|
|
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
|
|
521
|
-
const
|
|
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({
|