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.
- package/SELF_VERIFY.md +25 -25
- package/bin/cli.js +23 -14
- 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 +127 -116
- package/scripts/json-utils.mjs +99 -99
- package/scripts/langfuse-check.mjs +50 -50
- package/scripts/langfuse-setup.mjs +139 -128
- package/scripts/opencode-langfuse-check.mjs +208 -199
- package/scripts/opencode-langfuse-run.mjs +12 -1
- package/scripts/opencode-langfuse-setup.mjs +88 -6
- package/scripts/opencode-skills-discover.mjs +125 -0
- package/scripts/real-self-verify.mjs +97 -85
- package/scripts/resolve-opencode-cli.mjs +53 -53
- package/scripts/update-langfuse-runtime.mjs +39 -22
- package/setup-langfuse.bat +19 -19
- package/setup-langfuse.sh +19 -19
|
@@ -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
|
-
|
|
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({
|
package/scripts/json-utils.mjs
CHANGED
|
@@ -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
|
|