oh-langfuse 0.1.71 → 0.1.73
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/package.json +2 -1
- package/scripts/cli-detection-utils.mjs +114 -0
- package/scripts/codex-langfuse-check.mjs +28 -7
- package/scripts/codex-langfuse-setup.mjs +110 -88
- package/scripts/langfuse-check.mjs +50 -32
- package/scripts/langfuse-setup.mjs +121 -103
- package/scripts/resolve-opencode-cli.mjs +9 -7
- package/scripts/update-langfuse-runtime.mjs +20 -14
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oh-langfuse",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.73",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Use npm scripts to configure Claude Code / OpenCode / Codex with Langfuse tracing.",
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"scripts/auto-update-runtime.mjs",
|
|
17
17
|
"scripts/codex-langfuse-check.mjs",
|
|
18
18
|
"scripts/codex-langfuse-setup.mjs",
|
|
19
|
+
"scripts/cli-detection-utils.mjs",
|
|
19
20
|
"scripts/json-utils.mjs",
|
|
20
21
|
"scripts/langfuse-check.mjs",
|
|
21
22
|
"scripts/langfuse-setup.mjs",
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
|
|
6
|
+
function unique(values) {
|
|
7
|
+
return [...new Set(values.filter(Boolean))];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function stripExecutableExtension(name) {
|
|
11
|
+
return String(name || "").replace(/\.(cmd|bat|exe)$/i, "");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function isTargetBinary(target, candidate) {
|
|
15
|
+
const base = stripExecutableExtension(path.basename(String(candidate || ""))).toLowerCase();
|
|
16
|
+
return base === String(target || "").toLowerCase();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function sortWindowsCliCandidates(candidates) {
|
|
20
|
+
const extPriority = (candidate) => {
|
|
21
|
+
const ext = path.extname(String(candidate || "")).toLowerCase();
|
|
22
|
+
if (ext === ".cmd" || ext === ".bat" || ext === ".exe") return 0;
|
|
23
|
+
return 1;
|
|
24
|
+
};
|
|
25
|
+
return [...candidates].sort((a, b) => extPriority(a) - extPriority(b));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function parseWhereisBinaryOutput(target, stdout) {
|
|
29
|
+
const candidates = [];
|
|
30
|
+
for (const line of String(stdout || "").split(/\r?\n/)) {
|
|
31
|
+
const colon = line.indexOf(":");
|
|
32
|
+
const body = colon === -1 ? line : line.slice(colon + 1);
|
|
33
|
+
for (const token of body.split(/\s+/)) {
|
|
34
|
+
const candidate = token.trim();
|
|
35
|
+
if (candidate && path.isAbsolute(candidate) && isTargetBinary(target, candidate)) {
|
|
36
|
+
candidates.push(candidate);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return unique(candidates);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isLikelyNodePrefix(name) {
|
|
44
|
+
return /^(node|nodejs)(?:[-_.].*)?$/i.test(String(name || ""));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function listCommonUnixCliCandidates(target, roots = ["/usr/local", "/opt"]) {
|
|
48
|
+
if (process.platform === "win32") return [];
|
|
49
|
+
const candidates = [
|
|
50
|
+
path.join("/usr/local", "bin", target),
|
|
51
|
+
path.join("/usr", "bin", target),
|
|
52
|
+
path.join("/bin", target),
|
|
53
|
+
];
|
|
54
|
+
for (const root of roots) {
|
|
55
|
+
try {
|
|
56
|
+
if (!fs.existsSync(root)) continue;
|
|
57
|
+
for (const ent of fs.readdirSync(root, { withFileTypes: true })) {
|
|
58
|
+
if (ent.isDirectory() && isLikelyNodePrefix(ent.name)) {
|
|
59
|
+
candidates.push(path.join(root, ent.name, "bin", target));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
} catch {
|
|
63
|
+
// Best effort only; command lookups and other candidates may still work.
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return unique(candidates);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function listSystemCliCandidates(target) {
|
|
70
|
+
if (!target) return [];
|
|
71
|
+
const candidates = [];
|
|
72
|
+
if (process.platform === "win32") {
|
|
73
|
+
const result = spawnSync("where.exe", [target], { encoding: "utf8", windowsHide: true });
|
|
74
|
+
if (result.status === 0) {
|
|
75
|
+
candidates.push(...String(result.stdout || "").split(/\r?\n/).map((line) => line.trim()).filter(Boolean));
|
|
76
|
+
}
|
|
77
|
+
return sortWindowsCliCandidates(unique(candidates));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const which = spawnSync("which", ["-a", target], { encoding: "utf8", windowsHide: true });
|
|
81
|
+
if (which.status === 0) {
|
|
82
|
+
candidates.push(...String(which.stdout || "").split(/\r?\n/).map((line) => line.trim()).filter(Boolean));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const whereis = spawnSync("whereis", ["-b", target], { encoding: "utf8", windowsHide: true });
|
|
86
|
+
if (whereis.status === 0) {
|
|
87
|
+
candidates.push(...parseWhereisBinaryOutput(target, whereis.stdout));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
candidates.push(...listCommonUnixCliCandidates(target));
|
|
91
|
+
return unique(candidates);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function candidateExists(candidate) {
|
|
95
|
+
try {
|
|
96
|
+
return Boolean(candidate) && fs.existsSync(candidate);
|
|
97
|
+
} catch {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function hasSystemCli(target) {
|
|
103
|
+
return listSystemCliCandidates(target).some(candidateExists);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function defaultWindowsNpmCliCandidates(target) {
|
|
107
|
+
const appData = process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming");
|
|
108
|
+
if (process.platform !== "win32") return [];
|
|
109
|
+
return [
|
|
110
|
+
path.join(appData, "npm", `${target}.cmd`),
|
|
111
|
+
path.join(appData, "npm", `${target}.exe`),
|
|
112
|
+
path.join(appData, "npm", target),
|
|
113
|
+
];
|
|
114
|
+
}
|
|
@@ -38,8 +38,8 @@ function findLatestSession(sessionsDir) {
|
|
|
38
38
|
return latest;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
function commandOk(cmd, args) {
|
|
42
|
-
const r = spawnSync(cmd, args, { encoding: "utf8" });
|
|
41
|
+
function commandOk(cmd, args, env = process.env) {
|
|
42
|
+
const r = spawnSync(cmd, args, { encoding: "utf8", env });
|
|
43
43
|
return { ok: !r.error && r.status === 0, detail: (r.stdout || r.stderr || "").trim() };
|
|
44
44
|
}
|
|
45
45
|
|
|
@@ -63,8 +63,22 @@ function readCodexShimTarget(codexHome) {
|
|
|
63
63
|
function venvPython(codexHome) {
|
|
64
64
|
return process.platform === "win32"
|
|
65
65
|
? path.join(codexHome, "langfuse-venv", "Scripts", "python.exe")
|
|
66
|
-
: path.join(codexHome, "langfuse-venv", "bin", "python");
|
|
67
|
-
}
|
|
66
|
+
: path.join(codexHome, "langfuse-venv", "bin", "python");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function fallbackPythonCommand() {
|
|
70
|
+
return process.platform === "win32" ? "python" : "python3";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isolatedPythonPath(codexHome) {
|
|
74
|
+
return path.join(codexHome, "langfuse-python");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function langfuseImportOk(hookPython, fallbackTarget) {
|
|
78
|
+
if (fs.existsSync(hookPython)) return commandOk(hookPython, ["-c", "import langfuse; print('langfuse ok')"]);
|
|
79
|
+
const env = { ...process.env, PYTHONPATH: process.env.PYTHONPATH ? `${fallbackTarget}${path.delimiter}${process.env.PYTHONPATH}` : fallbackTarget };
|
|
80
|
+
return commandOk(fallbackPythonCommand(), ["-c", "import langfuse; print('langfuse ok')"], env);
|
|
81
|
+
}
|
|
68
82
|
|
|
69
83
|
function configHasNotify(configText) {
|
|
70
84
|
const firstSection = configText.search(/^\s*\[/m);
|
|
@@ -84,10 +98,11 @@ function main() {
|
|
|
84
98
|
const sessionsDir = path.join(codexHome, "sessions");
|
|
85
99
|
const latestSession = findLatestSession(sessionsDir);
|
|
86
100
|
const langfuseConfig = readJsonIfExists(langfuseConfigPath);
|
|
87
|
-
const configText = fs.existsSync(configPath) ? stripBom(fs.readFileSync(configPath, "utf8")) : "";
|
|
101
|
+
const configText = fs.existsSync(configPath) ? stripBom(fs.readFileSync(configPath, "utf8")) : "";
|
|
88
102
|
const hookPython = venvPython(codexHome);
|
|
103
|
+
const fallbackTarget = isolatedPythonPath(codexHome);
|
|
89
104
|
const python = commandOk(process.platform === "win32" ? "python" : "python3", ["--version"]);
|
|
90
|
-
const langfuseImport =
|
|
105
|
+
const langfuseImport = langfuseImportOk(hookPython, fallbackTarget);
|
|
91
106
|
const codexShimTarget = readCodexShimTarget(codexHome);
|
|
92
107
|
const codexShimUsesWindowsApps = !!codexShimTarget && isWindowsAppsPath(codexShimTarget);
|
|
93
108
|
|
|
@@ -138,7 +153,13 @@ function main() {
|
|
|
138
153
|
codexShimTarget || "not installed",
|
|
139
154
|
"Run setup/update again with a fixed oh-langfuse version; WindowsApps Codex paths cannot be called directly."
|
|
140
155
|
);
|
|
141
|
-
addResult(
|
|
156
|
+
addResult(
|
|
157
|
+
results,
|
|
158
|
+
"Langfuse hook Python",
|
|
159
|
+
fs.existsSync(hookPython) || fs.existsSync(fallbackTarget),
|
|
160
|
+
fs.existsSync(hookPython) ? hookPython : `${fallbackPythonCommand()} with PYTHONPATH=${fallbackTarget}`,
|
|
161
|
+
"Run setup again; on Linux the installer can fall back to an isolated PYTHONPATH target."
|
|
162
|
+
);
|
|
142
163
|
addResult(
|
|
143
164
|
results,
|
|
144
165
|
"Python langfuse package",
|
|
@@ -2,9 +2,10 @@ import fs from "node:fs";
|
|
|
2
2
|
import fsp from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import os from "node:os";
|
|
5
|
-
import { execFileSync, spawnSync } from "node:child_process";
|
|
6
|
-
import { fileURLToPath } from "node:url";
|
|
7
|
-
import {
|
|
5
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { defaultWindowsNpmCliCandidates, listSystemCliCandidates } from "./cli-detection-utils.mjs";
|
|
8
|
+
import { writeRuntimeInstallRecord } from "./runtime-state-utils.mjs";
|
|
8
9
|
|
|
9
10
|
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
10
11
|
const USER_ID_PATTERN = /^[a-z](?:\d{8}|wx\d{7})$/;
|
|
@@ -67,46 +68,63 @@ function pythonExecutableInVenv(venvDir) {
|
|
|
67
68
|
: path.join(venvDir, "bin", "python");
|
|
68
69
|
}
|
|
69
70
|
|
|
70
|
-
function
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
71
|
+
function pythonEnv(extraPythonPath = "") {
|
|
72
|
+
const env = { ...process.env, PYTHONUTF8: "1" };
|
|
73
|
+
if (extraPythonPath) {
|
|
74
|
+
env.PYTHONPATH = process.env.PYTHONPATH ? `${extraPythonPath}${path.delimiter}${process.env.PYTHONPATH}` : extraPythonPath;
|
|
75
|
+
}
|
|
76
|
+
return env;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function pythonCanImport(pythonCmd, moduleName, extraPythonPath = "") {
|
|
80
|
+
try {
|
|
81
|
+
execFileSync(pythonCmd, ["-c", `import ${moduleName}`], { stdio: "ignore", encoding: "utf8", env: pythonEnv(extraPythonPath) });
|
|
82
|
+
return true;
|
|
83
|
+
} catch {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function installLangfuseIntoTargetDir({ pythonCmd, baseDir, pipIndexUrl }) {
|
|
89
|
+
const targetDir = path.join(baseDir, "langfuse-python");
|
|
90
|
+
ensureDir(targetDir);
|
|
91
|
+
console.log(`Installing Python package into isolated target: ${targetDir}`);
|
|
92
|
+
execFileSync(
|
|
93
|
+
pythonCmd,
|
|
94
|
+
["-m", "pip", "install", "--target", targetDir, "--upgrade", "langfuse", "-i", pipIndexUrl],
|
|
95
|
+
{ stdio: "inherit", encoding: "utf8", env: pythonEnv() }
|
|
96
|
+
);
|
|
97
|
+
if (!pythonCanImport(pythonCmd, "langfuse", targetDir)) {
|
|
98
|
+
throw new Error(`langfuse was installed into ${targetDir}, but ${pythonCmd} still cannot import it with PYTHONPATH.`);
|
|
99
|
+
}
|
|
100
|
+
return { python: pythonCmd, pythonPath: targetDir };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function runPipInstallWithFallback({ pythonCmd, baseDir, pipIndexUrl }) {
|
|
104
|
+
const attempts = [
|
|
105
|
+
() => installLangfuseIntoTargetDir({ pythonCmd, baseDir, pipIndexUrl }),
|
|
106
|
+
() => installLangfuseIntoTargetDir({ pythonCmd: "python3", baseDir, pipIndexUrl }),
|
|
107
|
+
() => installLangfuseIntoTargetDir({ pythonCmd: "python", baseDir, pipIndexUrl })
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
const errors = [];
|
|
111
|
+
for (const attempt of attempts) {
|
|
112
|
+
try {
|
|
113
|
+
return attempt();
|
|
114
|
+
} catch (error) {
|
|
115
|
+
errors.push(error?.message || String(error));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
throw new Error(
|
|
120
|
+
`Failed to install langfuse into isolated Python target. Last errors: ${errors.join(" | ")}`
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function installLangfuseWithSystemPython({ pythonCmd, baseDir, pipIndexUrl }) {
|
|
125
|
+
console.log("Python venv is unavailable; falling back to isolated PYTHONPATH install for langfuse.");
|
|
126
|
+
return runPipInstallWithFallback({ pythonCmd, baseDir, pipIndexUrl });
|
|
127
|
+
}
|
|
110
128
|
|
|
111
129
|
function shQuote(s) {
|
|
112
130
|
return `'${String(s).replace(/'/g, "'\"'\"'")}'`;
|
|
@@ -217,19 +235,11 @@ function listLocalCodexCliCandidates() {
|
|
|
217
235
|
return candidates;
|
|
218
236
|
}
|
|
219
237
|
|
|
220
|
-
function listCliCandidatesFromPath(target) {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
const candidates = String(result.stdout || "")
|
|
226
|
-
.split(/\r?\n/)
|
|
227
|
-
.map((line) => line.trim())
|
|
228
|
-
.filter(Boolean);
|
|
229
|
-
return process.platform === "win32" ? sortWindowsCliCandidates(candidates) : candidates;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function sortWindowsCliCandidates(candidates) {
|
|
238
|
+
function listCliCandidatesFromPath(target) {
|
|
239
|
+
return listSystemCliCandidates(target);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function sortWindowsCliCandidates(candidates) {
|
|
233
243
|
const extPriority = (candidate) => {
|
|
234
244
|
const ext = path.extname(String(candidate || "")).toLowerCase();
|
|
235
245
|
if (ext === ".cmd" || ext === ".bat" || ext === ".exe") return 0;
|
|
@@ -238,16 +248,13 @@ function sortWindowsCliCandidates(candidates) {
|
|
|
238
248
|
return [...candidates].sort((a, b) => extPriority(a) - extPriority(b));
|
|
239
249
|
}
|
|
240
250
|
|
|
241
|
-
function resolveAgentCli({ target, preferred = "", shimDir = "" }) {
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
...(target
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
process.platform === "win32" ? path.join(appData, "npm", target) : "",
|
|
249
|
-
...listCliCandidatesFromPath(target)
|
|
250
|
-
];
|
|
251
|
+
function resolveAgentCli({ target, preferred = "", shimDir = "" }) {
|
|
252
|
+
const candidates = [
|
|
253
|
+
preferred,
|
|
254
|
+
...(target === "codex" ? listLocalCodexCliCandidates() : []),
|
|
255
|
+
...defaultWindowsNpmCliCandidates(target),
|
|
256
|
+
...listCliCandidatesFromPath(target)
|
|
257
|
+
];
|
|
251
258
|
for (const candidate of candidates) {
|
|
252
259
|
const found = existingCliCandidate(candidate, shimDir);
|
|
253
260
|
if (found) return found;
|
|
@@ -358,13 +365,13 @@ function createOrUpdateLangfuseVenv({ baseDir, pipIndexUrl = "https://pypi.tuna.
|
|
|
358
365
|
if (!fs.existsSync(venvPython)) {
|
|
359
366
|
console.log(`Creating Python virtual environment: ${venvDir}`);
|
|
360
367
|
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
|
-
}
|
|
368
|
+
execFileSync(pythonCmd, ["-m", "venv", venvDir], { stdio: "inherit", encoding: "utf8", env: { ...process.env, PYTHONUTF8: "1" } });
|
|
369
|
+
} catch (e) {
|
|
370
|
+
if (process.platform !== "win32") {
|
|
371
|
+
return installLangfuseWithSystemPython({ pythonCmd, baseDir, pipIndexUrl });
|
|
372
|
+
}
|
|
373
|
+
throw new Error("Failed to create Python venv. Please confirm Python venv support is available.");
|
|
374
|
+
}
|
|
368
375
|
}
|
|
369
376
|
|
|
370
377
|
console.log("Installing/updating Python package in venv: langfuse");
|
|
@@ -373,16 +380,31 @@ function createOrUpdateLangfuseVenv({ baseDir, pipIndexUrl = "https://pypi.tuna.
|
|
|
373
380
|
venvPython,
|
|
374
381
|
["-m", "pip", "install", "-U", "langfuse", "-i", pipIndexUrl],
|
|
375
382
|
{ 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
|
-
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
383
|
+
);
|
|
384
|
+
} catch (e) {
|
|
385
|
+
if (process.platform !== "win32") {
|
|
386
|
+
return installLangfuseWithSystemPython({ pythonCmd, baseDir, pipIndexUrl });
|
|
387
|
+
}
|
|
388
|
+
throw new Error(`Failed to install langfuse in venv: ${venvPython} -m pip install -U langfuse -i ${pipIndexUrl}`);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (!pythonCanImport(venvPython, "langfuse")) {
|
|
392
|
+
if (process.platform !== "win32") {
|
|
393
|
+
return installLangfuseWithSystemPython({ pythonCmd, baseDir, pipIndexUrl });
|
|
394
|
+
}
|
|
395
|
+
throw new Error(`langfuse was installed in venv, but cannot be imported by ${venvPython}`);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return { python: venvPython, pythonPath: "" };
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function codexNotifyCommand({ python, pythonPath = "", hookPath }) {
|
|
402
|
+
if (pythonPath && process.platform !== "win32") {
|
|
403
|
+
const script = `if [ -n "\${PYTHONPATH:-}" ]; then export PYTHONPATH=${shQuote(pythonPath)}:"$PYTHONPATH"; else export PYTHONPATH=${shQuote(pythonPath)}; fi; exec ${shQuote(python)} ${shQuote(hookPath)}`;
|
|
404
|
+
return ["sh", "-c", script];
|
|
405
|
+
}
|
|
406
|
+
return [python, hookPath];
|
|
407
|
+
}
|
|
386
408
|
|
|
387
409
|
function updateCodexNotify(configPath, notifyCommand) {
|
|
388
410
|
let content = fs.existsSync(configPath) ? fs.readFileSync(configPath, "utf8") : "";
|
|
@@ -483,11 +505,11 @@ async function main() {
|
|
|
483
505
|
userId,
|
|
484
506
|
});
|
|
485
507
|
|
|
486
|
-
const pythonCmd = process.platform === "win32" ? "python" : "python3";
|
|
487
|
-
const
|
|
488
|
-
? pythonCmd
|
|
489
|
-
: createOrUpdateLangfuseVenv({ baseDir: codexHome, pipIndexUrl });
|
|
490
|
-
updateCodexNotify(configPath,
|
|
508
|
+
const pythonCmd = process.platform === "win32" ? "python" : "python3";
|
|
509
|
+
const notifyRuntime = args["skip-pip-install"]
|
|
510
|
+
? { python: pythonCmd, pythonPath: "" }
|
|
511
|
+
: createOrUpdateLangfuseVenv({ baseDir: codexHome, pipIndexUrl });
|
|
512
|
+
updateCodexNotify(configPath, codexNotifyCommand({ python: notifyRuntime.python, pythonPath: notifyRuntime.pythonPath, hookPath: destHook }));
|
|
491
513
|
const codexShimDir = path.join(codexHome, "bin");
|
|
492
514
|
const realCodexCli = resolveAgentCli({ target: "codex", preferred: args.cmd || "", shimDir: codexShimDir });
|
|
493
515
|
const directShim = writeAgentCommandShim({
|
|
@@ -14,11 +14,19 @@ function addResult(results, item, ok, detail, fix = "") {
|
|
|
14
14
|
results.push({ item, ok, detail, fix });
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
function pythonExecutableInVenv(claudeDir) {
|
|
18
|
-
return process.platform === "win32"
|
|
19
|
-
? path.join(claudeDir, "langfuse-venv", "Scripts", "python.exe")
|
|
20
|
-
: path.join(claudeDir, "langfuse-venv", "bin", "python");
|
|
21
|
-
}
|
|
17
|
+
function pythonExecutableInVenv(claudeDir) {
|
|
18
|
+
return process.platform === "win32"
|
|
19
|
+
? path.join(claudeDir, "langfuse-venv", "Scripts", "python.exe")
|
|
20
|
+
: path.join(claudeDir, "langfuse-venv", "bin", "python");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function fallbackPythonCommand() {
|
|
24
|
+
return process.platform === "win32" ? "python" : "python3";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isolatedPythonPath(claudeDir) {
|
|
28
|
+
return path.join(claudeDir, "langfuse-python");
|
|
29
|
+
}
|
|
22
30
|
|
|
23
31
|
function launcherPath(hooksDir) {
|
|
24
32
|
return process.platform === "win32"
|
|
@@ -63,13 +71,18 @@ function pathExists(p) {
|
|
|
63
71
|
return typeof p === "string" && p.length > 0 && fs.existsSync(p);
|
|
64
72
|
}
|
|
65
73
|
|
|
66
|
-
function checkPythonLangfuseImport(pythonPath) {
|
|
67
|
-
if (!pathExists(pythonPath)) return { ok: false, detail: "python executable missing" };
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
74
|
+
function checkPythonLangfuseImport(pythonPath, extraPythonPath = "") {
|
|
75
|
+
if (!pathExists(pythonPath) && pythonPath !== "python3" && pythonPath !== "python") return { ok: false, detail: "python executable missing" };
|
|
76
|
+
const env = { ...process.env };
|
|
77
|
+
if (extraPythonPath) {
|
|
78
|
+
env.PYTHONPATH = process.env.PYTHONPATH ? `${extraPythonPath}${path.delimiter}${process.env.PYTHONPATH}` : extraPythonPath;
|
|
79
|
+
}
|
|
80
|
+
const result = spawnSync(pythonPath, ["-c", "import langfuse; print('OK')"], {
|
|
81
|
+
encoding: "utf8",
|
|
82
|
+
timeout: 15000,
|
|
83
|
+
windowsHide: true,
|
|
84
|
+
env
|
|
85
|
+
});
|
|
73
86
|
if (result.status === 0) return { ok: true, detail: "OK" };
|
|
74
87
|
const err = `${result.stderr || result.stdout || result.error?.message || "unknown error"}`.trim();
|
|
75
88
|
return { ok: false, detail: err || "import failed" };
|
|
@@ -80,10 +93,11 @@ function main() {
|
|
|
80
93
|
const claudeDir = path.join(userHome, ".claude");
|
|
81
94
|
const hooksDir = path.join(claudeDir, "hooks");
|
|
82
95
|
const settingsPath = path.join(claudeDir, "settings.json");
|
|
83
|
-
const pyPath = path.join(hooksDir, "langfuse_hook.py");
|
|
84
|
-
const expectedLauncher = launcherPath(hooksDir);
|
|
85
|
-
const expectedPython = pythonExecutableInVenv(claudeDir);
|
|
86
|
-
const
|
|
96
|
+
const pyPath = path.join(hooksDir, "langfuse_hook.py");
|
|
97
|
+
const expectedLauncher = launcherPath(hooksDir);
|
|
98
|
+
const expectedPython = pythonExecutableInVenv(claudeDir);
|
|
99
|
+
const fallbackTarget = isolatedPythonPath(claudeDir);
|
|
100
|
+
const logPath = path.join(claudeDir, "state", "langfuse_hook.log");
|
|
87
101
|
|
|
88
102
|
const results = [];
|
|
89
103
|
|
|
@@ -138,22 +152,26 @@ function main() {
|
|
|
138
152
|
"Run setup again. Claude Code should execute the generated Python hook command."
|
|
139
153
|
);
|
|
140
154
|
|
|
141
|
-
addResult(
|
|
142
|
-
results,
|
|
143
|
-
"hook python executable",
|
|
144
|
-
pathExists(expectedPython),
|
|
145
|
-
expectedPython
|
|
146
|
-
"Run setup again to recreate the Python venv."
|
|
147
|
-
);
|
|
148
|
-
|
|
149
|
-
const importCheck =
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
155
|
+
addResult(
|
|
156
|
+
results,
|
|
157
|
+
"hook python executable",
|
|
158
|
+
pathExists(expectedPython) || fs.existsSync(fallbackTarget),
|
|
159
|
+
pathExists(expectedPython) ? expectedPython : `${fallbackPythonCommand()} with PYTHONPATH=${fallbackTarget}`,
|
|
160
|
+
"Run setup again to recreate the Python venv."
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
const importCheck = pathExists(expectedPython)
|
|
164
|
+
? checkPythonLangfuseImport(expectedPython)
|
|
165
|
+
: checkPythonLangfuseImport(fallbackPythonCommand(), fallbackTarget);
|
|
166
|
+
addResult(
|
|
167
|
+
results,
|
|
168
|
+
"python package langfuse",
|
|
169
|
+
importCheck.ok,
|
|
170
|
+
importCheck.detail,
|
|
171
|
+
pathExists(expectedPython)
|
|
172
|
+
? `Run: ${expectedPython} -m pip install -U langfuse`
|
|
173
|
+
: "Run setup again to install langfuse into the isolated PYTHONPATH target."
|
|
174
|
+
);
|
|
157
175
|
|
|
158
176
|
addResult(results, "hook log path", true, logPath);
|
|
159
177
|
|
|
@@ -2,9 +2,10 @@ import fs from "node:fs";
|
|
|
2
2
|
import fsp from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import os from "node:os";
|
|
5
|
-
import { execFileSync, spawnSync } from "node:child_process";
|
|
6
|
-
import { fileURLToPath } from "node:url";
|
|
7
|
-
import {
|
|
5
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { defaultWindowsNpmCliCandidates, listSystemCliCandidates } from "./cli-detection-utils.mjs";
|
|
8
|
+
import { writeRuntimeInstallRecord } from "./runtime-state-utils.mjs";
|
|
8
9
|
|
|
9
10
|
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
10
11
|
const packageJson = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8"));
|
|
@@ -221,26 +222,16 @@ function existingCliCandidate(candidate, shimDir) {
|
|
|
221
222
|
}
|
|
222
223
|
}
|
|
223
224
|
|
|
224
|
-
function listCliCandidatesFromPath(target) {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
function resolveAgentCli({ target, preferred = "", shimDir = "" }) {
|
|
236
|
-
const appData = process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming");
|
|
237
|
-
const candidates = [
|
|
238
|
-
preferred,
|
|
239
|
-
process.platform === "win32" ? path.join(appData, "npm", `${target}.cmd`) : "",
|
|
240
|
-
process.platform === "win32" ? path.join(appData, "npm", `${target}.exe`) : "",
|
|
241
|
-
process.platform === "win32" ? path.join(appData, "npm", target) : "",
|
|
242
|
-
...listCliCandidatesFromPath(target)
|
|
243
|
-
];
|
|
225
|
+
function listCliCandidatesFromPath(target) {
|
|
226
|
+
return listSystemCliCandidates(target);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function resolveAgentCli({ target, preferred = "", shimDir = "" }) {
|
|
230
|
+
const candidates = [
|
|
231
|
+
preferred,
|
|
232
|
+
...defaultWindowsNpmCliCandidates(target),
|
|
233
|
+
...listCliCandidatesFromPath(target)
|
|
234
|
+
];
|
|
244
235
|
for (const candidate of candidates) {
|
|
245
236
|
const found = existingCliCandidate(candidate, shimDir);
|
|
246
237
|
if (found) return found;
|
|
@@ -349,68 +340,87 @@ function pythonExecutableInVenv(venvDir) {
|
|
|
349
340
|
: path.join(venvDir, "bin", "python");
|
|
350
341
|
}
|
|
351
342
|
|
|
352
|
-
function
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
function
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
343
|
+
function pythonEnv(extraPythonPath = "") {
|
|
344
|
+
const env = { ...process.env, PYTHONUTF8: "1" };
|
|
345
|
+
if (extraPythonPath) {
|
|
346
|
+
env.PYTHONPATH = process.env.PYTHONPATH ? `${extraPythonPath}${path.delimiter}${process.env.PYTHONPATH}` : extraPythonPath;
|
|
347
|
+
}
|
|
348
|
+
return env;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function pythonCanImport(pythonCmd, moduleName, extraPythonPath = "") {
|
|
352
|
+
try {
|
|
353
|
+
execFileSync(pythonCmd, ["-c", `import ${moduleName}`], { stdio: "ignore", encoding: "utf8", env: pythonEnv(extraPythonPath) });
|
|
354
|
+
return true;
|
|
355
|
+
} catch {
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function installLangfuseIntoTargetDir({ pythonCmd, baseDir, pipIndexUrl }) {
|
|
361
|
+
const targetDir = path.join(baseDir, "langfuse-python");
|
|
362
|
+
ensureDir(targetDir);
|
|
363
|
+
console.log(`Installing Python package into isolated target: ${targetDir}`);
|
|
364
|
+
execFileSync(
|
|
365
|
+
pythonCmd,
|
|
366
|
+
["-m", "pip", "install", "--target", targetDir, "--upgrade", "langfuse", "-i", pipIndexUrl],
|
|
367
|
+
{ stdio: "inherit", encoding: "utf8", env: pythonEnv() }
|
|
368
|
+
);
|
|
369
|
+
if (!pythonCanImport(pythonCmd, "langfuse", targetDir)) {
|
|
370
|
+
throw new Error(`langfuse was installed into ${targetDir}, but ${pythonCmd} still cannot import it with PYTHONPATH.`);
|
|
371
|
+
}
|
|
372
|
+
return { python: pythonCmd, pythonPath: targetDir };
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function runPipInstallWithFallback({ pythonCmd, baseDir, pipIndexUrl }) {
|
|
376
|
+
const attempts = [
|
|
377
|
+
() => installLangfuseIntoTargetDir({ pythonCmd, baseDir, pipIndexUrl }),
|
|
378
|
+
() => installLangfuseIntoTargetDir({ pythonCmd: "python3", baseDir, pipIndexUrl }),
|
|
379
|
+
() => installLangfuseIntoTargetDir({ pythonCmd: "python", baseDir, pipIndexUrl })
|
|
380
|
+
];
|
|
381
|
+
|
|
382
|
+
const errors = [];
|
|
383
|
+
for (const attempt of attempts) {
|
|
384
|
+
try {
|
|
385
|
+
return attempt();
|
|
386
|
+
} catch (error) {
|
|
387
|
+
errors.push(error?.message || String(error));
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
throw new Error(
|
|
392
|
+
`Failed to install langfuse into isolated Python target. Last errors: ${errors.join(" | ")}`
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function installLangfuseWithSystemPython({ pythonCmd, baseDir, pipIndexUrl }) {
|
|
397
|
+
console.log("Python venv is unavailable; falling back to isolated PYTHONPATH install for langfuse.");
|
|
398
|
+
return runPipInstallWithFallback({ pythonCmd, baseDir, pipIndexUrl });
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
async function createHookLauncher({ hooksDir, hookPython, hookPythonPath = "", pyPath }) {
|
|
402
|
+
if (process.platform === "win32") {
|
|
403
|
+
const launcher = path.join(hooksDir, "run-langfuse-hook.cmd");
|
|
404
|
+
const content = [
|
|
405
|
+
"@echo off",
|
|
406
|
+
hookPythonPath ? `set PYTHONPATH=${hookPythonPath};%PYTHONPATH%` : null,
|
|
407
|
+
`${cmdQuote(hookPython)} ${cmdQuote(pyPath)}`,
|
|
408
|
+
""
|
|
409
|
+
].filter(Boolean).join(os.EOL);
|
|
401
410
|
await fsp.writeFile(launcher, content, "utf8");
|
|
402
|
-
return launcher;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
const launcher = path.join(hooksDir, "run-langfuse-hook.sh");
|
|
406
|
-
const content = [
|
|
407
|
-
"#!/usr/bin/env sh",
|
|
408
|
-
`
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
411
|
+
return launcher;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const launcher = path.join(hooksDir, "run-langfuse-hook.sh");
|
|
415
|
+
const content = [
|
|
416
|
+
"#!/usr/bin/env sh",
|
|
417
|
+
hookPythonPath ? `if [ -n "\${PYTHONPATH:-}" ]; then export PYTHONPATH=${shQuote(hookPythonPath)}:"$PYTHONPATH"; else export PYTHONPATH=${shQuote(hookPythonPath)}; fi` : null,
|
|
418
|
+
`exec ${shQuote(hookPython)} ${shQuote(pyPath)}`,
|
|
419
|
+
""
|
|
420
|
+
].filter(Boolean).join("\n");
|
|
421
|
+
await fsp.writeFile(launcher, content, "utf8");
|
|
422
|
+
fs.chmodSync(launcher, 0o755);
|
|
423
|
+
return launcher;
|
|
414
424
|
}
|
|
415
425
|
|
|
416
426
|
function createOrUpdateLangfuseVenv({ baseDir, pipIndexUrl = "https://pypi.tuna.tsinghua.edu.cn/simple" }) {
|
|
@@ -421,13 +431,13 @@ function createOrUpdateLangfuseVenv({ baseDir, pipIndexUrl = "https://pypi.tuna.
|
|
|
421
431
|
if (!fs.existsSync(venvPython)) {
|
|
422
432
|
console.log(`Creating Python virtual environment: ${venvDir}`);
|
|
423
433
|
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
|
-
}
|
|
434
|
+
execFileSync(pythonCmd, ["-m", "venv", venvDir], { stdio: "inherit", encoding: "utf8", env: { ...process.env, PYTHONUTF8: "1" } });
|
|
435
|
+
} catch (e) {
|
|
436
|
+
if (process.platform !== "win32") {
|
|
437
|
+
return installLangfuseWithSystemPython({ pythonCmd, baseDir, pipIndexUrl });
|
|
438
|
+
}
|
|
439
|
+
throw new Error("Failed to create Python venv. Please confirm Python venv support is available.");
|
|
440
|
+
}
|
|
431
441
|
}
|
|
432
442
|
|
|
433
443
|
console.log("Installing/updating Python package in venv: langfuse");
|
|
@@ -436,16 +446,23 @@ function createOrUpdateLangfuseVenv({ baseDir, pipIndexUrl = "https://pypi.tuna.
|
|
|
436
446
|
venvPython,
|
|
437
447
|
["-m", "pip", "install", "-U", "langfuse", "-i", pipIndexUrl],
|
|
438
448
|
{ 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
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
+
);
|
|
450
|
+
} catch (e) {
|
|
451
|
+
if (process.platform !== "win32") {
|
|
452
|
+
return installLangfuseWithSystemPython({ pythonCmd, baseDir, pipIndexUrl });
|
|
453
|
+
}
|
|
454
|
+
throw new Error(`Failed to install langfuse in venv: ${venvPython} -m pip install -U langfuse -i ${pipIndexUrl}`);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (!pythonCanImport(venvPython, "langfuse")) {
|
|
458
|
+
if (process.platform !== "win32") {
|
|
459
|
+
return installLangfuseWithSystemPython({ pythonCmd, baseDir, pipIndexUrl });
|
|
460
|
+
}
|
|
461
|
+
throw new Error(`langfuse was installed in venv, but cannot be imported by ${venvPython}`);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
return { python: venvPython, pythonPath: "" };
|
|
465
|
+
}
|
|
449
466
|
|
|
450
467
|
async function main() {
|
|
451
468
|
const args = parseArgs(process.argv.slice(2));
|
|
@@ -526,8 +543,9 @@ async function main() {
|
|
|
526
543
|
if (nextPyText !== pyText) {
|
|
527
544
|
await fsp.writeFile(pyPath, nextPyText, "utf8");
|
|
528
545
|
}
|
|
529
|
-
const
|
|
530
|
-
const
|
|
546
|
+
const hookRuntime = createOrUpdateLangfuseVenv({ baseDir: claudeDir, pipIndexUrl });
|
|
547
|
+
const hookPython = hookRuntime.python;
|
|
548
|
+
const hookLauncher = await createHookLauncher({ hooksDir, hookPython, hookPythonPath: hookRuntime.pythonPath, pyPath });
|
|
531
549
|
const claudeShimDir = path.join(claudeDir, "bin");
|
|
532
550
|
const realClaudeCli = resolveAgentCli({ target: "claude", preferred: args.cmd || "", shimDir: claudeShimDir });
|
|
533
551
|
const directShim = writeAgentCommandShim({
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import os from "node:os";
|
|
4
|
-
import { spawnSync } from "node:child_process";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { listSystemCliCandidates } from "./cli-detection-utils.mjs";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* OpenCode CLI 的官方安装脚本默认会把二进制放到 ~/.opencode/bin;
|
|
@@ -36,10 +37,11 @@ export function resolveOpencodeCli(preferred) {
|
|
|
36
37
|
candidates.push(path.join(process.env.APPDATA, "npm", "node_modules", "opencode-windows-x64-baseline", "bin", "opencode.exe"));
|
|
37
38
|
}
|
|
38
39
|
candidates.push(path.join(home, "scoop", "shims", "opencode.exe"));
|
|
39
|
-
} else {
|
|
40
|
-
candidates.push(path.join(home, ".opencode", "bin", "opencode"));
|
|
41
|
-
candidates.push(path.join(home, ".local", "bin", "opencode"));
|
|
42
|
-
|
|
40
|
+
} else {
|
|
41
|
+
candidates.push(path.join(home, ".opencode", "bin", "opencode"));
|
|
42
|
+
candidates.push(path.join(home, ".local", "bin", "opencode"));
|
|
43
|
+
candidates.push(...listSystemCliCandidates("opencode"));
|
|
44
|
+
}
|
|
43
45
|
|
|
44
46
|
for (const c of candidates) {
|
|
45
47
|
if (c && fs.existsSync(c)) return c;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { spawnSync } from "node:child_process";
|
|
5
|
-
import { fileURLToPath } from "node:url";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { hasSystemCli } from "./cli-detection-utils.mjs";
|
|
7
|
+
import { buildUpdatePlan, extractVersionFromNpmMetadata } from "./update-utils.mjs";
|
|
8
|
+
import { writeRuntimeInstallRecord } from "./runtime-state-utils.mjs";
|
|
8
9
|
|
|
9
10
|
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
10
11
|
const packageJson = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8"));
|
|
@@ -59,16 +60,21 @@ function readJsonIfExists(p) {
|
|
|
59
60
|
}
|
|
60
61
|
}
|
|
61
62
|
|
|
62
|
-
function detectInstalledTargets(home = os.homedir()) {
|
|
63
|
-
const codexHome = process.env.CODEX_HOME || path.join(home, ".codex");
|
|
64
|
-
return {
|
|
65
|
-
claude:
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
63
|
+
function detectInstalledTargets(home = os.homedir()) {
|
|
64
|
+
const codexHome = process.env.CODEX_HOME || path.join(home, ".codex");
|
|
65
|
+
return {
|
|
66
|
+
claude:
|
|
67
|
+
fs.existsSync(path.join(home, ".claude", "hooks", "langfuse_hook.py")) ||
|
|
68
|
+
hasSystemCli("claude"),
|
|
69
|
+
opencode:
|
|
70
|
+
fs.existsSync(path.join(home, ".config", "opencode", "opencode.json")) ||
|
|
71
|
+
fs.existsSync(path.join(home, ".config", "opencode", "plugins", "opencode-plugin-langfuse")) ||
|
|
72
|
+
hasSystemCli("opencode"),
|
|
73
|
+
codex:
|
|
74
|
+
fs.existsSync(path.join(codexHome, "hooks", "codex_langfuse_notify.py")) ||
|
|
75
|
+
hasSystemCli("codex"),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
72
78
|
|
|
73
79
|
function claudeConfig(home) {
|
|
74
80
|
const settings = readJsonIfExists(path.join(home, ".claude", "settings.json")) || {};
|