oh-langfuse 0.1.59 → 0.1.61

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.
@@ -39,9 +39,9 @@ function parseArgs(argv) {
39
39
  return args;
40
40
  }
41
41
 
42
- async function latestVersion(registry = "https://registry.npmjs.org") {
42
+ async function latestVersion(packageName, registry = "https://registry.npmjs.org") {
43
43
  const base = registry.replace(/\/+$/, "");
44
- const response = await fetch(`${base}/oh-langfuse`, { headers: { accept: "application/json" } });
44
+ const response = await fetch(`${base}/${packageName}`, { headers: { accept: "application/json" } });
45
45
  if (!response.ok) throw new Error(`npm registry ${response.status} ${response.statusText}`);
46
46
  return extractVersionFromNpmMetadata(await response.json());
47
47
  }
@@ -65,11 +65,13 @@ function runNpx(args, options = {}) {
65
65
  return spawnSync("cmd.exe", ["/d", "/s", "/c", npxCommand(), ...args], {
66
66
  ...options,
67
67
  windowsHide: true,
68
+ encoding: "utf8",
68
69
  });
69
70
  }
70
71
  return spawnSync(npxCommand(), args, {
71
72
  ...options,
72
73
  windowsHide: true,
74
+ encoding: "utf8",
73
75
  });
74
76
  }
75
77
 
@@ -98,13 +100,23 @@ function shouldPrintStartupStatus(args, env = process.env) {
98
100
  return !!(args["startup-status"] || args.startupStatus || /^(1|true|yes|on)$/i.test(raw));
99
101
  }
100
102
 
101
- function printAlreadyCurrent(target, version) {
103
+ function printAlreadyCurrent(target, packageName, version) {
102
104
  const label = targetLabel(target);
103
- console.log(paint(`[OK] ${label} Langfuse \u5df2\u662f\u6700\u65b0\uff1a${packageJson.name}@${version}`, t.bold, t.green));
105
+ console.log(paint(`[OK] ${label} Langfuse \u5df2\u662f\u6700\u65b0\uff1a${packageName}@${version}`, t.bold, t.green));
104
106
  }
105
107
 
106
- function runUpdate(target, args) {
107
- const updateArgs = ["-y", "oh-langfuse@latest", "update", target];
108
+ function updateCommandForPackage(packageName, target) {
109
+ if (packageName === "oh-aicoding-tool") return `npx oh-aicoding-tool@latest langfuse update ${target}`;
110
+ return `npx ${packageName}@latest update ${target}`;
111
+ }
112
+
113
+ function updateArgsForPackage(packageName, target) {
114
+ if (packageName === "oh-aicoding-tool") return ["-y", "oh-aicoding-tool@latest", "langfuse", "update", target];
115
+ return ["-y", `${packageName}@latest`, "update", target];
116
+ }
117
+
118
+ function runUpdate(target, packageName, args) {
119
+ const updateArgs = updateArgsForPackage(packageName, target);
108
120
  if (args["skip-check"]) updateArgs.push("--skip-check");
109
121
  if (args.npmRegistry) updateArgs.push(`--npmRegistry=${args.npmRegistry}`);
110
122
  if (args.pipIndexUrl) updateArgs.push(`--pipIndexUrl=${args.pipIndexUrl}`);
@@ -133,26 +145,27 @@ async function main() {
133
145
  throw new Error("Usage: oh-langfuse auto-update <claude|opencode|codex>");
134
146
  }
135
147
 
148
+ const record = getRuntimeInstallRecord(target);
149
+ const packageName = record?.packageName || packageJson.name;
136
150
  let latest;
137
151
  try {
138
- latest = await latestVersion(args.npmRegistry);
152
+ latest = await latestVersion(packageName, args.npmRegistry);
139
153
  } catch (error) {
140
- if (args.verbose) console.log(`[INFO] oh-langfuse update check skipped: ${error.message}`);
154
+ if (args.verbose) console.log(`[INFO] ${packageName} update check skipped: ${error.message}`);
141
155
  return 0;
142
156
  }
143
157
 
144
- const record = getRuntimeInstallRecord(target);
145
158
  const installedVersion = record?.packageVersion || record?.version || "";
146
159
  const needsUpdate = installedVersion ? isNewerVersion(latest, installedVersion) : isNewerVersion(latest, packageJson.version);
147
160
  if (!needsUpdate) {
148
- if (shouldPrintStartupStatus(args)) printAlreadyCurrent(target, installedVersion || packageJson.version);
161
+ if (shouldPrintStartupStatus(args)) printAlreadyCurrent(target, packageName, installedVersion || packageJson.version);
149
162
  return 0;
150
163
  }
151
164
 
152
165
  const message = installedVersion
153
- ? `oh-langfuse ${target} runtime update available: ${installedVersion} -> ${latest}.`
154
- : `oh-langfuse ${target} runtime may need update. Latest package: ${latest}.`;
155
- const updateCommand = `npx oh-langfuse@latest update ${target}`;
166
+ ? `${packageName} ${target} runtime update available: ${installedVersion} -> ${latest}.`
167
+ : `${packageName} ${target} runtime may need update. Latest package: ${latest}.`;
168
+ const updateCommand = updateCommandForPackage(packageName, target);
156
169
 
157
170
  if (mode === "notify") {
158
171
  printUpdateAvailable(target, message, updateCommand);
@@ -163,7 +176,7 @@ async function main() {
163
176
  if (mode === "auto") {
164
177
  printUpdateAvailable(target, message, updateCommand);
165
178
  printUpdateCommand(target, updateCommand);
166
- const code = runUpdate(target, args);
179
+ const code = runUpdate(target, packageName, args);
167
180
  return args.strict ? code : 0;
168
181
  }
169
182
 
@@ -176,7 +189,7 @@ async function main() {
176
189
  const answer = await question(`${paint("[CONFIRM]", t.bold, t.gold)} Update now? (y/N) `);
177
190
  if (!/^(y|yes)$/i.test(answer)) return 0;
178
191
  printUpdateCommand(target, updateCommand);
179
- const code = runUpdate(target, args);
192
+ const code = runUpdate(target, packageName, args);
180
193
  return args.strict ? code : 0;
181
194
  }
182
195
 
@@ -52,61 +52,63 @@ function writeJsonPretty(p, obj) {
52
52
  }
53
53
 
54
54
  function tomlString(s) {
55
- return JSON.stringify(String(s));
55
+ const str = String(s);
56
+ const escaped = str.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
57
+ return `"${escaped}"`;
56
58
  }
57
59
 
58
60
  function tomlArray(items) {
59
61
  return `[${items.map(tomlString).join(", ")}]`;
60
62
  }
61
63
 
62
- function pythonExecutableInVenv(venvDir) {
63
- return process.platform === "win32"
64
- ? path.join(venvDir, "Scripts", "python.exe")
65
- : path.join(venvDir, "bin", "python");
66
- }
67
-
68
- function pythonCanImport(pythonCmd, moduleName) {
69
- try {
70
- execFileSync(pythonCmd, ["-c", `import ${moduleName}`], { stdio: "ignore" });
71
- return true;
72
- } catch {
73
- return false;
74
- }
75
- }
76
-
77
- function runPipInstallWithFallback({ pythonCmd, pipIndexUrl }) {
78
- const attempts = [
79
- { command: pythonCmd, args: ["-m", "pip", "install", "--user", "-U", "langfuse", "-i", pipIndexUrl] },
80
- { command: "pip3", args: ["install", "--user", "-U", "langfuse", "-i", pipIndexUrl] },
81
- { command: "pip", args: ["install", "--user", "-U", "langfuse", "-i", pipIndexUrl] }
82
- ];
83
-
84
- const errors = [];
85
- for (const attempt of attempts) {
86
- try {
87
- console.log(`Trying Python package install: ${attempt.command} ${attempt.args.join(" ")}`);
88
- execFileSync(attempt.command, attempt.args, { stdio: "inherit" });
89
- return;
90
- } catch (error) {
91
- errors.push(`${attempt.command}: ${error?.message || error}`);
92
- }
93
- }
94
-
95
- throw new Error(
96
- `Failed to install langfuse with system Python/pip. Tried python -m pip, pip3, and pip. Last errors: ${errors.join(" | ")}`
97
- );
98
- }
99
-
100
- function installLangfuseWithSystemPython({ pythonCmd, pipIndexUrl }) {
101
- console.log("Python venv is unavailable; falling back to system Python user install for langfuse.");
102
- runPipInstallWithFallback({ pythonCmd, pipIndexUrl });
103
- if (!pythonCanImport(pythonCmd, "langfuse")) {
104
- throw new Error("langfuse was installed with pip, but python3 still cannot import it. Install python3-venv and rerun setup, for example: sudo apt install python3-venv");
105
- }
106
- return pythonCmd;
107
- }
108
-
109
- function shQuote(s) {
64
+ function pythonExecutableInVenv(venvDir) {
65
+ return process.platform === "win32"
66
+ ? path.join(venvDir, "Scripts", "python.exe")
67
+ : path.join(venvDir, "bin", "python");
68
+ }
69
+
70
+ function pythonCanImport(pythonCmd, moduleName) {
71
+ try {
72
+ execFileSync(pythonCmd, ["-c", `import ${moduleName}`], { stdio: "ignore", encoding: "utf8", env: { ...process.env, PYTHONUTF8: "1" } });
73
+ return true;
74
+ } catch {
75
+ return false;
76
+ }
77
+ }
78
+
79
+ function runPipInstallWithFallback({ pythonCmd, pipIndexUrl }) {
80
+ const attempts = [
81
+ { command: pythonCmd, args: ["-m", "pip", "install", "--user", "-U", "langfuse", "-i", pipIndexUrl] },
82
+ { command: "pip3", args: ["install", "--user", "-U", "langfuse", "-i", pipIndexUrl] },
83
+ { command: "pip", args: ["install", "--user", "-U", "langfuse", "-i", pipIndexUrl] }
84
+ ];
85
+
86
+ const errors = [];
87
+ for (const attempt of attempts) {
88
+ try {
89
+ console.log(`Trying Python package install: ${attempt.command} ${attempt.args.join(" ")}`);
90
+ execFileSync(attempt.command, attempt.args, { stdio: "inherit", encoding: "utf8", env: { ...process.env, PYTHONUTF8: "1" } });
91
+ return;
92
+ } catch (error) {
93
+ errors.push(`${attempt.command}: ${error?.message || error}`);
94
+ }
95
+ }
96
+
97
+ throw new Error(
98
+ `Failed to install langfuse with system Python/pip. Tried python -m pip, pip3, and pip. Last errors: ${errors.join(" | ")}`
99
+ );
100
+ }
101
+
102
+ function installLangfuseWithSystemPython({ pythonCmd, pipIndexUrl }) {
103
+ console.log("Python venv is unavailable; falling back to system Python user install for langfuse.");
104
+ runPipInstallWithFallback({ pythonCmd, pipIndexUrl });
105
+ if (!pythonCanImport(pythonCmd, "langfuse")) {
106
+ throw new Error("langfuse was installed with pip, but python3 still cannot import it. Install python3-venv and rerun setup, for example: sudo apt install python3-venv");
107
+ }
108
+ return pythonCmd;
109
+ }
110
+
111
+ function shQuote(s) {
110
112
  return `'${String(s).replace(/'/g, "'\"'\"'")}'`;
111
113
  }
112
114
 
@@ -145,7 +147,7 @@ function writeAutoUpdateHelper(target) {
145
147
  "exit /b 0",
146
148
  ""
147
149
  ];
148
- fs.writeFileSync(helper, lines.join(os.EOL), "utf8");
150
+ fs.writeFileSync(helper, "\ufeff" + lines.join(os.EOL), "utf8");
149
151
  return helper;
150
152
  }
151
153
 
@@ -181,44 +183,44 @@ function isPathInsideDir(candidate, dir) {
181
183
  return relative === "" || (relative && !relative.startsWith("..") && !path.isAbsolute(relative));
182
184
  }
183
185
 
184
- function existingCliCandidate(candidate, shimDir) {
185
- if (!candidate || isPathInsideDir(candidate, shimDir)) return "";
186
- if (process.platform === "win32" && isWindowsAppsPath(candidate)) return "";
187
- try {
188
- return fs.existsSync(candidate) ? candidate : "";
189
- } catch {
190
- return "";
191
- }
192
- }
193
-
194
- function isWindowsAppsPath(candidate) {
195
- return String(candidate || "").toLowerCase().includes("\\program files\\windowsapps\\");
196
- }
197
-
198
- function listLocalCodexCliCandidates() {
199
- if (process.platform !== "win32") return [];
200
- const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local");
201
- const codexBin = path.join(localAppData, "OpenAI", "Codex", "bin");
202
- const candidates = [
203
- process.env.CODEX_CLI_PATH || "",
204
- path.join(codexBin, "codex.exe"),
205
- ];
206
- try {
207
- if (fs.existsSync(codexBin)) {
208
- for (const ent of fs.readdirSync(codexBin, { withFileTypes: true })) {
209
- if (ent.isDirectory()) candidates.push(path.join(codexBin, ent.name, "codex.exe"));
210
- }
211
- }
212
- } catch {
213
- // Best effort only; PATH candidates are still checked below.
214
- }
215
- return candidates;
216
- }
217
-
218
- function listCliCandidatesFromPath(target) {
219
- const cmd = process.platform === "win32" ? "where.exe" : "which";
220
- const args = process.platform === "win32" ? [target] : ["-a", target];
221
- const result = spawnSync(cmd, args, { encoding: "utf8", windowsHide: true });
186
+ function existingCliCandidate(candidate, shimDir) {
187
+ if (!candidate || isPathInsideDir(candidate, shimDir)) return "";
188
+ if (process.platform === "win32" && isWindowsAppsPath(candidate)) return "";
189
+ try {
190
+ return fs.existsSync(candidate) ? candidate : "";
191
+ } catch {
192
+ return "";
193
+ }
194
+ }
195
+
196
+ function isWindowsAppsPath(candidate) {
197
+ return String(candidate || "").toLowerCase().includes("\\program files\\windowsapps\\");
198
+ }
199
+
200
+ function listLocalCodexCliCandidates() {
201
+ if (process.platform !== "win32") return [];
202
+ const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local");
203
+ const codexBin = path.join(localAppData, "OpenAI", "Codex", "bin");
204
+ const candidates = [
205
+ process.env.CODEX_CLI_PATH || "",
206
+ path.join(codexBin, "codex.exe"),
207
+ ];
208
+ try {
209
+ if (fs.existsSync(codexBin)) {
210
+ for (const ent of fs.readdirSync(codexBin, { withFileTypes: true })) {
211
+ if (ent.isDirectory()) candidates.push(path.join(codexBin, ent.name, "codex.exe"));
212
+ }
213
+ }
214
+ } catch {
215
+ // Best effort only; PATH candidates are still checked below.
216
+ }
217
+ return candidates;
218
+ }
219
+
220
+ function listCliCandidatesFromPath(target) {
221
+ const cmd = process.platform === "win32" ? "where.exe" : "which";
222
+ const args = process.platform === "win32" ? [target] : ["-a", target];
223
+ const result = spawnSync(cmd, args, { encoding: "utf8", windowsHide: true });
222
224
  if (result.status !== 0) return [];
223
225
  const candidates = String(result.stdout || "")
224
226
  .split(/\r?\n/)
@@ -236,14 +238,14 @@ function sortWindowsCliCandidates(candidates) {
236
238
  return [...candidates].sort((a, b) => extPriority(a) - extPriority(b));
237
239
  }
238
240
 
239
- function resolveAgentCli({ target, preferred = "", shimDir = "" }) {
240
- const appData = process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming");
241
- const candidates = [
242
- preferred,
243
- ...(target === "codex" ? listLocalCodexCliCandidates() : []),
244
- process.platform === "win32" ? path.join(appData, "npm", `${target}.cmd`) : "",
245
- process.platform === "win32" ? path.join(appData, "npm", `${target}.exe`) : "",
246
- process.platform === "win32" ? path.join(appData, "npm", target) : "",
241
+ function resolveAgentCli({ target, preferred = "", shimDir = "" }) {
242
+ const appData = process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming");
243
+ const candidates = [
244
+ preferred,
245
+ ...(target === "codex" ? listLocalCodexCliCandidates() : []),
246
+ process.platform === "win32" ? path.join(appData, "npm", `${target}.cmd`) : "",
247
+ process.platform === "win32" ? path.join(appData, "npm", `${target}.exe`) : "",
248
+ process.platform === "win32" ? path.join(appData, "npm", target) : "",
247
249
  ...listCliCandidatesFromPath(target)
248
250
  ];
249
251
  for (const candidate of candidates) {
@@ -268,7 +270,7 @@ function prependWindowsUserPath(dir) {
268
270
  " [Environment]::SetEnvironmentVariable('Path', $next, 'User');",
269
271
  "}"
270
272
  ].join(" ");
271
- const result = spawnSync("powershell", ["-NoProfile", "-Command", cmd], { stdio: "inherit", windowsHide: true });
273
+ const result = spawnSync("powershell", ["-NoProfile", "-Command", cmd], { stdio: "inherit", windowsHide: true, encoding: "utf8" });
272
274
  if (result.status !== 0) throw new Error("Failed to prepend Codex Langfuse shim to the user PATH.");
273
275
 
274
276
  const currentParts = String(process.env.PATH || "").split(path.delimiter);
@@ -299,7 +301,7 @@ function writeAgentCommandShim({ baseDir, target, executable, realCli, publicKey
299
301
  "exit /b %ERRORLEVEL%",
300
302
  ""
301
303
  ].filter(Boolean).join(os.EOL);
302
- fs.writeFileSync(shim, content, "utf8");
304
+ fs.writeFileSync(shim, "\ufeff" + content, "utf8");
303
305
  return { shim, shimDir };
304
306
  }
305
307
 
@@ -331,7 +333,7 @@ function createAgentLauncher({ baseDir, target, executable }) {
331
333
  `${executable} %*`,
332
334
  ""
333
335
  ].join(os.EOL);
334
- fs.writeFileSync(launcher, content, "utf8");
336
+ fs.writeFileSync(launcher, "\ufeff" + content, "utf8");
335
337
  return launcher;
336
338
  }
337
339
 
@@ -355,14 +357,14 @@ function createOrUpdateLangfuseVenv({ baseDir, pipIndexUrl = "https://pypi.tuna.
355
357
 
356
358
  if (!fs.existsSync(venvPython)) {
357
359
  console.log(`Creating Python virtual environment: ${venvDir}`);
358
- try {
359
- execFileSync(pythonCmd, ["-m", "venv", venvDir], { stdio: "inherit" });
360
- } catch (e) {
361
- if (process.platform !== "win32") {
362
- return installLangfuseWithSystemPython({ pythonCmd, pipIndexUrl });
363
- }
364
- throw new Error("Failed to create Python venv. Please confirm Python venv support is available.");
365
- }
360
+ try {
361
+ execFileSync(pythonCmd, ["-m", "venv", venvDir], { stdio: "inherit", encoding: "utf8", env: { ...process.env, PYTHONUTF8: "1" } });
362
+ } catch (e) {
363
+ if (process.platform !== "win32") {
364
+ return installLangfuseWithSystemPython({ pythonCmd, pipIndexUrl });
365
+ }
366
+ throw new Error("Failed to create Python venv. Please confirm Python venv support is available.");
367
+ }
366
368
  }
367
369
 
368
370
  console.log("Installing/updating Python package in venv: langfuse");
@@ -370,14 +372,14 @@ function createOrUpdateLangfuseVenv({ baseDir, pipIndexUrl = "https://pypi.tuna.
370
372
  execFileSync(
371
373
  venvPython,
372
374
  ["-m", "pip", "install", "-U", "langfuse", "-i", pipIndexUrl],
373
- { stdio: "inherit" }
374
- );
375
- } catch (e) {
376
- if (process.platform !== "win32") {
377
- return installLangfuseWithSystemPython({ pythonCmd, pipIndexUrl });
378
- }
379
- throw new Error(`Failed to install langfuse in venv: ${venvPython} -m pip install -U langfuse -i ${pipIndexUrl}`);
380
- }
375
+ { stdio: "inherit", encoding: "utf8", env: { ...process.env, PYTHONUTF8: "1" } }
376
+ );
377
+ } catch (e) {
378
+ if (process.platform !== "win32") {
379
+ return installLangfuseWithSystemPython({ pythonCmd, pipIndexUrl });
380
+ }
381
+ throw new Error(`Failed to install langfuse in venv: ${venvPython} -m pip install -U langfuse -i ${pipIndexUrl}`);
382
+ }
381
383
 
382
384
  return venvPython;
383
385
  }
@@ -398,7 +400,8 @@ function updateCodexNotify(configPath, notifyCommand) {
398
400
  const next = [];
399
401
 
400
402
  for (const line of lines) {
401
- if (/^\s*\[/.test(line)) {
403
+ const trimmed = line.trim();
404
+ if (/^\[/.test(trimmed) && !trimmed.startsWith("[[")) {
402
405
  if (!replaced && !inserted) {
403
406
  while (next.length && next[next.length - 1].trim() === "") next.pop();
404
407
  next.push(notifyLine, "");
@@ -419,7 +422,14 @@ function updateCodexNotify(configPath, notifyCommand) {
419
422
  next.push(notifyLine, "");
420
423
  }
421
424
 
422
- fs.writeFileSync(configPath, next.join(os.EOL), "utf8");
425
+ const result = next.join("\n");
426
+ fs.writeFileSync(configPath, result, "utf8");
427
+
428
+ const verifyContent = fs.readFileSync(configPath, "utf8");
429
+ const notifyMatch = verifyContent.match(/^notify\s*=\s*\[[\s\S]*?\]/m);
430
+ if (!notifyMatch) {
431
+ throw new Error(`Failed to write valid notify config to ${configPath}. Generated content:\n${result}`);
432
+ }
423
433
  }
424
434
 
425
435
  async function main() {
@@ -81,7 +81,7 @@ function extractZipWithPowerShell({ zipPath, destDir }) {
81
81
  const cmd = cmdParts.join(" ");
82
82
 
83
83
  try {
84
- execFileSync("powershell", ["-NoProfile", "-Command", cmd], { stdio: "inherit" });
84
+ execFileSync("powershell", ["-NoProfile", "-Command", cmd], { stdio: "inherit", encoding: "utf8" });
85
85
  } catch {
86
86
  throw new Error("解压 hooks zip 失败:请确认 zip 文件存在,且 PowerShell 支持 Expand-Archive。");
87
87
  }
@@ -97,7 +97,7 @@ function downloadAndExtractZipWithPowerShell({ zipUrl, destDir }) {
97
97
  ].join(" ");
98
98
 
99
99
  try {
100
- execFileSync("powershell", ["-NoProfile", "-Command", cmd], { stdio: "inherit" });
100
+ execFileSync("powershell", ["-NoProfile", "-Command", cmd], { stdio: "inherit", encoding: "utf8" });
101
101
  } catch (e) {
102
102
  const msg = e?.message ? String(e.message) : "";
103
103
  if (msg.includes("(401)") || msg.toLowerCase().includes("unauthorized")) {
@@ -176,7 +176,7 @@ function writeAutoUpdateHelper(target) {
176
176
  "exit /b 0",
177
177
  ""
178
178
  ];
179
- fs.writeFileSync(helper, lines.join(os.EOL), "utf8");
179
+ fs.writeFileSync(helper, "\ufeff" + lines.join(os.EOL), "utf8");
180
180
  return helper;
181
181
  }
182
182
 
@@ -263,7 +263,7 @@ function prependWindowsUserPath(dir) {
263
263
  " [Environment]::SetEnvironmentVariable('Path', $next, 'User');",
264
264
  "}"
265
265
  ].join(" ");
266
- const result = spawnSync("powershell", ["-NoProfile", "-Command", cmd], { stdio: "inherit", windowsHide: true });
266
+ const result = spawnSync("powershell", ["-NoProfile", "-Command", cmd], { stdio: "inherit", windowsHide: true, encoding: "utf8" });
267
267
  if (result.status !== 0) throw new Error("Failed to prepend Claude Langfuse shim to the user PATH.");
268
268
 
269
269
  const currentParts = String(process.env.PATH || "").split(path.delimiter);
@@ -294,7 +294,7 @@ function writeAgentCommandShim({ baseDir, target, executable, realCli, publicKey
294
294
  "exit /b %ERRORLEVEL%",
295
295
  ""
296
296
  ].filter(Boolean).join(os.EOL);
297
- fs.writeFileSync(shim, content, "utf8");
297
+ fs.writeFileSync(shim, "\ufeff" + content, "utf8");
298
298
  return { shim, shimDir };
299
299
  }
300
300
 
@@ -326,7 +326,7 @@ function createAgentLauncher({ baseDir, target, executable }) {
326
326
  `${executable} %*`,
327
327
  ""
328
328
  ].join(os.EOL);
329
- fs.writeFileSync(launcher, content, "utf8");
329
+ fs.writeFileSync(launcher, "\ufeff" + content, "utf8");
330
330
  return launcher;
331
331
  }
332
332
 
@@ -343,54 +343,54 @@ function createAgentLauncher({ baseDir, target, executable }) {
343
343
  return launcher;
344
344
  }
345
345
 
346
- function pythonExecutableInVenv(venvDir) {
347
- return process.platform === "win32"
348
- ? path.join(venvDir, "Scripts", "python.exe")
349
- : path.join(venvDir, "bin", "python");
350
- }
351
-
352
- function pythonCanImport(pythonCmd, moduleName) {
353
- try {
354
- execFileSync(pythonCmd, ["-c", `import ${moduleName}`], { stdio: "ignore" });
355
- return true;
356
- } catch {
357
- return false;
358
- }
359
- }
360
-
361
- function runPipInstallWithFallback({ pythonCmd, pipIndexUrl }) {
362
- const attempts = [
363
- { command: pythonCmd, args: ["-m", "pip", "install", "--user", "-U", "langfuse", "-i", pipIndexUrl] },
364
- { command: "pip3", args: ["install", "--user", "-U", "langfuse", "-i", pipIndexUrl] },
365
- { command: "pip", args: ["install", "--user", "-U", "langfuse", "-i", pipIndexUrl] }
366
- ];
367
-
368
- const errors = [];
369
- for (const attempt of attempts) {
370
- try {
371
- console.log(`Trying Python package install: ${attempt.command} ${attempt.args.join(" ")}`);
372
- execFileSync(attempt.command, attempt.args, { stdio: "inherit" });
373
- return;
374
- } catch (error) {
375
- errors.push(`${attempt.command}: ${error?.message || error}`);
376
- }
377
- }
378
-
379
- throw new Error(
380
- `Failed to install langfuse with system Python/pip. Tried python -m pip, pip3, and pip. Last errors: ${errors.join(" | ")}`
381
- );
382
- }
383
-
384
- function installLangfuseWithSystemPython({ pythonCmd, pipIndexUrl }) {
385
- console.log("Python venv is unavailable; falling back to system Python user install for langfuse.");
386
- runPipInstallWithFallback({ pythonCmd, pipIndexUrl });
387
- if (!pythonCanImport(pythonCmd, "langfuse")) {
388
- throw new Error("langfuse was installed with pip, but python3 still cannot import it. Install python3-venv and rerun setup, for example: sudo apt install python3-venv");
389
- }
390
- return pythonCmd;
391
- }
392
-
393
- async function createHookLauncher({ hooksDir, hookPython, pyPath }) {
346
+ function pythonExecutableInVenv(venvDir) {
347
+ return process.platform === "win32"
348
+ ? path.join(venvDir, "Scripts", "python.exe")
349
+ : path.join(venvDir, "bin", "python");
350
+ }
351
+
352
+ function pythonCanImport(pythonCmd, moduleName) {
353
+ try {
354
+ execFileSync(pythonCmd, ["-c", `import ${moduleName}`], { stdio: "ignore", encoding: "utf8", env: { ...process.env, PYTHONUTF8: "1" } });
355
+ return true;
356
+ } catch {
357
+ return false;
358
+ }
359
+ }
360
+
361
+ function runPipInstallWithFallback({ pythonCmd, pipIndexUrl }) {
362
+ const attempts = [
363
+ { command: pythonCmd, args: ["-m", "pip", "install", "--user", "-U", "langfuse", "-i", pipIndexUrl] },
364
+ { command: "pip3", args: ["install", "--user", "-U", "langfuse", "-i", pipIndexUrl] },
365
+ { command: "pip", args: ["install", "--user", "-U", "langfuse", "-i", pipIndexUrl] }
366
+ ];
367
+
368
+ const errors = [];
369
+ for (const attempt of attempts) {
370
+ try {
371
+ console.log(`Trying Python package install: ${attempt.command} ${attempt.args.join(" ")}`);
372
+ execFileSync(attempt.command, attempt.args, { stdio: "inherit", encoding: "utf8", env: { ...process.env, PYTHONUTF8: "1" } });
373
+ return;
374
+ } catch (error) {
375
+ errors.push(`${attempt.command}: ${error?.message || error}`);
376
+ }
377
+ }
378
+
379
+ throw new Error(
380
+ `Failed to install langfuse with system Python/pip. Tried python -m pip, pip3, and pip. Last errors: ${errors.join(" | ")}`
381
+ );
382
+ }
383
+
384
+ function installLangfuseWithSystemPython({ pythonCmd, pipIndexUrl }) {
385
+ console.log("Python venv is unavailable; falling back to system Python user install for langfuse.");
386
+ runPipInstallWithFallback({ pythonCmd, pipIndexUrl });
387
+ if (!pythonCanImport(pythonCmd, "langfuse")) {
388
+ throw new Error("langfuse was installed with pip, but python3 still cannot import it. Install python3-venv and rerun setup, for example: sudo apt install python3-venv");
389
+ }
390
+ return pythonCmd;
391
+ }
392
+
393
+ async function createHookLauncher({ hooksDir, hookPython, pyPath }) {
394
394
  if (process.platform === "win32") {
395
395
  const launcher = path.join(hooksDir, "run-langfuse-hook.cmd");
396
396
  const content = [
@@ -398,7 +398,7 @@ async function createHookLauncher({ hooksDir, hookPython, pyPath }) {
398
398
  `${cmdQuote(hookPython)} ${cmdQuote(pyPath)}`,
399
399
  ""
400
400
  ].join(os.EOL);
401
- await fsp.writeFile(launcher, content, "utf8");
401
+ await fsp.writeFile(launcher, "\ufeff" + content, "utf8");
402
402
  return launcher;
403
403
  }
404
404
 
@@ -420,14 +420,14 @@ function createOrUpdateLangfuseVenv({ baseDir, pipIndexUrl = "https://pypi.tuna.
420
420
 
421
421
  if (!fs.existsSync(venvPython)) {
422
422
  console.log(`Creating Python virtual environment: ${venvDir}`);
423
- try {
424
- execFileSync(pythonCmd, ["-m", "venv", venvDir], { stdio: "inherit" });
425
- } catch (e) {
426
- if (process.platform !== "win32") {
427
- return installLangfuseWithSystemPython({ pythonCmd, pipIndexUrl });
428
- }
429
- throw new Error("Failed to create Python venv. Please confirm Python venv support is available.");
430
- }
423
+ try {
424
+ execFileSync(pythonCmd, ["-m", "venv", venvDir], { stdio: "inherit", encoding: "utf8", env: { ...process.env, PYTHONUTF8: "1" } });
425
+ } catch (e) {
426
+ if (process.platform !== "win32") {
427
+ return installLangfuseWithSystemPython({ pythonCmd, pipIndexUrl });
428
+ }
429
+ throw new Error("Failed to create Python venv. Please confirm Python venv support is available.");
430
+ }
431
431
  }
432
432
 
433
433
  console.log("Installing/updating Python package in venv: langfuse");
@@ -435,14 +435,14 @@ function createOrUpdateLangfuseVenv({ baseDir, pipIndexUrl = "https://pypi.tuna.
435
435
  execFileSync(
436
436
  venvPython,
437
437
  ["-m", "pip", "install", "-U", "langfuse", "-i", pipIndexUrl],
438
- { stdio: "inherit" }
439
- );
440
- } catch (e) {
441
- if (process.platform !== "win32") {
442
- return installLangfuseWithSystemPython({ pythonCmd, pipIndexUrl });
443
- }
444
- throw new Error(`Failed to install langfuse in venv: ${venvPython} -m pip install -U langfuse -i ${pipIndexUrl}`);
445
- }
438
+ { stdio: "inherit", encoding: "utf8", env: { ...process.env, PYTHONUTF8: "1" } }
439
+ );
440
+ } catch (e) {
441
+ if (process.platform !== "win32") {
442
+ return installLangfuseWithSystemPython({ pythonCmd, pipIndexUrl });
443
+ }
444
+ throw new Error(`Failed to install langfuse in venv: ${venvPython} -m pip install -U langfuse -i ${pipIndexUrl}`);
445
+ }
446
446
 
447
447
  return venvPython;
448
448
  }