oh-langfuse 0.1.79 → 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.
- package/SELF_VERIFY.md +25 -25
- package/bin/cli.js +41 -41
- package/codex_langfuse_notify.py +162 -2
- package/langfuse_hook.py +165 -2
- package/package.json +2 -1
- package/scripts/auto-update-runtime.mjs +14 -14
- package/scripts/cli-detection-utils.mjs +114 -114
- package/scripts/codex-langfuse-check.mjs +65 -65
- package/scripts/codex-langfuse-setup.mjs +140 -140
- package/scripts/json-utils.mjs +99 -99
- package/scripts/langfuse-check.mjs +50 -50
- package/scripts/langfuse-setup.mjs +154 -154
- package/scripts/opencode-langfuse-check.mjs +208 -199
- package/scripts/opencode-langfuse-run.mjs +21 -21
- package/scripts/opencode-langfuse-setup.mjs +106 -50
- package/scripts/opencode-skills-discover.mjs +125 -0
- package/scripts/real-self-verify.mjs +103 -103
- package/scripts/resolve-opencode-cli.mjs +53 -53
- package/scripts/update-langfuse-runtime.mjs +49 -49
- package/setup-langfuse.bat +19 -19
- package/setup-langfuse.sh +19 -19
|
@@ -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
|
|
|
@@ -2,30 +2,30 @@ 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";
|
|
9
|
-
|
|
10
|
-
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
11
|
-
const packageJson = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8"));
|
|
12
|
-
|
|
13
|
-
const DEFAULT_LANGFUSE_BASE_URL = "https://metrics.openharmonyinsight.cn";
|
|
14
|
-
const LEGACY_LANGFUSE_BASE_URLS = new Set([
|
|
15
|
-
"http://120.46.221.227:3000",
|
|
16
|
-
"https://120.46.221.227:3000",
|
|
17
|
-
]);
|
|
18
|
-
const USER_ID_PATTERN = /^[a-z](?:\d{8}|wx\d{7})$/;
|
|
19
|
-
const USER_ID_PATTERN_TEXT = "^[a-z](?:\\d{8}|wx\\d{7})$";
|
|
20
|
-
|
|
21
|
-
function normalizeLegacyBaseUrl(baseUrl) {
|
|
22
|
-
const value = String(baseUrl || "").trim().replace(/\/+$/, "");
|
|
23
|
-
return LEGACY_LANGFUSE_BASE_URLS.has(value) ? DEFAULT_LANGFUSE_BASE_URL : baseUrl;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function normalizeUserId(v) {
|
|
27
|
-
return String(v || "").trim();
|
|
28
|
-
}
|
|
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
|
+
|
|
10
|
+
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
11
|
+
const packageJson = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8"));
|
|
12
|
+
|
|
13
|
+
const DEFAULT_LANGFUSE_BASE_URL = "https://metrics.openharmonyinsight.cn";
|
|
14
|
+
const LEGACY_LANGFUSE_BASE_URLS = new Set([
|
|
15
|
+
"http://120.46.221.227:3000",
|
|
16
|
+
"https://120.46.221.227:3000",
|
|
17
|
+
]);
|
|
18
|
+
const USER_ID_PATTERN = /^[a-z](?:\d{8}|wx\d{7})$/;
|
|
19
|
+
const USER_ID_PATTERN_TEXT = "^[a-z](?:\\d{8}|wx\\d{7})$";
|
|
20
|
+
|
|
21
|
+
function normalizeLegacyBaseUrl(baseUrl) {
|
|
22
|
+
const value = String(baseUrl || "").trim().replace(/\/+$/, "");
|
|
23
|
+
return LEGACY_LANGFUSE_BASE_URLS.has(value) ? DEFAULT_LANGFUSE_BASE_URL : baseUrl;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function normalizeUserId(v) {
|
|
27
|
+
return String(v || "").trim();
|
|
28
|
+
}
|
|
29
29
|
|
|
30
30
|
function assertValidUserId(userId) {
|
|
31
31
|
if (!USER_ID_PATTERN.test(normalizeUserId(userId))) {
|
|
@@ -172,7 +172,7 @@ function writeAutoUpdateHelper(target) {
|
|
|
172
172
|
const helperDir = autoUpdateHelperDir();
|
|
173
173
|
ensureDir(helperDir);
|
|
174
174
|
const runtimePath = autoUpdateRuntimePath();
|
|
175
|
-
const fallbackArgs = ["-y", "--registry=https://registry.npmjs.org/", "oh-langfuse@latest", "auto-update", target, "--skip-check", "--startup-status"];
|
|
175
|
+
const fallbackArgs = ["-y", "--registry=https://registry.npmjs.org/", "oh-langfuse@latest", "auto-update", target, "--skip-check", "--startup-status"];
|
|
176
176
|
|
|
177
177
|
if (process.platform === "win32") {
|
|
178
178
|
const helper = path.join(helperDir, `oh-langfuse-auto-update-${target}.cmd`);
|
|
@@ -187,7 +187,7 @@ function writeAutoUpdateHelper(target) {
|
|
|
187
187
|
"exit /b 0",
|
|
188
188
|
""
|
|
189
189
|
];
|
|
190
|
-
fs.writeFileSync(helper, lines.join(os.EOL), "utf8");
|
|
190
|
+
fs.writeFileSync(helper, lines.join(os.EOL), "utf8");
|
|
191
191
|
return helper;
|
|
192
192
|
}
|
|
193
193
|
|
|
@@ -232,16 +232,16 @@ function existingCliCandidate(candidate, shimDir) {
|
|
|
232
232
|
}
|
|
233
233
|
}
|
|
234
234
|
|
|
235
|
-
function listCliCandidatesFromPath(target) {
|
|
236
|
-
return listSystemCliCandidates(target);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
function resolveAgentCli({ target, preferred = "", shimDir = "" }) {
|
|
240
|
-
const candidates = [
|
|
241
|
-
preferred,
|
|
242
|
-
...defaultWindowsNpmCliCandidates(target),
|
|
243
|
-
...listCliCandidatesFromPath(target)
|
|
244
|
-
];
|
|
235
|
+
function listCliCandidatesFromPath(target) {
|
|
236
|
+
return listSystemCliCandidates(target);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function resolveAgentCli({ target, preferred = "", shimDir = "" }) {
|
|
240
|
+
const candidates = [
|
|
241
|
+
preferred,
|
|
242
|
+
...defaultWindowsNpmCliCandidates(target),
|
|
243
|
+
...listCliCandidatesFromPath(target)
|
|
244
|
+
];
|
|
245
245
|
for (const candidate of candidates) {
|
|
246
246
|
const found = existingCliCandidate(candidate, shimDir);
|
|
247
247
|
if (found) return found;
|
|
@@ -295,7 +295,7 @@ function writeAgentCommandShim({ baseDir, target, executable, realCli, publicKey
|
|
|
295
295
|
"exit /b %ERRORLEVEL%",
|
|
296
296
|
""
|
|
297
297
|
].filter(Boolean).join(os.EOL);
|
|
298
|
-
fs.writeFileSync(shim, content, "utf8");
|
|
298
|
+
fs.writeFileSync(shim, content, "utf8");
|
|
299
299
|
return { shim, shimDir };
|
|
300
300
|
}
|
|
301
301
|
|
|
@@ -327,7 +327,7 @@ function createAgentLauncher({ baseDir, target, executable }) {
|
|
|
327
327
|
`${executable} %*`,
|
|
328
328
|
""
|
|
329
329
|
].join(os.EOL);
|
|
330
|
-
fs.writeFileSync(launcher, content, "utf8");
|
|
330
|
+
fs.writeFileSync(launcher, content, "utf8");
|
|
331
331
|
return launcher;
|
|
332
332
|
}
|
|
333
333
|
|
|
@@ -350,87 +350,87 @@ function pythonExecutableInVenv(venvDir) {
|
|
|
350
350
|
: path.join(venvDir, "bin", "python");
|
|
351
351
|
}
|
|
352
352
|
|
|
353
|
-
function pythonEnv(extraPythonPath = "") {
|
|
354
|
-
const env = { ...process.env, PYTHONUTF8: "1" };
|
|
355
|
-
if (extraPythonPath) {
|
|
356
|
-
env.PYTHONPATH = process.env.PYTHONPATH ? `${extraPythonPath}${path.delimiter}${process.env.PYTHONPATH}` : extraPythonPath;
|
|
357
|
-
}
|
|
358
|
-
return env;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
function pythonCanImport(pythonCmd, moduleName, extraPythonPath = "") {
|
|
362
|
-
try {
|
|
363
|
-
execFileSync(pythonCmd, ["-c", `import ${moduleName}`], { stdio: "ignore", encoding: "utf8", env: pythonEnv(extraPythonPath) });
|
|
364
|
-
return true;
|
|
365
|
-
} catch {
|
|
366
|
-
return false;
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
function installLangfuseIntoTargetDir({ pythonCmd, baseDir, pipIndexUrl }) {
|
|
371
|
-
const targetDir = path.join(baseDir, "langfuse-python");
|
|
372
|
-
ensureDir(targetDir);
|
|
373
|
-
console.log(`Installing Python package into isolated target: ${targetDir}`);
|
|
374
|
-
execFileSync(
|
|
375
|
-
pythonCmd,
|
|
376
|
-
["-m", "pip", "install", "--target", targetDir, "--upgrade", "langfuse", "-i", pipIndexUrl],
|
|
377
|
-
{ stdio: "inherit", encoding: "utf8", env: pythonEnv() }
|
|
378
|
-
);
|
|
379
|
-
if (!pythonCanImport(pythonCmd, "langfuse", targetDir)) {
|
|
380
|
-
throw new Error(`langfuse was installed into ${targetDir}, but ${pythonCmd} still cannot import it with PYTHONPATH.`);
|
|
381
|
-
}
|
|
382
|
-
return { python: pythonCmd, pythonPath: targetDir };
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
function runPipInstallWithFallback({ pythonCmd, baseDir, pipIndexUrl }) {
|
|
386
|
-
const attempts = [
|
|
387
|
-
() => installLangfuseIntoTargetDir({ pythonCmd, baseDir, pipIndexUrl }),
|
|
388
|
-
() => installLangfuseIntoTargetDir({ pythonCmd: "python3", baseDir, pipIndexUrl }),
|
|
389
|
-
() => installLangfuseIntoTargetDir({ pythonCmd: "python", baseDir, pipIndexUrl })
|
|
390
|
-
];
|
|
391
|
-
|
|
392
|
-
const errors = [];
|
|
393
|
-
for (const attempt of attempts) {
|
|
394
|
-
try {
|
|
395
|
-
return attempt();
|
|
396
|
-
} catch (error) {
|
|
397
|
-
errors.push(error?.message || String(error));
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
throw new Error(
|
|
402
|
-
`Failed to install langfuse into isolated Python target. Last errors: ${errors.join(" | ")}`
|
|
403
|
-
);
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
function installLangfuseWithSystemPython({ pythonCmd, baseDir, pipIndexUrl }) {
|
|
407
|
-
console.log("Python venv is unavailable; falling back to isolated PYTHONPATH install for langfuse.");
|
|
408
|
-
return runPipInstallWithFallback({ pythonCmd, baseDir, pipIndexUrl });
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
async function createHookLauncher({ hooksDir, hookPython, hookPythonPath = "", pyPath }) {
|
|
412
|
-
if (process.platform === "win32") {
|
|
413
|
-
const launcher = path.join(hooksDir, "run-langfuse-hook.cmd");
|
|
414
|
-
const content = [
|
|
415
|
-
"@echo off",
|
|
416
|
-
hookPythonPath ? `set PYTHONPATH=${hookPythonPath};%PYTHONPATH%` : null,
|
|
417
|
-
`${cmdQuote(hookPython)} ${cmdQuote(pyPath)}`,
|
|
418
|
-
""
|
|
419
|
-
].filter(Boolean).join(os.EOL);
|
|
420
|
-
await fsp.writeFile(launcher, content, "utf8");
|
|
421
|
-
return launcher;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
const launcher = path.join(hooksDir, "run-langfuse-hook.sh");
|
|
425
|
-
const content = [
|
|
426
|
-
"#!/usr/bin/env sh",
|
|
427
|
-
hookPythonPath ? `if [ -n "\${PYTHONPATH:-}" ]; then export PYTHONPATH=${shQuote(hookPythonPath)}:"$PYTHONPATH"; else export PYTHONPATH=${shQuote(hookPythonPath)}; fi` : null,
|
|
428
|
-
`exec ${shQuote(hookPython)} ${shQuote(pyPath)}`,
|
|
429
|
-
""
|
|
430
|
-
].filter(Boolean).join("\n");
|
|
431
|
-
await fsp.writeFile(launcher, content, "utf8");
|
|
432
|
-
fs.chmodSync(launcher, 0o755);
|
|
433
|
-
return launcher;
|
|
353
|
+
function pythonEnv(extraPythonPath = "") {
|
|
354
|
+
const env = { ...process.env, PYTHONUTF8: "1" };
|
|
355
|
+
if (extraPythonPath) {
|
|
356
|
+
env.PYTHONPATH = process.env.PYTHONPATH ? `${extraPythonPath}${path.delimiter}${process.env.PYTHONPATH}` : extraPythonPath;
|
|
357
|
+
}
|
|
358
|
+
return env;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function pythonCanImport(pythonCmd, moduleName, extraPythonPath = "") {
|
|
362
|
+
try {
|
|
363
|
+
execFileSync(pythonCmd, ["-c", `import ${moduleName}`], { stdio: "ignore", encoding: "utf8", env: pythonEnv(extraPythonPath) });
|
|
364
|
+
return true;
|
|
365
|
+
} catch {
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function installLangfuseIntoTargetDir({ pythonCmd, baseDir, pipIndexUrl }) {
|
|
371
|
+
const targetDir = path.join(baseDir, "langfuse-python");
|
|
372
|
+
ensureDir(targetDir);
|
|
373
|
+
console.log(`Installing Python package into isolated target: ${targetDir}`);
|
|
374
|
+
execFileSync(
|
|
375
|
+
pythonCmd,
|
|
376
|
+
["-m", "pip", "install", "--target", targetDir, "--upgrade", "langfuse", "-i", pipIndexUrl],
|
|
377
|
+
{ stdio: "inherit", encoding: "utf8", env: pythonEnv() }
|
|
378
|
+
);
|
|
379
|
+
if (!pythonCanImport(pythonCmd, "langfuse", targetDir)) {
|
|
380
|
+
throw new Error(`langfuse was installed into ${targetDir}, but ${pythonCmd} still cannot import it with PYTHONPATH.`);
|
|
381
|
+
}
|
|
382
|
+
return { python: pythonCmd, pythonPath: targetDir };
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function runPipInstallWithFallback({ pythonCmd, baseDir, pipIndexUrl }) {
|
|
386
|
+
const attempts = [
|
|
387
|
+
() => installLangfuseIntoTargetDir({ pythonCmd, baseDir, pipIndexUrl }),
|
|
388
|
+
() => installLangfuseIntoTargetDir({ pythonCmd: "python3", baseDir, pipIndexUrl }),
|
|
389
|
+
() => installLangfuseIntoTargetDir({ pythonCmd: "python", baseDir, pipIndexUrl })
|
|
390
|
+
];
|
|
391
|
+
|
|
392
|
+
const errors = [];
|
|
393
|
+
for (const attempt of attempts) {
|
|
394
|
+
try {
|
|
395
|
+
return attempt();
|
|
396
|
+
} catch (error) {
|
|
397
|
+
errors.push(error?.message || String(error));
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
throw new Error(
|
|
402
|
+
`Failed to install langfuse into isolated Python target. Last errors: ${errors.join(" | ")}`
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function installLangfuseWithSystemPython({ pythonCmd, baseDir, pipIndexUrl }) {
|
|
407
|
+
console.log("Python venv is unavailable; falling back to isolated PYTHONPATH install for langfuse.");
|
|
408
|
+
return runPipInstallWithFallback({ pythonCmd, baseDir, pipIndexUrl });
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
async function createHookLauncher({ hooksDir, hookPython, hookPythonPath = "", pyPath }) {
|
|
412
|
+
if (process.platform === "win32") {
|
|
413
|
+
const launcher = path.join(hooksDir, "run-langfuse-hook.cmd");
|
|
414
|
+
const content = [
|
|
415
|
+
"@echo off",
|
|
416
|
+
hookPythonPath ? `set PYTHONPATH=${hookPythonPath};%PYTHONPATH%` : null,
|
|
417
|
+
`${cmdQuote(hookPython)} ${cmdQuote(pyPath)}`,
|
|
418
|
+
""
|
|
419
|
+
].filter(Boolean).join(os.EOL);
|
|
420
|
+
await fsp.writeFile(launcher, content, "utf8");
|
|
421
|
+
return launcher;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const launcher = path.join(hooksDir, "run-langfuse-hook.sh");
|
|
425
|
+
const content = [
|
|
426
|
+
"#!/usr/bin/env sh",
|
|
427
|
+
hookPythonPath ? `if [ -n "\${PYTHONPATH:-}" ]; then export PYTHONPATH=${shQuote(hookPythonPath)}:"$PYTHONPATH"; else export PYTHONPATH=${shQuote(hookPythonPath)}; fi` : null,
|
|
428
|
+
`exec ${shQuote(hookPython)} ${shQuote(pyPath)}`,
|
|
429
|
+
""
|
|
430
|
+
].filter(Boolean).join("\n");
|
|
431
|
+
await fsp.writeFile(launcher, content, "utf8");
|
|
432
|
+
fs.chmodSync(launcher, 0o755);
|
|
433
|
+
return launcher;
|
|
434
434
|
}
|
|
435
435
|
|
|
436
436
|
function createOrUpdateLangfuseVenv({ baseDir, pipIndexUrl = "https://pypi.tuna.tsinghua.edu.cn/simple" }) {
|
|
@@ -441,13 +441,13 @@ function createOrUpdateLangfuseVenv({ baseDir, pipIndexUrl = "https://pypi.tuna.
|
|
|
441
441
|
if (!fs.existsSync(venvPython)) {
|
|
442
442
|
console.log(`Creating Python virtual environment: ${venvDir}`);
|
|
443
443
|
try {
|
|
444
|
-
execFileSync(pythonCmd, ["-m", "venv", venvDir], { stdio: "inherit", encoding: "utf8", env: { ...process.env, PYTHONUTF8: "1" } });
|
|
445
|
-
} catch (e) {
|
|
446
|
-
if (process.platform !== "win32") {
|
|
447
|
-
return installLangfuseWithSystemPython({ pythonCmd, baseDir, pipIndexUrl });
|
|
448
|
-
}
|
|
449
|
-
throw new Error("Failed to create Python venv. Please confirm Python venv support is available.");
|
|
450
|
-
}
|
|
444
|
+
execFileSync(pythonCmd, ["-m", "venv", venvDir], { stdio: "inherit", encoding: "utf8", env: { ...process.env, PYTHONUTF8: "1" } });
|
|
445
|
+
} catch (e) {
|
|
446
|
+
if (process.platform !== "win32") {
|
|
447
|
+
return installLangfuseWithSystemPython({ pythonCmd, baseDir, pipIndexUrl });
|
|
448
|
+
}
|
|
449
|
+
throw new Error("Failed to create Python venv. Please confirm Python venv support is available.");
|
|
450
|
+
}
|
|
451
451
|
}
|
|
452
452
|
|
|
453
453
|
console.log("Installing/updating Python package in venv: langfuse");
|
|
@@ -456,23 +456,23 @@ function createOrUpdateLangfuseVenv({ baseDir, pipIndexUrl = "https://pypi.tuna.
|
|
|
456
456
|
venvPython,
|
|
457
457
|
["-m", "pip", "install", "-U", "langfuse", "-i", pipIndexUrl],
|
|
458
458
|
{ stdio: "inherit", encoding: "utf8", env: { ...process.env, PYTHONUTF8: "1" } }
|
|
459
|
-
);
|
|
460
|
-
} catch (e) {
|
|
461
|
-
if (process.platform !== "win32") {
|
|
462
|
-
return installLangfuseWithSystemPython({ pythonCmd, baseDir, pipIndexUrl });
|
|
463
|
-
}
|
|
464
|
-
throw new Error(`Failed to install langfuse in venv: ${venvPython} -m pip install -U langfuse -i ${pipIndexUrl}`);
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
if (!pythonCanImport(venvPython, "langfuse")) {
|
|
468
|
-
if (process.platform !== "win32") {
|
|
469
|
-
return installLangfuseWithSystemPython({ pythonCmd, baseDir, pipIndexUrl });
|
|
470
|
-
}
|
|
471
|
-
throw new Error(`langfuse was installed in venv, but cannot be imported by ${venvPython}`);
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
return { python: venvPython, pythonPath: "" };
|
|
475
|
-
}
|
|
459
|
+
);
|
|
460
|
+
} catch (e) {
|
|
461
|
+
if (process.platform !== "win32") {
|
|
462
|
+
return installLangfuseWithSystemPython({ pythonCmd, baseDir, pipIndexUrl });
|
|
463
|
+
}
|
|
464
|
+
throw new Error(`Failed to install langfuse in venv: ${venvPython} -m pip install -U langfuse -i ${pipIndexUrl}`);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (!pythonCanImport(venvPython, "langfuse")) {
|
|
468
|
+
if (process.platform !== "win32") {
|
|
469
|
+
return installLangfuseWithSystemPython({ pythonCmd, baseDir, pipIndexUrl });
|
|
470
|
+
}
|
|
471
|
+
throw new Error(`langfuse was installed in venv, but cannot be imported by ${venvPython}`);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return { python: venvPython, pythonPath: "" };
|
|
475
|
+
}
|
|
476
476
|
|
|
477
477
|
async function main() {
|
|
478
478
|
const args = parseArgs(process.argv.slice(2));
|
|
@@ -483,14 +483,14 @@ async function main() {
|
|
|
483
483
|
}
|
|
484
484
|
assertValidUserId(userId);
|
|
485
485
|
|
|
486
|
-
const langfuseHost = normalizeLegacyBaseUrl(
|
|
487
|
-
args.langfuseBaseUrl ||
|
|
488
|
-
args.langfuseHost ||
|
|
489
|
-
args.host ||
|
|
490
|
-
process.env.LANGFUSE_BASEURL ||
|
|
491
|
-
process.env.LANGFUSE_HOST ||
|
|
492
|
-
DEFAULT_LANGFUSE_BASE_URL
|
|
493
|
-
);
|
|
486
|
+
const langfuseHost = normalizeLegacyBaseUrl(
|
|
487
|
+
args.langfuseBaseUrl ||
|
|
488
|
+
args.langfuseHost ||
|
|
489
|
+
args.host ||
|
|
490
|
+
process.env.LANGFUSE_BASEURL ||
|
|
491
|
+
process.env.LANGFUSE_HOST ||
|
|
492
|
+
DEFAULT_LANGFUSE_BASE_URL
|
|
493
|
+
);
|
|
494
494
|
|
|
495
495
|
const publicKey =
|
|
496
496
|
args.publicKey || process.env.LANGFUSE_PUBLIC_KEY || "pk-lf-da0c90a7-6e93-4eb7-bb86-c1047c8d187d";
|
|
@@ -554,9 +554,9 @@ async function main() {
|
|
|
554
554
|
if (nextPyText !== pyText) {
|
|
555
555
|
await fsp.writeFile(pyPath, nextPyText, "utf8");
|
|
556
556
|
}
|
|
557
|
-
const hookRuntime = createOrUpdateLangfuseVenv({ baseDir: claudeDir, pipIndexUrl });
|
|
558
|
-
const hookPython = hookRuntime.python;
|
|
559
|
-
const hookLauncher = await createHookLauncher({ hooksDir, hookPython, hookPythonPath: hookRuntime.pythonPath, pyPath });
|
|
557
|
+
const hookRuntime = createOrUpdateLangfuseVenv({ baseDir: claudeDir, pipIndexUrl });
|
|
558
|
+
const hookPython = hookRuntime.python;
|
|
559
|
+
const hookLauncher = await createHookLauncher({ hooksDir, hookPython, hookPythonPath: hookRuntime.pythonPath, pyPath });
|
|
560
560
|
const claudeShimDir = path.join(claudeDir, "bin");
|
|
561
561
|
const realClaudeCli = resolveAgentCli({ target: "claude", preferred: args.cmd || "", shimDir: claudeShimDir });
|
|
562
562
|
const directShim = writeAgentCommandShim({
|