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.
- package/README.md +142 -142
- package/bin/cli.js +425 -425
- package/codex_langfuse_notify.py +517 -517
- package/langfuse_hook.py +581 -581
- package/package.json +1 -1
- package/scripts/auto-update-runtime.mjs +190 -190
- package/scripts/codex-langfuse-check.mjs +81 -81
- package/scripts/codex-langfuse-setup.mjs +358 -314
- package/scripts/langfuse-check.mjs +180 -180
- package/scripts/langfuse-setup.mjs +370 -326
- package/scripts/log-filter-utils.mjs +26 -26
- package/scripts/metrics-utils.mjs +377 -377
- package/scripts/opencode-langfuse-check.mjs +9 -0
- package/scripts/opencode-langfuse-setup.mjs +944 -935
- package/scripts/real-self-verify.mjs +621 -621
- package/scripts/runtime-state-utils.mjs +53 -53
- package/scripts/update-langfuse-runtime.mjs +260 -260
- package/scripts/update-utils.mjs +73 -73
|
@@ -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
|
|
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
|
-
|
|
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
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
|
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
|
-
|
|
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));
|