oh-langfuse 0.1.53 → 0.1.55

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.
@@ -1,24 +1,24 @@
1
1
  import fs from "node:fs";
2
2
  import fsp from "node:fs/promises";
3
3
  import path from "node:path";
4
- import os from "node:os";
5
- import { execFileSync, spawnSync } from "node:child_process";
6
- import { fileURLToPath } from "node:url";
7
- import { writeRuntimeInstallRecord } from "./runtime-state-utils.mjs";
8
-
9
- const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
10
- const USER_ID_PATTERN = /^[a-z](?:\d{8}|wx\d{7})$/;
11
- const USER_ID_PATTERN_TEXT = "^[a-z](?:\\d{8}|wx\\d{7})$";
12
-
13
- function normalizeUserId(v) {
14
- return String(v || "").trim();
15
- }
16
-
17
- function assertValidUserId(userId) {
18
- if (!USER_ID_PATTERN.test(normalizeUserId(userId))) {
19
- throw new Error(`工号格式不正确:--userId 必须匹配 ${USER_ID_PATTERN_TEXT},例如 h00613222 或 hwx1234567`);
20
- }
21
- }
4
+ import os from "node:os";
5
+ import { execFileSync, spawnSync } from "node:child_process";
6
+ import { fileURLToPath } from "node:url";
7
+ import { writeRuntimeInstallRecord } from "./runtime-state-utils.mjs";
8
+
9
+ const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
10
+ const USER_ID_PATTERN = /^[a-z](?:\d{8}|wx\d{7})$/;
11
+ const USER_ID_PATTERN_TEXT = "^[a-z](?:\\d{8}|wx\\d{7})$";
12
+
13
+ function normalizeUserId(v) {
14
+ return String(v || "").trim();
15
+ }
16
+
17
+ function assertValidUserId(userId) {
18
+ if (!USER_ID_PATTERN.test(normalizeUserId(userId))) {
19
+ throw new Error(`工号格式不正确:--userId 必须匹配 ${USER_ID_PATTERN_TEXT},例如 h00613222 或 hwx1234567`);
20
+ }
21
+ }
22
22
 
23
23
  function parseArgs(argv) {
24
24
  const args = {};
@@ -65,297 +65,341 @@ function pythonExecutableInVenv(venvDir) {
65
65
  : path.join(venvDir, "bin", "python");
66
66
  }
67
67
 
68
- function shQuote(s) {
69
- return `'${String(s).replace(/'/g, "'\"'\"'")}'`;
70
- }
71
-
72
- function psQuote(s) {
73
- return `'${String(s).replace(/'/g, "''")}'`;
74
- }
75
-
76
- function cmdQuote(s) {
77
- return `"${String(s).replace(/"/g, '""')}"`;
78
- }
79
-
80
- function autoUpdateRuntimePath() {
81
- return path.join(packageRoot, "scripts", "auto-update-runtime.mjs");
82
- }
83
-
84
- function autoUpdateHelperDir() {
85
- return path.join(os.homedir(), ".config", "oh-langfuse", "bin");
86
- }
87
-
88
- function writeAutoUpdateHelper(target) {
89
- const helperDir = autoUpdateHelperDir();
90
- ensureDir(helperDir);
91
- const runtimePath = autoUpdateRuntimePath();
92
- const fallbackArgs = ["-y", "oh-langfuse@latest", "auto-update", target, "--skip-check", "--startup-status"];
93
-
94
- if (process.platform === "win32") {
95
- const helper = path.join(helperDir, `oh-langfuse-auto-update-${target}.cmd`);
96
- const lines = [
97
- "@echo off",
98
- "REM Auto-generated by scripts/codex-langfuse-setup.mjs",
99
- `if exist ${cmdQuote(runtimePath)} (`,
100
- ` call ${cmdQuote(process.execPath)} ${cmdQuote(runtimePath)} ${target} --skip-check --startup-status %*`,
101
- ") else (",
102
- ` call npx.cmd ${fallbackArgs.map(cmdQuote).join(" ")} %*`,
103
- ")",
104
- "exit /b 0",
105
- ""
106
- ];
107
- fs.writeFileSync(helper, lines.join(os.EOL), "utf8");
108
- return helper;
109
- }
110
-
111
- const helper = path.join(helperDir, `oh-langfuse-auto-update-${target}`);
112
- const lines = [
113
- "#!/usr/bin/env sh",
114
- "# Auto-generated by scripts/codex-langfuse-setup.mjs",
115
- "set +e",
116
- `if [ -f ${shQuote(runtimePath)} ]; then`,
117
- ` ${shQuote(process.execPath)} ${shQuote(runtimePath)} ${shQuote(target)} --skip-check --startup-status "$@"`,
118
- "else",
119
- ` npx ${fallbackArgs.map(shQuote).join(" ")} "$@"`,
120
- "fi",
121
- "exit 0",
122
- ""
123
- ];
124
- fs.writeFileSync(helper, lines.join("\n"), "utf8");
125
- fs.chmodSync(helper, 0o755);
126
- return helper;
127
- }
128
-
129
- function windowsAutoUpdateCommand(target) {
130
- return `call ${cmdQuote(writeAutoUpdateHelper(target))}`;
131
- }
132
-
133
- function unixAutoUpdateCommand(target) {
134
- return `${shQuote(writeAutoUpdateHelper(target))} || true`;
135
- }
136
-
137
- function isPathInsideDir(candidate, dir) {
138
- if (!candidate || !dir) return false;
139
- const relative = path.relative(path.resolve(dir), path.resolve(candidate));
140
- return relative === "" || (relative && !relative.startsWith("..") && !path.isAbsolute(relative));
141
- }
142
-
143
- function existingCliCandidate(candidate, shimDir) {
144
- if (!candidate || isPathInsideDir(candidate, shimDir)) return "";
68
+ function pythonCanImport(pythonCmd, moduleName) {
145
69
  try {
146
- return fs.existsSync(candidate) ? candidate : "";
70
+ execFileSync(pythonCmd, ["-c", `import ${moduleName}`], { stdio: "ignore" });
71
+ return true;
147
72
  } catch {
148
- return "";
73
+ return false;
149
74
  }
150
75
  }
151
76
 
152
- function listCliCandidatesFromPath(target) {
153
- const cmd = process.platform === "win32" ? "where.exe" : "which";
154
- const args = process.platform === "win32" ? [target] : ["-a", target];
155
- const result = spawnSync(cmd, args, { encoding: "utf8", windowsHide: true });
156
- if (result.status !== 0) return [];
157
- const candidates = String(result.stdout || "")
158
- .split(/\r?\n/)
159
- .map((line) => line.trim())
160
- .filter(Boolean);
161
- return process.platform === "win32" ? sortWindowsCliCandidates(candidates) : candidates;
162
- }
163
-
164
- function sortWindowsCliCandidates(candidates) {
165
- const extPriority = (candidate) => {
166
- const ext = path.extname(String(candidate || "")).toLowerCase();
167
- if (ext === ".cmd" || ext === ".bat" || ext === ".exe") return 0;
168
- return 1;
169
- };
170
- return [...candidates].sort((a, b) => extPriority(a) - extPriority(b));
171
- }
172
-
173
- function resolveAgentCli({ target, preferred = "", shimDir = "" }) {
174
- const appData = process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming");
175
- const candidates = [
176
- preferred,
177
- process.platform === "win32" ? path.join(appData, "npm", `${target}.cmd`) : "",
178
- process.platform === "win32" ? path.join(appData, "npm", `${target}.exe`) : "",
179
- process.platform === "win32" ? path.join(appData, "npm", target) : "",
180
- ...listCliCandidatesFromPath(target)
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] }
181
82
  ];
182
- for (const candidate of candidates) {
183
- const found = existingCliCandidate(candidate, shimDir);
184
- if (found) return found;
185
- }
186
- return "";
187
- }
188
-
189
- function prependWindowsUserPath(dir) {
190
- if (process.platform !== "win32") return false;
191
- const cmd = [
192
- "$ErrorActionPreference = 'Stop';",
193
- `$dir = ${psQuote(dir)};`,
194
- "$current = [Environment]::GetEnvironmentVariable('Path', 'User');",
195
- "$parts = @();",
196
- "if ($current) { $parts = $current -split ';' | Where-Object { $_ -and $_.Trim() } }",
197
- "$exists = $false;",
198
- "foreach ($part in $parts) { if ([string]::Equals($part.TrimEnd('\\'), $dir.TrimEnd('\\'), [StringComparison]::OrdinalIgnoreCase)) { $exists = $true } }",
199
- "if (-not $exists) {",
200
- " $next = (@($dir) + $parts) -join ';';",
201
- " [Environment]::SetEnvironmentVariable('Path', $next, 'User');",
202
- "}"
203
- ].join(" ");
204
- const result = spawnSync("powershell", ["-NoProfile", "-Command", cmd], { stdio: "inherit", windowsHide: true });
205
- if (result.status !== 0) throw new Error("Failed to prepend Codex Langfuse shim to the user PATH.");
206
83
 
207
- const currentParts = String(process.env.PATH || "").split(path.delimiter);
208
- const normalized = dir.replace(/[\\/]+$/, "").toLowerCase();
209
- if (!currentParts.some((part) => part && part.replace(/[\\/]+$/, "").toLowerCase() === normalized)) {
210
- process.env.PATH = `${dir}${path.delimiter}${process.env.PATH || ""}`;
211
- }
212
- return true;
213
- }
214
-
215
- function writeAgentCommandShim({ baseDir, target, executable, realCli, publicKey, secretKey, baseUrl, userId }) {
216
- if (!realCli) return null;
217
- const shimDir = path.join(baseDir, "bin");
218
- ensureDir(shimDir);
219
- if (process.platform === "win32") {
220
- const shim = path.join(shimDir, `${executable}.cmd`);
221
- const envPrefix = `OH_LANGFUSE_${target.toUpperCase()}_SHIM`;
222
- const content = [
223
- "@echo off",
224
- "REM Auto-generated by scripts/codex-langfuse-setup.mjs",
225
- `set ${envPrefix}=1`,
226
- `set LANGFUSE_PUBLIC_KEY=${publicKey}`,
227
- `set LANGFUSE_SECRET_KEY=${secretKey}`,
228
- `set LANGFUSE_BASEURL=${baseUrl}`,
229
- userId ? `set LANGFUSE_USER_ID=${userId}` : null,
230
- windowsAutoUpdateCommand(target),
231
- `call ${cmdQuote(realCli)} %*`,
232
- "exit /b %ERRORLEVEL%",
233
- ""
234
- ].filter(Boolean).join(os.EOL);
235
- fs.writeFileSync(shim, content, "utf8");
236
- return { shim, shimDir };
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
+ }
237
93
  }
238
94
 
239
- const shim = path.join(shimDir, executable);
240
- const lines = [
241
- "#!/usr/bin/env sh",
242
- "# Auto-generated by scripts/codex-langfuse-setup.mjs",
243
- "set -eu",
244
- `export OH_LANGFUSE_${target.toUpperCase()}_SHIM=1`,
245
- `export LANGFUSE_PUBLIC_KEY=${shQuote(publicKey)}`,
246
- `export LANGFUSE_SECRET_KEY=${shQuote(secretKey)}`,
247
- `export LANGFUSE_BASEURL=${shQuote(baseUrl)}`,
248
- userId ? `export LANGFUSE_USER_ID=${shQuote(userId)}` : null,
249
- unixAutoUpdateCommand(target),
250
- `exec ${shQuote(realCli)} "$@"`,
251
- ""
252
- ].filter(Boolean);
253
- fs.writeFileSync(shim, lines.join("\n"), "utf8");
254
- fs.chmodSync(shim, 0o755);
255
- return { shim, shimDir };
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
+ );
256
98
  }
257
99
 
258
- function createAgentLauncher({ baseDir, target, executable }) {
259
- if (process.platform === "win32") {
260
- const launcher = path.join(baseDir, `launch-${target}-langfuse.cmd`);
261
- const content = [
262
- "@echo off",
263
- windowsAutoUpdateCommand(target),
264
- `${executable} %*`,
265
- ""
266
- ].join(os.EOL);
267
- fs.writeFileSync(launcher, content, "utf8");
268
- return launcher;
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");
269
105
  }
270
-
271
- const launcher = path.join(baseDir, `launch-${target}-langfuse.sh`);
272
- const content = [
273
- "#!/usr/bin/env sh",
274
- "set -eu",
275
- unixAutoUpdateCommand(target),
276
- `exec ${shQuote(executable)} "$@"`,
277
- ""
278
- ].join("\n");
279
- fs.writeFileSync(launcher, content, "utf8");
280
- fs.chmodSync(launcher, 0o755);
281
- return launcher;
106
+ return pythonCmd;
282
107
  }
108
+
109
+ function shQuote(s) {
110
+ return `'${String(s).replace(/'/g, "'\"'\"'")}'`;
111
+ }
283
112
 
284
- function createOrUpdateLangfuseVenv({ baseDir, pipIndexUrl = "https://pypi.tuna.tsinghua.edu.cn/simple" }) {
113
+ function psQuote(s) {
114
+ return `'${String(s).replace(/'/g, "''")}'`;
115
+ }
116
+
117
+ function cmdQuote(s) {
118
+ return `"${String(s).replace(/"/g, '""')}"`;
119
+ }
120
+
121
+ function autoUpdateRuntimePath() {
122
+ return path.join(packageRoot, "scripts", "auto-update-runtime.mjs");
123
+ }
124
+
125
+ function autoUpdateHelperDir() {
126
+ return path.join(os.homedir(), ".config", "oh-langfuse", "bin");
127
+ }
128
+
129
+ function writeAutoUpdateHelper(target) {
130
+ const helperDir = autoUpdateHelperDir();
131
+ ensureDir(helperDir);
132
+ const runtimePath = autoUpdateRuntimePath();
133
+ const fallbackArgs = ["-y", "oh-langfuse@latest", "auto-update", target, "--skip-check", "--startup-status"];
134
+
135
+ if (process.platform === "win32") {
136
+ const helper = path.join(helperDir, `oh-langfuse-auto-update-${target}.cmd`);
137
+ const lines = [
138
+ "@echo off",
139
+ "REM Auto-generated by scripts/codex-langfuse-setup.mjs",
140
+ `if exist ${cmdQuote(runtimePath)} (`,
141
+ ` call ${cmdQuote(process.execPath)} ${cmdQuote(runtimePath)} ${target} --skip-check --startup-status %*`,
142
+ ") else (",
143
+ ` call npx.cmd ${fallbackArgs.map(cmdQuote).join(" ")} %*`,
144
+ ")",
145
+ "exit /b 0",
146
+ ""
147
+ ];
148
+ fs.writeFileSync(helper, lines.join(os.EOL), "utf8");
149
+ return helper;
150
+ }
151
+
152
+ const helper = path.join(helperDir, `oh-langfuse-auto-update-${target}`);
153
+ const lines = [
154
+ "#!/usr/bin/env sh",
155
+ "# Auto-generated by scripts/codex-langfuse-setup.mjs",
156
+ "set +e",
157
+ `if [ -f ${shQuote(runtimePath)} ]; then`,
158
+ ` ${shQuote(process.execPath)} ${shQuote(runtimePath)} ${shQuote(target)} --skip-check --startup-status "$@"`,
159
+ "else",
160
+ ` npx ${fallbackArgs.map(shQuote).join(" ")} "$@"`,
161
+ "fi",
162
+ "exit 0",
163
+ ""
164
+ ];
165
+ fs.writeFileSync(helper, lines.join("\n"), "utf8");
166
+ fs.chmodSync(helper, 0o755);
167
+ return helper;
168
+ }
169
+
170
+ function windowsAutoUpdateCommand(target) {
171
+ return `call ${cmdQuote(writeAutoUpdateHelper(target))}`;
172
+ }
173
+
174
+ function unixAutoUpdateCommand(target) {
175
+ return `${shQuote(writeAutoUpdateHelper(target))} || true`;
176
+ }
177
+
178
+ function isPathInsideDir(candidate, dir) {
179
+ if (!candidate || !dir) return false;
180
+ const relative = path.relative(path.resolve(dir), path.resolve(candidate));
181
+ return relative === "" || (relative && !relative.startsWith("..") && !path.isAbsolute(relative));
182
+ }
183
+
184
+ function existingCliCandidate(candidate, shimDir) {
185
+ if (!candidate || isPathInsideDir(candidate, shimDir)) return "";
186
+ try {
187
+ return fs.existsSync(candidate) ? candidate : "";
188
+ } catch {
189
+ return "";
190
+ }
191
+ }
192
+
193
+ function listCliCandidatesFromPath(target) {
194
+ const cmd = process.platform === "win32" ? "where.exe" : "which";
195
+ const args = process.platform === "win32" ? [target] : ["-a", target];
196
+ const result = spawnSync(cmd, args, { encoding: "utf8", windowsHide: true });
197
+ if (result.status !== 0) return [];
198
+ const candidates = String(result.stdout || "")
199
+ .split(/\r?\n/)
200
+ .map((line) => line.trim())
201
+ .filter(Boolean);
202
+ return process.platform === "win32" ? sortWindowsCliCandidates(candidates) : candidates;
203
+ }
204
+
205
+ function sortWindowsCliCandidates(candidates) {
206
+ const extPriority = (candidate) => {
207
+ const ext = path.extname(String(candidate || "")).toLowerCase();
208
+ if (ext === ".cmd" || ext === ".bat" || ext === ".exe") return 0;
209
+ return 1;
210
+ };
211
+ return [...candidates].sort((a, b) => extPriority(a) - extPriority(b));
212
+ }
213
+
214
+ function resolveAgentCli({ target, preferred = "", shimDir = "" }) {
215
+ const appData = process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming");
216
+ const candidates = [
217
+ preferred,
218
+ process.platform === "win32" ? path.join(appData, "npm", `${target}.cmd`) : "",
219
+ process.platform === "win32" ? path.join(appData, "npm", `${target}.exe`) : "",
220
+ process.platform === "win32" ? path.join(appData, "npm", target) : "",
221
+ ...listCliCandidatesFromPath(target)
222
+ ];
223
+ for (const candidate of candidates) {
224
+ const found = existingCliCandidate(candidate, shimDir);
225
+ if (found) return found;
226
+ }
227
+ return "";
228
+ }
229
+
230
+ function prependWindowsUserPath(dir) {
231
+ if (process.platform !== "win32") return false;
232
+ const cmd = [
233
+ "$ErrorActionPreference = 'Stop';",
234
+ `$dir = ${psQuote(dir)};`,
235
+ "$current = [Environment]::GetEnvironmentVariable('Path', 'User');",
236
+ "$parts = @();",
237
+ "if ($current) { $parts = $current -split ';' | Where-Object { $_ -and $_.Trim() } }",
238
+ "$exists = $false;",
239
+ "foreach ($part in $parts) { if ([string]::Equals($part.TrimEnd('\\'), $dir.TrimEnd('\\'), [StringComparison]::OrdinalIgnoreCase)) { $exists = $true } }",
240
+ "if (-not $exists) {",
241
+ " $next = (@($dir) + $parts) -join ';';",
242
+ " [Environment]::SetEnvironmentVariable('Path', $next, 'User');",
243
+ "}"
244
+ ].join(" ");
245
+ const result = spawnSync("powershell", ["-NoProfile", "-Command", cmd], { stdio: "inherit", windowsHide: true });
246
+ if (result.status !== 0) throw new Error("Failed to prepend Codex Langfuse shim to the user PATH.");
247
+
248
+ const currentParts = String(process.env.PATH || "").split(path.delimiter);
249
+ const normalized = dir.replace(/[\\/]+$/, "").toLowerCase();
250
+ if (!currentParts.some((part) => part && part.replace(/[\\/]+$/, "").toLowerCase() === normalized)) {
251
+ process.env.PATH = `${dir}${path.delimiter}${process.env.PATH || ""}`;
252
+ }
253
+ return true;
254
+ }
255
+
256
+ function writeAgentCommandShim({ baseDir, target, executable, realCli, publicKey, secretKey, baseUrl, userId }) {
257
+ if (!realCli) return null;
258
+ const shimDir = path.join(baseDir, "bin");
259
+ ensureDir(shimDir);
260
+ if (process.platform === "win32") {
261
+ const shim = path.join(shimDir, `${executable}.cmd`);
262
+ const envPrefix = `OH_LANGFUSE_${target.toUpperCase()}_SHIM`;
263
+ const content = [
264
+ "@echo off",
265
+ "REM Auto-generated by scripts/codex-langfuse-setup.mjs",
266
+ `set ${envPrefix}=1`,
267
+ `set LANGFUSE_PUBLIC_KEY=${publicKey}`,
268
+ `set LANGFUSE_SECRET_KEY=${secretKey}`,
269
+ `set LANGFUSE_BASEURL=${baseUrl}`,
270
+ userId ? `set LANGFUSE_USER_ID=${userId}` : null,
271
+ windowsAutoUpdateCommand(target),
272
+ `call ${cmdQuote(realCli)} %*`,
273
+ "exit /b %ERRORLEVEL%",
274
+ ""
275
+ ].filter(Boolean).join(os.EOL);
276
+ fs.writeFileSync(shim, content, "utf8");
277
+ return { shim, shimDir };
278
+ }
279
+
280
+ const shim = path.join(shimDir, executable);
281
+ const lines = [
282
+ "#!/usr/bin/env sh",
283
+ "# Auto-generated by scripts/codex-langfuse-setup.mjs",
284
+ "set -eu",
285
+ `export OH_LANGFUSE_${target.toUpperCase()}_SHIM=1`,
286
+ `export LANGFUSE_PUBLIC_KEY=${shQuote(publicKey)}`,
287
+ `export LANGFUSE_SECRET_KEY=${shQuote(secretKey)}`,
288
+ `export LANGFUSE_BASEURL=${shQuote(baseUrl)}`,
289
+ userId ? `export LANGFUSE_USER_ID=${shQuote(userId)}` : null,
290
+ unixAutoUpdateCommand(target),
291
+ `exec ${shQuote(realCli)} "$@"`,
292
+ ""
293
+ ].filter(Boolean);
294
+ fs.writeFileSync(shim, lines.join("\n"), "utf8");
295
+ fs.chmodSync(shim, 0o755);
296
+ return { shim, shimDir };
297
+ }
298
+
299
+ function createAgentLauncher({ baseDir, target, executable }) {
300
+ if (process.platform === "win32") {
301
+ const launcher = path.join(baseDir, `launch-${target}-langfuse.cmd`);
302
+ const content = [
303
+ "@echo off",
304
+ windowsAutoUpdateCommand(target),
305
+ `${executable} %*`,
306
+ ""
307
+ ].join(os.EOL);
308
+ fs.writeFileSync(launcher, content, "utf8");
309
+ return launcher;
310
+ }
311
+
312
+ const launcher = path.join(baseDir, `launch-${target}-langfuse.sh`);
313
+ const content = [
314
+ "#!/usr/bin/env sh",
315
+ "set -eu",
316
+ unixAutoUpdateCommand(target),
317
+ `exec ${shQuote(executable)} "$@"`,
318
+ ""
319
+ ].join("\n");
320
+ fs.writeFileSync(launcher, content, "utf8");
321
+ fs.chmodSync(launcher, 0o755);
322
+ return launcher;
323
+ }
324
+
325
+ function createOrUpdateLangfuseVenv({ baseDir, pipIndexUrl = "https://pypi.tuna.tsinghua.edu.cn/simple" }) {
285
326
  const venvDir = path.join(baseDir, "langfuse-venv");
286
327
  const pythonCmd = process.platform === "win32" ? "python" : "python3";
287
328
  const venvPython = pythonExecutableInVenv(venvDir);
288
329
 
289
330
  if (!fs.existsSync(venvPython)) {
290
331
  console.log(`Creating Python virtual environment: ${venvDir}`);
291
- try {
292
- execFileSync(pythonCmd, ["-m", "venv", venvDir], { stdio: "inherit" });
293
- } catch (e) {
294
- if (process.platform !== "win32") {
295
- throw new Error("Failed to create Python venv. Please install python3-venv first, for example: sudo apt install python3-venv");
296
- }
297
- throw new Error("Failed to create Python venv. Please confirm Python venv support is available.");
298
- }
332
+ try {
333
+ execFileSync(pythonCmd, ["-m", "venv", venvDir], { stdio: "inherit" });
334
+ } catch (e) {
335
+ if (process.platform !== "win32") {
336
+ return installLangfuseWithSystemPython({ pythonCmd, pipIndexUrl });
337
+ }
338
+ throw new Error("Failed to create Python venv. Please confirm Python venv support is available.");
339
+ }
299
340
  }
300
341
 
301
342
  console.log("Installing/updating Python package in venv: langfuse");
302
343
  try {
303
344
  execFileSync(
304
345
  venvPython,
305
- ["-m", "pip", "install", "-U", "langfuse", "-i", pipIndexUrl],
346
+ ["-m", "pip", "install", "-U", "langfuse", "-i", pipIndexUrl],
306
347
  { stdio: "inherit" }
307
348
  );
308
349
  } catch (e) {
350
+ if (process.platform !== "win32") {
351
+ return installLangfuseWithSystemPython({ pythonCmd, pipIndexUrl });
352
+ }
309
353
  throw new Error(`Failed to install langfuse in venv: ${venvPython} -m pip install -U langfuse -i ${pipIndexUrl}`);
310
354
  }
311
355
 
312
356
  return venvPython;
313
357
  }
314
358
 
315
- function updateCodexNotify(configPath, notifyCommand) {
316
- let content = fs.existsSync(configPath) ? fs.readFileSync(configPath, "utf8") : "";
317
- content = stripBom(content);
359
+ function updateCodexNotify(configPath, notifyCommand) {
360
+ let content = fs.existsSync(configPath) ? fs.readFileSync(configPath, "utf8") : "";
361
+ content = stripBom(content);
318
362
  if (fs.existsSync(configPath)) {
319
363
  const backupPath = `${configPath}.bak-${Date.now()}`;
320
364
  fs.copyFileSync(configPath, backupPath);
321
365
  console.log(`Backed up existing Codex config: ${backupPath}`);
322
- }
323
- const lines = content.split(/\r?\n/);
324
- const notifyLine = `notify = ${tomlArray(notifyCommand)}`;
325
- let inTopLevel = true;
326
- let replaced = false;
327
- let inserted = false;
328
- const next = [];
329
-
330
- for (const line of lines) {
331
- if (/^\s*\[/.test(line)) {
332
- if (!replaced && !inserted) {
333
- while (next.length && next[next.length - 1].trim() === "") next.pop();
334
- next.push(notifyLine, "");
335
- inserted = true;
336
- }
337
- inTopLevel = false;
338
- }
339
- if (inTopLevel && /^\s*notify\s*=/.test(line)) {
340
- if (!replaced) next.push(notifyLine);
341
- replaced = true;
342
- continue;
343
- }
366
+ }
367
+ const lines = content.split(/\r?\n/);
368
+ const notifyLine = `notify = ${tomlArray(notifyCommand)}`;
369
+ let inTopLevel = true;
370
+ let replaced = false;
371
+ let inserted = false;
372
+ const next = [];
373
+
374
+ for (const line of lines) {
375
+ if (/^\s*\[/.test(line)) {
376
+ if (!replaced && !inserted) {
377
+ while (next.length && next[next.length - 1].trim() === "") next.pop();
378
+ next.push(notifyLine, "");
379
+ inserted = true;
380
+ }
381
+ inTopLevel = false;
382
+ }
383
+ if (inTopLevel && /^\s*notify\s*=/.test(line)) {
384
+ if (!replaced) next.push(notifyLine);
385
+ replaced = true;
386
+ continue;
387
+ }
344
388
  next.push(line);
345
- }
346
-
347
- if (!replaced && !inserted) {
348
- while (next.length && next[next.length - 1].trim() === "") next.pop();
349
- next.push(notifyLine, "");
350
- }
389
+ }
390
+
391
+ if (!replaced && !inserted) {
392
+ while (next.length && next[next.length - 1].trim() === "") next.pop();
393
+ next.push(notifyLine, "");
394
+ }
351
395
 
352
396
  fs.writeFileSync(configPath, next.join(os.EOL), "utf8");
353
397
  }
354
398
 
355
- async function main() {
356
- const args = parseArgs(process.argv.slice(2));
357
- const rootDir = packageRoot;
358
- const packageJson = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8"));
399
+ async function main() {
400
+ const args = parseArgs(process.argv.slice(2));
401
+ const rootDir = packageRoot;
402
+ const packageJson = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8"));
359
403
 
360
404
  const publicKey =
361
405
  args.publicKey || process.env.LANGFUSE_PUBLIC_KEY || "pk-lf-da0c90a7-6e93-4eb7-bb86-c1047c8d187d";
@@ -368,12 +412,12 @@ async function main() {
368
412
  process.env.LANGFUSE_BASEURL ||
369
413
  process.env.LANGFUSE_HOST ||
370
414
  "http://120.46.221.227:3000";
371
- const userId = normalizeUserId(args.userId || args.userid || "");
372
- if (!userId || typeof userId !== "string") {
373
- throw new Error("缺少参数:--userId=你的工号");
374
- }
375
- assertValidUserId(userId);
376
- const pipIndexUrl = args.pipIndexUrl || process.env.LANGFUSE_PIP_INDEX_URL || "https://pypi.tuna.tsinghua.edu.cn/simple";
415
+ const userId = normalizeUserId(args.userId || args.userid || "");
416
+ if (!userId || typeof userId !== "string") {
417
+ throw new Error("缺少参数:--userId=你的工号");
418
+ }
419
+ assertValidUserId(userId);
420
+ const pipIndexUrl = args.pipIndexUrl || process.env.LANGFUSE_PIP_INDEX_URL || "https://pypi.tuna.tsinghua.edu.cn/simple";
377
421
 
378
422
  if (!publicKey || !secretKey) {
379
423
  throw new Error("Missing Langfuse keys: provide --publicKey and --secretKey or set LANGFUSE_PUBLIC_KEY/LANGFUSE_SECRET_KEY.");
@@ -404,49 +448,49 @@ async function main() {
404
448
  });
405
449
 
406
450
  const pythonCmd = process.platform === "win32" ? "python" : "python3";
407
- const notifyPython = args["skip-pip-install"]
408
- ? pythonCmd
409
- : createOrUpdateLangfuseVenv({ baseDir: codexHome, pipIndexUrl });
410
- updateCodexNotify(configPath, [notifyPython, destHook]);
411
- const codexShimDir = path.join(codexHome, "bin");
412
- const realCodexCli = resolveAgentCli({ target: "codex", preferred: args.cmd || "", shimDir: codexShimDir });
413
- const directShim = writeAgentCommandShim({
414
- baseDir: codexHome,
415
- target: "codex",
416
- executable: "codex",
417
- realCli: realCodexCli,
418
- publicKey,
419
- secretKey,
420
- baseUrl,
421
- userId
422
- });
423
- if (directShim?.shimDir) {
424
- prependWindowsUserPath(directShim.shimDir);
425
- }
426
- const agentLauncher = createAgentLauncher({
427
- baseDir: codexHome,
428
- target: "codex",
429
- executable: realCodexCli ? cmdQuote(realCodexCli) : "codex"
430
- });
431
-
432
- console.log(`Updated Codex notify hook: ${configPath}`);
433
- console.log(`Installed hook script: ${destHook}`);
434
- console.log(`Wrote Langfuse config: ${path.join(langfuseDir, "config.json")}`);
435
- console.log(`Agent launcher with update check: ${agentLauncher}`);
436
- if (directShim) {
437
- console.log(`Direct Codex command shim ready: ${directShim.shim}`);
438
- console.log(`Real Codex CLI: ${realCodexCli}`);
439
- } else {
440
- console.log("Codex CLI not found; skipped direct codex command shim.");
441
- }
442
- const runtimeState = writeRuntimeInstallRecord("codex", {
443
- packageName: packageJson.name,
444
- packageVersion: packageJson.version,
445
- });
446
- console.log(`Runtime version recorded: ${runtimeState}`);
447
-
448
- console.log("Done. Restart Codex so the updated notify command is loaded.");
449
- }
451
+ const notifyPython = args["skip-pip-install"]
452
+ ? pythonCmd
453
+ : createOrUpdateLangfuseVenv({ baseDir: codexHome, pipIndexUrl });
454
+ updateCodexNotify(configPath, [notifyPython, destHook]);
455
+ const codexShimDir = path.join(codexHome, "bin");
456
+ const realCodexCli = resolveAgentCli({ target: "codex", preferred: args.cmd || "", shimDir: codexShimDir });
457
+ const directShim = writeAgentCommandShim({
458
+ baseDir: codexHome,
459
+ target: "codex",
460
+ executable: "codex",
461
+ realCli: realCodexCli,
462
+ publicKey,
463
+ secretKey,
464
+ baseUrl,
465
+ userId
466
+ });
467
+ if (directShim?.shimDir) {
468
+ prependWindowsUserPath(directShim.shimDir);
469
+ }
470
+ const agentLauncher = createAgentLauncher({
471
+ baseDir: codexHome,
472
+ target: "codex",
473
+ executable: realCodexCli ? cmdQuote(realCodexCli) : "codex"
474
+ });
475
+
476
+ console.log(`Updated Codex notify hook: ${configPath}`);
477
+ console.log(`Installed hook script: ${destHook}`);
478
+ console.log(`Wrote Langfuse config: ${path.join(langfuseDir, "config.json")}`);
479
+ console.log(`Agent launcher with update check: ${agentLauncher}`);
480
+ if (directShim) {
481
+ console.log(`Direct Codex command shim ready: ${directShim.shim}`);
482
+ console.log(`Real Codex CLI: ${realCodexCli}`);
483
+ } else {
484
+ console.log("Codex CLI not found; skipped direct codex command shim.");
485
+ }
486
+ const runtimeState = writeRuntimeInstallRecord("codex", {
487
+ packageName: packageJson.name,
488
+ packageVersion: packageJson.version,
489
+ });
490
+ console.log(`Runtime version recorded: ${runtimeState}`);
491
+
492
+ console.log("Done. Restart Codex so the updated notify command is loaded.");
493
+ }
450
494
 
451
495
  main().catch((err) => {
452
496
  console.error(err?.message || String(err));