oh-langfuse 0.1.60 → 0.1.62

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,12 +39,12 @@ function parseArgs(argv) {
39
39
  return args;
40
40
  }
41
41
 
42
- async function latestVersion(packageName, registry = "https://registry.npmjs.org") {
43
- const base = registry.replace(/\/+$/, "");
44
- const response = await fetch(`${base}/${packageName}`, { headers: { accept: "application/json" } });
45
- if (!response.ok) throw new Error(`npm registry ${response.status} ${response.statusText}`);
46
- return extractVersionFromNpmMetadata(await response.json());
47
- }
42
+ async function latestVersion(packageName, registry = "https://registry.npmjs.org") {
43
+ const base = registry.replace(/\/+$/, "");
44
+ const response = await fetch(`${base}/${packageName}`, { headers: { accept: "application/json" } });
45
+ if (!response.ok) throw new Error(`npm registry ${response.status} ${response.statusText}`);
46
+ return extractVersionFromNpmMetadata(await response.json());
47
+ }
48
48
 
49
49
  function question(text) {
50
50
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
@@ -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,26 +100,26 @@ 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, packageName, version) {
102
- const label = targetLabel(target);
103
- console.log(paint(`[OK] ${label} Langfuse \u5df2\u662f\u6700\u65b0\uff1a${packageName}@${version}`, t.bold, t.green));
104
- }
105
-
106
- function updateCommandForPackage(packageName, target) {
107
- if (packageName === "oh-aicoding-tool") return `npx oh-aicoding-tool@latest langfuse update ${target}`;
108
- return `npx ${packageName}@latest update ${target}`;
109
- }
110
-
111
- function updateArgsForPackage(packageName, target) {
112
- if (packageName === "oh-aicoding-tool") return ["-y", "oh-aicoding-tool@latest", "langfuse", "update", target];
113
- return ["-y", `${packageName}@latest`, "update", target];
114
- }
115
-
116
- function runUpdate(target, packageName, args) {
117
- const updateArgs = updateArgsForPackage(packageName, target);
118
- if (args["skip-check"]) updateArgs.push("--skip-check");
119
- if (args.npmRegistry) updateArgs.push(`--npmRegistry=${args.npmRegistry}`);
120
- if (args.pipIndexUrl) updateArgs.push(`--pipIndexUrl=${args.pipIndexUrl}`);
103
+ function printAlreadyCurrent(target, packageName, version) {
104
+ const label = targetLabel(target);
105
+ console.log(paint(`[OK] ${label} Langfuse \u5df2\u662f\u6700\u65b0\uff1a${packageName}@${version}`, t.bold, t.green));
106
+ }
107
+
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);
120
+ if (args["skip-check"]) updateArgs.push("--skip-check");
121
+ if (args.npmRegistry) updateArgs.push(`--npmRegistry=${args.npmRegistry}`);
122
+ if (args.pipIndexUrl) updateArgs.push(`--pipIndexUrl=${args.pipIndexUrl}`);
121
123
  const result = runNpx(updateArgs, {
122
124
  stdio: "inherit",
123
125
  timeout: 900000,
@@ -143,27 +145,27 @@ async function main() {
143
145
  throw new Error("Usage: oh-langfuse auto-update <claude|opencode|codex>");
144
146
  }
145
147
 
146
- const record = getRuntimeInstallRecord(target);
147
- const packageName = record?.packageName || packageJson.name;
148
- let latest;
149
- try {
150
- latest = await latestVersion(packageName, args.npmRegistry);
151
- } catch (error) {
152
- if (args.verbose) console.log(`[INFO] ${packageName} update check skipped: ${error.message}`);
153
- return 0;
154
- }
155
-
156
- const installedVersion = record?.packageVersion || record?.version || "";
157
- const needsUpdate = installedVersion ? isNewerVersion(latest, installedVersion) : isNewerVersion(latest, packageJson.version);
158
- if (!needsUpdate) {
159
- if (shouldPrintStartupStatus(args)) printAlreadyCurrent(target, packageName, installedVersion || packageJson.version);
160
- return 0;
161
- }
162
-
163
- const message = installedVersion
164
- ? `${packageName} ${target} runtime update available: ${installedVersion} -> ${latest}.`
165
- : `${packageName} ${target} runtime may need update. Latest package: ${latest}.`;
166
- const updateCommand = updateCommandForPackage(packageName, target);
148
+ const record = getRuntimeInstallRecord(target);
149
+ const packageName = record?.packageName || packageJson.name;
150
+ let latest;
151
+ try {
152
+ latest = await latestVersion(packageName, args.npmRegistry);
153
+ } catch (error) {
154
+ if (args.verbose) console.log(`[INFO] ${packageName} update check skipped: ${error.message}`);
155
+ return 0;
156
+ }
157
+
158
+ const installedVersion = record?.packageVersion || record?.version || "";
159
+ const needsUpdate = installedVersion ? isNewerVersion(latest, installedVersion) : isNewerVersion(latest, packageJson.version);
160
+ if (!needsUpdate) {
161
+ if (shouldPrintStartupStatus(args)) printAlreadyCurrent(target, packageName, installedVersion || packageJson.version);
162
+ return 0;
163
+ }
164
+
165
+ const message = installedVersion
166
+ ? `${packageName} ${target} runtime update available: ${installedVersion} -> ${latest}.`
167
+ : `${packageName} ${target} runtime may need update. Latest package: ${latest}.`;
168
+ const updateCommand = updateCommandForPackage(packageName, target);
167
169
 
168
170
  if (mode === "notify") {
169
171
  printUpdateAvailable(target, message, updateCommand);
@@ -172,9 +174,9 @@ async function main() {
172
174
  }
173
175
 
174
176
  if (mode === "auto") {
175
- printUpdateAvailable(target, message, updateCommand);
176
- printUpdateCommand(target, updateCommand);
177
- const code = runUpdate(target, packageName, args);
177
+ printUpdateAvailable(target, message, updateCommand);
178
+ printUpdateCommand(target, updateCommand);
179
+ const code = runUpdate(target, packageName, args);
178
180
  return args.strict ? code : 0;
179
181
  }
180
182
 
@@ -184,10 +186,10 @@ async function main() {
184
186
  }
185
187
 
186
188
  printUpdateAvailable(target, message, updateCommand);
187
- const answer = await question(`${paint("[CONFIRM]", t.bold, t.gold)} Update now? (y/N) `);
188
- if (!/^(y|yes)$/i.test(answer)) return 0;
189
- printUpdateCommand(target, updateCommand);
190
- const code = runUpdate(target, packageName, args);
189
+ const answer = await question(`${paint("[CONFIRM]", t.bold, t.gold)} Update now? (y/N) `);
190
+ if (!/^(y|yes)$/i.test(answer)) return 0;
191
+ printUpdateCommand(target, updateCommand);
192
+ const code = runUpdate(target, packageName, args);
191
193
  return args.strict ? code : 0;
192
194
  }
193
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() {