oh-langfuse 0.1.31 → 0.1.33
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 +37 -1
- package/bin/cli.js +11 -1
- package/codex_langfuse_notify.py +2 -1
- package/langfuse_hook.py +2 -1
- package/package.json +4 -2
- package/scripts/auto-update-runtime.mjs +112 -0
- package/scripts/codex-langfuse-setup.mjs +61 -17
- package/scripts/langfuse-setup.mjs +44 -4
- package/scripts/opencode-langfuse-setup.mjs +43 -7
- package/scripts/runtime-state-utils.mjs +53 -0
- package/scripts/update-langfuse-runtime.mjs +5 -0
- package/scripts/update-utils.mjs +27 -0
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
`oh-langfuse` 是用于给 Claude Code、OpenCode 和 Codex 配置 Langfuse 追踪的命令行工具。它提供交互式安装向导,也支持 `setup` / `check` 直接命令,方便在用户机器上安装、修复和校验配置。
|
|
4
4
|
|
|
5
|
-
当前 npm 版本:`0.1.
|
|
5
|
+
当前 npm 版本:`0.1.33`
|
|
6
6
|
|
|
7
7
|
## 能做什么
|
|
8
8
|
|
|
@@ -34,6 +34,42 @@ npx oh-langfuse@latest check opencode
|
|
|
34
34
|
npx oh-langfuse@latest check codex
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
+
## 自动更新
|
|
38
|
+
|
|
39
|
+
安装或更新 runtime 后,工具会在 `~/.config/oh-langfuse/runtime.json` 记录 Claude Code / OpenCode / Codex 当前写入本机 runtime 的 `oh-langfuse` 版本。
|
|
40
|
+
|
|
41
|
+
OpenCode 生成的 `launch-opencode-langfuse.*` 会在启动 agent 前执行一次更新检测:如果 npm 上的 `oh-langfuse@latest` 高于本机 runtime 版本,会提示是否更新;用户确认后才会运行 `npx oh-langfuse@latest update opencode`。Claude Code 和 Codex 也会生成对应 launcher:
|
|
42
|
+
|
|
43
|
+
- `~/.claude/launch-claude-langfuse.cmd` / `~/.claude/launch-claude-langfuse.sh`
|
|
44
|
+
- `~/.codex/launch-codex-langfuse.cmd` / `~/.codex/launch-codex-langfuse.sh`
|
|
45
|
+
|
|
46
|
+
如果直接运行系统原始的 `claude` / `codex` 命令,工具不会强行替换该命令;需要使用上述 launcher 才能做到启动前检测并提示更新。也可以手动运行:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npx oh-langfuse@latest auto-update opencode
|
|
50
|
+
npx oh-langfuse@latest auto-update claude
|
|
51
|
+
npx oh-langfuse@latest auto-update codex
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Skill 使用量统计
|
|
55
|
+
|
|
56
|
+
工具会为每次识别到的 skill 使用额外写入一条 observation:
|
|
57
|
+
|
|
58
|
+
```text
|
|
59
|
+
Skill Use / <skill_name>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Dashboard 中统计各 skill 使用量可配置:
|
|
63
|
+
|
|
64
|
+
```text
|
|
65
|
+
View: Observations
|
|
66
|
+
Metric: Count
|
|
67
|
+
Filter: Observation Name contains Skill Use /
|
|
68
|
+
Breakdown Dimension: Observation Name
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`AI Interaction` 仍然保留 `skill_use_count`、`skill_names`、`skill_names_json` 和 `skill_names_csv`,用于查看单次交互的效率汇总。为避免极端情况下产生过多 observation,OpenCode 每次交互最多记录 20 个去重后的 skill 使用事件。
|
|
72
|
+
|
|
37
73
|
本地开发运行:
|
|
38
74
|
|
|
39
75
|
```bash
|
package/bin/cli.js
CHANGED
|
@@ -796,7 +796,8 @@ function printHelp() {
|
|
|
796
796
|
"oh-langfuse update all",
|
|
797
797
|
"oh-langfuse update claude",
|
|
798
798
|
"oh-langfuse update opencode",
|
|
799
|
-
"oh-langfuse update codex"
|
|
799
|
+
"oh-langfuse update codex",
|
|
800
|
+
"oh-langfuse auto-update opencode"
|
|
800
801
|
]);
|
|
801
802
|
renderSection("Options", [
|
|
802
803
|
`${paint("--dry-run", t.gold)} Preview actions without writing files or installing packages.`,
|
|
@@ -856,6 +857,15 @@ async function main() {
|
|
|
856
857
|
];
|
|
857
858
|
return runNodeScript("update-langfuse-runtime.mjs", updateArgs, options);
|
|
858
859
|
}
|
|
860
|
+
if (cmd === "auto-update") {
|
|
861
|
+
return runNodeScript("auto-update-runtime.mjs", [
|
|
862
|
+
target || "all",
|
|
863
|
+
...(hasValue(options.npmRegistry) ? [`--npmRegistry=${options.npmRegistry}`] : []),
|
|
864
|
+
...(hasValue(options.pipIndexUrl) ? [`--pipIndexUrl=${options.pipIndexUrl}`] : []),
|
|
865
|
+
...(options.skipCheck ? ["--skip-check"] : []),
|
|
866
|
+
...(options.yes ? ["--yes"] : []),
|
|
867
|
+
], options);
|
|
868
|
+
}
|
|
859
869
|
if (cmd === "check" && target === "claude") return checkClaude(options);
|
|
860
870
|
if (cmd === "check" && target === "opencode") return checkOpenCode(options);
|
|
861
871
|
if (cmd === "check" && target === "codex") return checkCodex(options);
|
package/codex_langfuse_notify.py
CHANGED
|
@@ -655,13 +655,14 @@ def emit_codex_turn(
|
|
|
655
655
|
|
|
656
656
|
for skill in skill_usages:
|
|
657
657
|
with langfuse.start_as_current_observation(
|
|
658
|
-
name=f"Skill Use
|
|
658
|
+
name=f"Skill Use / {skill['name']}",
|
|
659
659
|
metadata={
|
|
660
660
|
"source": "codex",
|
|
661
661
|
"user_id": user_id or "",
|
|
662
662
|
"session_id": session_id,
|
|
663
663
|
"interaction_id": interaction_meta["interaction_id"],
|
|
664
664
|
"skill_name": skill["name"],
|
|
665
|
+
"skill_use_count": 1,
|
|
665
666
|
"skill_namespace": skill["skill_namespace"],
|
|
666
667
|
"detected_by": skill["detected_by"],
|
|
667
668
|
"turn_number": turn_num,
|
package/langfuse_hook.py
CHANGED
|
@@ -689,13 +689,14 @@ def emit_turn(
|
|
|
689
689
|
|
|
690
690
|
for skill in skill_usages:
|
|
691
691
|
with langfuse.start_as_current_observation(
|
|
692
|
-
name=f"Skill Use
|
|
692
|
+
name=f"Skill Use / {skill['name']}",
|
|
693
693
|
metadata={
|
|
694
694
|
"source": "claude",
|
|
695
695
|
"user_id": user_id or "",
|
|
696
696
|
"session_id": session_id,
|
|
697
697
|
"interaction_id": interaction_meta["interaction_id"],
|
|
698
698
|
"skill_name": skill["name"],
|
|
699
|
+
"skill_use_count": 1,
|
|
699
700
|
"skill_namespace": skill["skill_namespace"],
|
|
700
701
|
"detected_by": skill["detected_by"],
|
|
701
702
|
"turn_number": turn_num,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oh-langfuse",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.33",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Use npm scripts to configure Claude Code / OpenCode / Codex with Langfuse tracing.",
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
"code-tool-langfuse": "bin/cli.js"
|
|
13
13
|
},
|
|
14
14
|
"files": [
|
|
15
|
-
"bin",
|
|
15
|
+
"bin",
|
|
16
|
+
"scripts/auto-update-runtime.mjs",
|
|
16
17
|
"scripts/codex-langfuse-check.mjs",
|
|
17
18
|
"scripts/codex-langfuse-setup.mjs",
|
|
18
19
|
"scripts/json-utils.mjs",
|
|
@@ -25,6 +26,7 @@
|
|
|
25
26
|
"scripts/real-self-verify.mjs",
|
|
26
27
|
"scripts/log-filter-utils.mjs",
|
|
27
28
|
"scripts/metrics-utils.mjs",
|
|
29
|
+
"scripts/runtime-state-utils.mjs",
|
|
28
30
|
"scripts/update-langfuse-runtime.mjs",
|
|
29
31
|
"scripts/update-utils.mjs",
|
|
30
32
|
"langfuse_hook.py",
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import readline from "node:readline";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { extractVersionFromNpmMetadata, isNewerVersion } from "./update-utils.mjs";
|
|
7
|
+
import { getRuntimeInstallRecord } from "./runtime-state-utils.mjs";
|
|
8
|
+
|
|
9
|
+
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
10
|
+
const packageJson = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8"));
|
|
11
|
+
const ALLOWED_TARGETS = new Set(["claude", "opencode", "codex"]);
|
|
12
|
+
|
|
13
|
+
function parseArgs(argv) {
|
|
14
|
+
const args = { _: [] };
|
|
15
|
+
for (const raw of argv) {
|
|
16
|
+
if (!raw.startsWith("--")) {
|
|
17
|
+
args._.push(raw);
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
const eq = raw.indexOf("=");
|
|
21
|
+
if (eq === -1) args[raw.slice(2)] = true;
|
|
22
|
+
else args[raw.slice(2, eq)] = raw.slice(eq + 1);
|
|
23
|
+
}
|
|
24
|
+
return args;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function latestVersion(registry = "https://registry.npmjs.org") {
|
|
28
|
+
const base = registry.replace(/\/+$/, "");
|
|
29
|
+
const response = await fetch(`${base}/oh-langfuse`, { headers: { accept: "application/json" } });
|
|
30
|
+
if (!response.ok) throw new Error(`npm registry ${response.status} ${response.statusText}`);
|
|
31
|
+
return extractVersionFromNpmMetadata(await response.json());
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function question(text) {
|
|
35
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
36
|
+
return new Promise((resolve) => {
|
|
37
|
+
rl.question(text, (answer) => {
|
|
38
|
+
rl.close();
|
|
39
|
+
resolve(String(answer || "").trim());
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function npxCommand() {
|
|
45
|
+
return process.platform === "win32" ? "npx.cmd" : "npx";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function runUpdate(target, args) {
|
|
49
|
+
const updateArgs = ["-y", "oh-langfuse@latest", "update", target];
|
|
50
|
+
if (args["skip-check"]) updateArgs.push("--skip-check");
|
|
51
|
+
if (args.npmRegistry) updateArgs.push(`--npmRegistry=${args.npmRegistry}`);
|
|
52
|
+
if (args.pipIndexUrl) updateArgs.push(`--pipIndexUrl=${args.pipIndexUrl}`);
|
|
53
|
+
const command = npxCommand();
|
|
54
|
+
const result = spawnSync(command, updateArgs, {
|
|
55
|
+
stdio: "inherit",
|
|
56
|
+
shell: process.platform === "win32",
|
|
57
|
+
windowsHide: true,
|
|
58
|
+
timeout: 900000,
|
|
59
|
+
});
|
|
60
|
+
return result.status ?? (result.error ? 1 : 0);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function main() {
|
|
64
|
+
const args = parseArgs(process.argv.slice(2));
|
|
65
|
+
if (/^(0|false|no|off)$/i.test(String(process.env.OH_LANGFUSE_AUTO_UPDATE || ""))) return 0;
|
|
66
|
+
|
|
67
|
+
const target = String(args._[0] || "").trim().toLowerCase();
|
|
68
|
+
if (!ALLOWED_TARGETS.has(target)) {
|
|
69
|
+
throw new Error("Usage: oh-langfuse auto-update <claude|opencode|codex>");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let latest;
|
|
73
|
+
try {
|
|
74
|
+
latest = await latestVersion(args.npmRegistry);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
if (args.verbose) console.log(`[INFO] oh-langfuse update check skipped: ${error.message}`);
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const record = getRuntimeInstallRecord(target);
|
|
81
|
+
const installedVersion = record?.packageVersion || record?.version || "";
|
|
82
|
+
const needsUpdate = installedVersion ? isNewerVersion(latest, installedVersion) : isNewerVersion(latest, packageJson.version);
|
|
83
|
+
if (!needsUpdate && installedVersion) return 0;
|
|
84
|
+
if (!needsUpdate && !installedVersion) return 0;
|
|
85
|
+
|
|
86
|
+
const message = installedVersion
|
|
87
|
+
? `oh-langfuse ${target} runtime update available: ${installedVersion} -> ${latest}.`
|
|
88
|
+
: `oh-langfuse ${target} runtime may need update. Latest package: ${latest}.`;
|
|
89
|
+
|
|
90
|
+
if (args.yes || args.y) {
|
|
91
|
+
console.log(`[INFO] ${message}`);
|
|
92
|
+
const code = runUpdate(target, args);
|
|
93
|
+
return args.strict ? code : 0;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
97
|
+
console.log(`[INFO] ${message} Run: npx oh-langfuse@latest update ${target}`);
|
|
98
|
+
return 0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const answer = await question(`${message}\nUpdate now? (y/N) `);
|
|
102
|
+
if (!/^(y|yes)$/i.test(answer)) return 0;
|
|
103
|
+
const code = runUpdate(target, args);
|
|
104
|
+
return args.strict ? code : 0;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
main()
|
|
108
|
+
.then((code) => process.exit(code))
|
|
109
|
+
.catch((error) => {
|
|
110
|
+
console.error(`[WARN] oh-langfuse auto-update skipped: ${error?.message || String(error)}`);
|
|
111
|
+
process.exit(0);
|
|
112
|
+
});
|
|
@@ -1,9 +1,10 @@
|
|
|
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";
|
|
4
|
+
import os from "node:os";
|
|
5
5
|
import { execFileSync } from "node:child_process";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { writeRuntimeInstallRecord } from "./runtime-state-utils.mjs";
|
|
7
8
|
|
|
8
9
|
const USER_ID_PATTERN = /^[a-z](?:\d{8}|wx\d{7})$/;
|
|
9
10
|
const USER_ID_PATTERN_TEXT = "^[a-z](?:\\d{8}|wx\\d{7})$";
|
|
@@ -57,11 +58,46 @@ function tomlArray(items) {
|
|
|
57
58
|
return `[${items.map(tomlString).join(", ")}]`;
|
|
58
59
|
}
|
|
59
60
|
|
|
60
|
-
function pythonExecutableInVenv(venvDir) {
|
|
61
|
-
return process.platform === "win32"
|
|
62
|
-
? path.join(venvDir, "Scripts", "python.exe")
|
|
63
|
-
: path.join(venvDir, "bin", "python");
|
|
64
|
-
}
|
|
61
|
+
function pythonExecutableInVenv(venvDir) {
|
|
62
|
+
return process.platform === "win32"
|
|
63
|
+
? path.join(venvDir, "Scripts", "python.exe")
|
|
64
|
+
: path.join(venvDir, "bin", "python");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function shQuote(s) {
|
|
68
|
+
return `'${String(s).replace(/'/g, "'\"'\"'")}'`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function createAgentLauncher({ baseDir, target, executable }) {
|
|
72
|
+
if (process.platform === "win32") {
|
|
73
|
+
const launcher = path.join(baseDir, `launch-${target}-langfuse.cmd`);
|
|
74
|
+
const content = [
|
|
75
|
+
"@echo off",
|
|
76
|
+
"where npx >nul 2>nul",
|
|
77
|
+
"if %ERRORLEVEL% EQU 0 (",
|
|
78
|
+
` call npx -y oh-langfuse@latest auto-update ${target} --skip-check`,
|
|
79
|
+
")",
|
|
80
|
+
`${executable} %*`,
|
|
81
|
+
""
|
|
82
|
+
].join(os.EOL);
|
|
83
|
+
fs.writeFileSync(launcher, content, "utf8");
|
|
84
|
+
return launcher;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const launcher = path.join(baseDir, `launch-${target}-langfuse.sh`);
|
|
88
|
+
const content = [
|
|
89
|
+
"#!/usr/bin/env sh",
|
|
90
|
+
"set -eu",
|
|
91
|
+
"if command -v npx >/dev/null 2>&1; then",
|
|
92
|
+
` npx -y oh-langfuse@latest auto-update ${target} --skip-check || true`,
|
|
93
|
+
"fi",
|
|
94
|
+
`exec ${shQuote(executable)} "$@"`,
|
|
95
|
+
""
|
|
96
|
+
].join("\n");
|
|
97
|
+
fs.writeFileSync(launcher, content, "utf8");
|
|
98
|
+
fs.chmodSync(launcher, 0o755);
|
|
99
|
+
return launcher;
|
|
100
|
+
}
|
|
65
101
|
|
|
66
102
|
function createOrUpdateLangfuseVenv({ baseDir, pipIndexUrl = "https://pypi.tuna.tsinghua.edu.cn/simple" }) {
|
|
67
103
|
const venvDir = path.join(baseDir, "langfuse-venv");
|
|
@@ -134,9 +170,10 @@ function updateCodexNotify(configPath, notifyCommand) {
|
|
|
134
170
|
fs.writeFileSync(configPath, next.join(os.EOL), "utf8");
|
|
135
171
|
}
|
|
136
172
|
|
|
137
|
-
async function main() {
|
|
138
|
-
const args = parseArgs(process.argv.slice(2));
|
|
139
|
-
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
173
|
+
async function main() {
|
|
174
|
+
const args = parseArgs(process.argv.slice(2));
|
|
175
|
+
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
176
|
+
const packageJson = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8"));
|
|
140
177
|
|
|
141
178
|
const publicKey =
|
|
142
179
|
args.publicKey || process.env.LANGFUSE_PUBLIC_KEY || "pk-lf-da0c90a7-6e93-4eb7-bb86-c1047c8d187d";
|
|
@@ -188,14 +225,21 @@ async function main() {
|
|
|
188
225
|
const notifyPython = args["skip-pip-install"]
|
|
189
226
|
? pythonCmd
|
|
190
227
|
: createOrUpdateLangfuseVenv({ baseDir: codexHome, pipIndexUrl });
|
|
191
|
-
updateCodexNotify(configPath, [notifyPython, destHook]);
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
console.log(`
|
|
195
|
-
console.log(`
|
|
196
|
-
|
|
197
|
-
console.log(
|
|
198
|
-
|
|
228
|
+
updateCodexNotify(configPath, [notifyPython, destHook]);
|
|
229
|
+
const agentLauncher = createAgentLauncher({ baseDir: codexHome, target: "codex", executable: "codex" });
|
|
230
|
+
|
|
231
|
+
console.log(`Updated Codex notify hook: ${configPath}`);
|
|
232
|
+
console.log(`Installed hook script: ${destHook}`);
|
|
233
|
+
console.log(`Wrote Langfuse config: ${path.join(langfuseDir, "config.json")}`);
|
|
234
|
+
console.log(`Agent launcher with update check: ${agentLauncher}`);
|
|
235
|
+
const runtimeState = writeRuntimeInstallRecord("codex", {
|
|
236
|
+
packageName: packageJson.name,
|
|
237
|
+
packageVersion: packageJson.version,
|
|
238
|
+
});
|
|
239
|
+
console.log(`Runtime version recorded: ${runtimeState}`);
|
|
240
|
+
|
|
241
|
+
console.log("Done. Restart Codex so the updated notify command is loaded.");
|
|
242
|
+
}
|
|
199
243
|
|
|
200
244
|
main().catch((err) => {
|
|
201
245
|
console.error(err?.message || String(err));
|
|
@@ -4,8 +4,10 @@ import path from "node:path";
|
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import { execFileSync } from "node:child_process";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { writeRuntimeInstallRecord } from "./runtime-state-utils.mjs";
|
|
7
8
|
|
|
8
9
|
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
10
|
+
const packageJson = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8"));
|
|
9
11
|
|
|
10
12
|
const USER_ID_PATTERN = /^[a-z](?:\d{8}|wx\d{7})$/;
|
|
11
13
|
const USER_ID_PATTERN_TEXT = "^[a-z](?:\\d{8}|wx\\d{7})$";
|
|
@@ -147,6 +149,37 @@ function shQuote(s) {
|
|
|
147
149
|
return `'${String(s).replace(/'/g, "'\\''")}'`;
|
|
148
150
|
}
|
|
149
151
|
|
|
152
|
+
function createAgentLauncher({ baseDir, target, executable }) {
|
|
153
|
+
if (process.platform === "win32") {
|
|
154
|
+
const launcher = path.join(baseDir, `launch-${target}-langfuse.cmd`);
|
|
155
|
+
const content = [
|
|
156
|
+
"@echo off",
|
|
157
|
+
"where npx >nul 2>nul",
|
|
158
|
+
"if %ERRORLEVEL% EQU 0 (",
|
|
159
|
+
` call npx -y oh-langfuse@latest auto-update ${target} --skip-check`,
|
|
160
|
+
")",
|
|
161
|
+
`${executable} %*`,
|
|
162
|
+
""
|
|
163
|
+
].join(os.EOL);
|
|
164
|
+
fs.writeFileSync(launcher, content, "utf8");
|
|
165
|
+
return launcher;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const launcher = path.join(baseDir, `launch-${target}-langfuse.sh`);
|
|
169
|
+
const content = [
|
|
170
|
+
"#!/usr/bin/env sh",
|
|
171
|
+
"set -eu",
|
|
172
|
+
"if command -v npx >/dev/null 2>&1; then",
|
|
173
|
+
` npx -y oh-langfuse@latest auto-update ${target} --skip-check || true`,
|
|
174
|
+
"fi",
|
|
175
|
+
`exec ${shQuote(executable)} "$@"`,
|
|
176
|
+
""
|
|
177
|
+
].join("\n");
|
|
178
|
+
fs.writeFileSync(launcher, content, "utf8");
|
|
179
|
+
fs.chmodSync(launcher, 0o755);
|
|
180
|
+
return launcher;
|
|
181
|
+
}
|
|
182
|
+
|
|
150
183
|
function pythonExecutableInVenv(venvDir) {
|
|
151
184
|
return process.platform === "win32"
|
|
152
185
|
? path.join(venvDir, "Scripts", "python.exe")
|
|
@@ -288,6 +321,7 @@ async function main() {
|
|
|
288
321
|
}
|
|
289
322
|
const hookPython = createOrUpdateLangfuseVenv({ baseDir: claudeDir, pipIndexUrl });
|
|
290
323
|
const hookLauncher = await createHookLauncher({ hooksDir, hookPython, pyPath });
|
|
324
|
+
const agentLauncher = createAgentLauncher({ baseDir: claudeDir, target: "claude", executable: "claude" });
|
|
291
325
|
|
|
292
326
|
// 4) 合并写入 settings.json
|
|
293
327
|
const settingsPath = path.join(claudeDir, "settings.json");
|
|
@@ -322,10 +356,16 @@ async function main() {
|
|
|
322
356
|
|
|
323
357
|
const merged = deepMerge(existing, desired);
|
|
324
358
|
ensureDir(claudeDir);
|
|
325
|
-
writeJsonPretty(settingsPath, merged);
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
359
|
+
writeJsonPretty(settingsPath, merged);
|
|
360
|
+
const runtimeState = writeRuntimeInstallRecord("claude", {
|
|
361
|
+
packageName: packageJson.name,
|
|
362
|
+
packageVersion: packageJson.version,
|
|
363
|
+
});
|
|
364
|
+
console.log(`Runtime version recorded: ${runtimeState}`);
|
|
365
|
+
console.log(`已更新:${settingsPath}`);
|
|
366
|
+
console.log(`Agent launcher with update check: ${agentLauncher}`);
|
|
367
|
+
|
|
368
|
+
}
|
|
329
369
|
|
|
330
370
|
main().catch((err) => {
|
|
331
371
|
console.error(err?.message || String(err));
|
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
1
|
+
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import { spawn, spawnSync } from "node:child_process";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
5
6
|
import { parseJsonRelaxed, stripBom } from "./json-utils.mjs";
|
|
6
7
|
import { shouldSuppressAgentLogLine } from "./log-filter-utils.mjs";
|
|
8
|
+
import { writeRuntimeInstallRecord } from "./runtime-state-utils.mjs";
|
|
7
9
|
|
|
8
10
|
const USER_ID_PATTERN = /^[a-z](?:\d{8}|wx\d{7})$/;
|
|
9
11
|
const USER_ID_PATTERN_TEXT = "^[a-z](?:\\d{8}|wx\\d{7})$";
|
|
12
|
+
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
13
|
+
const packageJson = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8"));
|
|
10
14
|
|
|
11
15
|
function normalizeUserId(v) {
|
|
12
16
|
return String(v || "").trim();
|
|
@@ -296,6 +300,7 @@ function getPatchedLangfuseDistIndexJs() {
|
|
|
296
300
|
" }",
|
|
297
301
|
" return out;",
|
|
298
302
|
"};",
|
|
303
|
+
"const MAX_SKILL_USE_SPANS_PER_INTERACTION = 20;",
|
|
299
304
|
"",
|
|
300
305
|
"const escapeRegExp = (value) => String(value).replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');",
|
|
301
306
|
"",
|
|
@@ -456,6 +461,23 @@ function getPatchedLangfuseDistIndexJs() {
|
|
|
456
461
|
" }",
|
|
457
462
|
" set.add(activity.toolCallId || `${kind}:${set.size + 1}`);",
|
|
458
463
|
" };",
|
|
464
|
+
" const emitSkillUseSpans = ({ skillNames, sessionId, messageId, interactionId }) => {",
|
|
465
|
+
" for (const skillName of skillNames.slice(0, MAX_SKILL_USE_SPANS_PER_INTERACTION)) {",
|
|
466
|
+
" const span = metricsTracer.startSpan(`Skill Use / ${skillName}`);",
|
|
467
|
+
' span.setAttribute("oh.langfuse.source", "opencode");',
|
|
468
|
+
' span.setAttribute("oh.langfuse.user_id", userId || "");',
|
|
469
|
+
' span.setAttribute("oh.langfuse.metrics_schema_version", "1.0");',
|
|
470
|
+
' span.setAttribute("langfuse.observation.metadata.source", "opencode");',
|
|
471
|
+
' span.setAttribute("langfuse.observation.metadata.user_id", userId || "");',
|
|
472
|
+
' span.setAttribute("langfuse.observation.metadata.session_id", sessionId || "unknown");',
|
|
473
|
+
' span.setAttribute("langfuse.observation.metadata.message_id", messageId || "unknown");',
|
|
474
|
+
' span.setAttribute("langfuse.observation.metadata.interaction_id", interactionId);',
|
|
475
|
+
' span.setAttribute("langfuse.observation.metadata.metrics_schema_version", "1.0");',
|
|
476
|
+
' span.setAttribute("langfuse.observation.metadata.skill_name", skillName);',
|
|
477
|
+
' span.setAttribute("langfuse.observation.metadata.skill_use_count", 1);',
|
|
478
|
+
" span.end();",
|
|
479
|
+
" }",
|
|
480
|
+
" };",
|
|
459
481
|
"",
|
|
460
482
|
" const recordInteractionMetric = (event) => {",
|
|
461
483
|
" const payload = eventPayload(event);",
|
|
@@ -492,7 +514,8 @@ function getPatchedLangfuseDistIndexJs() {
|
|
|
492
514
|
' span.setAttribute("langfuse.observation.metadata.source", "opencode");',
|
|
493
515
|
' span.setAttribute("langfuse.observation.metadata.user_id", userId || "");',
|
|
494
516
|
' span.setAttribute("langfuse.observation.metadata.session_id", sessionId || "unknown");',
|
|
495
|
-
'
|
|
517
|
+
' const interactionId = `opencode:${userId || "unknown"}:${sessionId || "unknown"}:${messageId}`;',
|
|
518
|
+
' span.setAttribute("langfuse.observation.metadata.interaction_id", interactionId);',
|
|
496
519
|
' span.setAttribute("langfuse.observation.metadata.metrics_schema_version", "1.0");',
|
|
497
520
|
' span.setAttribute("langfuse.observation.metadata.interaction_count", 1);',
|
|
498
521
|
' span.setAttribute("langfuse.observation.metadata.user_message_count", 1);',
|
|
@@ -511,6 +534,7 @@ function getPatchedLangfuseDistIndexJs() {
|
|
|
511
534
|
' if (tokenMetrics.reasoning !== undefined) span.setAttribute("langfuse.observation.metadata.reasoning_tokens", tokenMetrics.reasoning);',
|
|
512
535
|
' if (text) span.setAttribute("langfuse.observation.metadata.output_text_preview", text.slice(0, 512));',
|
|
513
536
|
" span.end();",
|
|
537
|
+
" emitSkillUseSpans({ skillNames, sessionId, messageId, interactionId });",
|
|
514
538
|
" messageTextById.delete(messageId);",
|
|
515
539
|
" skillNamesByMessageId.delete(messageId);",
|
|
516
540
|
" skillNamesBySessionId.delete(sessionId);",
|
|
@@ -560,11 +584,15 @@ function writeWindowsLauncherCmd(opencodeDir, { publicKey, secretKey, baseUrl, u
|
|
|
560
584
|
if (process.platform !== "win32") return null;
|
|
561
585
|
const p = path.join(opencodeDir, "launch-opencode-langfuse.cmd");
|
|
562
586
|
const cmd = ["@echo off", "REM Auto-generated by scripts/opencode-langfuse-setup.mjs"];
|
|
563
|
-
cmd.push(`set LANGFUSE_PUBLIC_KEY=${publicKey}`);
|
|
564
|
-
cmd.push(`set LANGFUSE_SECRET_KEY=${secretKey}`);
|
|
565
|
-
cmd.push(`set LANGFUSE_BASEURL=${baseUrl}`);
|
|
566
|
-
if (userId) cmd.push(`set LANGFUSE_USER_ID=${userId}`);
|
|
567
|
-
cmd.push(
|
|
587
|
+
cmd.push(`set LANGFUSE_PUBLIC_KEY=${publicKey}`);
|
|
588
|
+
cmd.push(`set LANGFUSE_SECRET_KEY=${secretKey}`);
|
|
589
|
+
cmd.push(`set LANGFUSE_BASEURL=${baseUrl}`);
|
|
590
|
+
if (userId) cmd.push(`set LANGFUSE_USER_ID=${userId}`);
|
|
591
|
+
cmd.push("where npx >nul 2>nul");
|
|
592
|
+
cmd.push("if %ERRORLEVEL% EQU 0 (");
|
|
593
|
+
cmd.push(" call npx -y oh-langfuse@latest auto-update opencode --skip-check");
|
|
594
|
+
cmd.push(")");
|
|
595
|
+
cmd.push('if exist "%USERPROFILE%\\.opencode\\bin\\opencode.exe" (');
|
|
568
596
|
cmd.push(' "%USERPROFILE%\\.opencode\\bin\\opencode.exe" %*');
|
|
569
597
|
cmd.push(" exit /b %ERRORLEVEL%");
|
|
570
598
|
cmd.push(")");
|
|
@@ -588,6 +616,9 @@ function writeUnixLauncherSh(opencodeDir, { publicKey, secretKey, baseUrl, userI
|
|
|
588
616
|
`export LANGFUSE_SECRET_KEY=${shQuote(secretKey)}`,
|
|
589
617
|
`export LANGFUSE_BASEURL=${shQuote(baseUrl)}`,
|
|
590
618
|
userId ? `export LANGFUSE_USER_ID=${shQuote(userId)}` : null,
|
|
619
|
+
'if command -v npx >/dev/null 2>&1; then',
|
|
620
|
+
' npx -y oh-langfuse@latest auto-update opencode --skip-check || true',
|
|
621
|
+
"fi",
|
|
591
622
|
'if [ -x "$HOME/.opencode/bin/opencode" ]; then',
|
|
592
623
|
' exec "$HOME/.opencode/bin/opencode" "$@"',
|
|
593
624
|
"fi",
|
|
@@ -906,6 +937,11 @@ async function main() {
|
|
|
906
937
|
}
|
|
907
938
|
|
|
908
939
|
console.log(paint("完成。", colors.green, colors.bold));
|
|
940
|
+
const runtimeState = writeRuntimeInstallRecord("opencode", {
|
|
941
|
+
packageName: packageJson.name,
|
|
942
|
+
packageVersion: packageJson.version,
|
|
943
|
+
});
|
|
944
|
+
console.log(`Runtime version recorded: ${runtimeState}`);
|
|
909
945
|
printCommandHint("可运行以下命令校验:", "npx oh-langfuse@latest check opencode");
|
|
910
946
|
if (process.platform !== "win32") {
|
|
911
947
|
console.log("");
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
const STATE_VERSION = 1;
|
|
6
|
+
|
|
7
|
+
export function runtimeStateDir(home = os.homedir()) {
|
|
8
|
+
return path.join(home, ".config", "oh-langfuse");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function runtimeStatePath(home = os.homedir()) {
|
|
12
|
+
return path.join(runtimeStateDir(home), "runtime.json");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function stripBom(s) {
|
|
16
|
+
return typeof s === "string" && s.charCodeAt(0) === 0xfeff ? s.slice(1) : s;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function readRuntimeState(home = os.homedir()) {
|
|
20
|
+
const p = runtimeStatePath(home);
|
|
21
|
+
try {
|
|
22
|
+
if (!fs.existsSync(p)) return { version: STATE_VERSION, targets: {} };
|
|
23
|
+
const text = stripBom(fs.readFileSync(p, "utf8"));
|
|
24
|
+
if (!text.trim()) return { version: STATE_VERSION, targets: {} };
|
|
25
|
+
const parsed = JSON.parse(text);
|
|
26
|
+
return {
|
|
27
|
+
version: parsed.version || STATE_VERSION,
|
|
28
|
+
targets: parsed.targets && typeof parsed.targets === "object" ? parsed.targets : {},
|
|
29
|
+
};
|
|
30
|
+
} catch {
|
|
31
|
+
return { version: STATE_VERSION, targets: {} };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function getRuntimeInstallRecord(target, home = os.homedir()) {
|
|
36
|
+
return readRuntimeState(home).targets?.[target] || null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function writeRuntimeInstallRecord(target, record = {}, home = os.homedir()) {
|
|
40
|
+
const state = readRuntimeState(home);
|
|
41
|
+
const dir = runtimeStateDir(home);
|
|
42
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
43
|
+
state.version = STATE_VERSION;
|
|
44
|
+
state.targets = state.targets || {};
|
|
45
|
+
state.targets[target] = {
|
|
46
|
+
...state.targets[target],
|
|
47
|
+
...record,
|
|
48
|
+
target,
|
|
49
|
+
updatedAt: new Date().toISOString(),
|
|
50
|
+
};
|
|
51
|
+
fs.writeFileSync(runtimeStatePath(home), JSON.stringify(state, null, 2) + os.EOL, "utf8");
|
|
52
|
+
return runtimeStatePath(home);
|
|
53
|
+
}
|
|
@@ -4,6 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import { spawnSync } from "node:child_process";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { extractVersionFromNpmMetadata, selectUpdateTargets } from "./update-utils.mjs";
|
|
7
|
+
import { writeRuntimeInstallRecord } from "./runtime-state-utils.mjs";
|
|
7
8
|
|
|
8
9
|
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
9
10
|
const packageJson = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8"));
|
|
@@ -166,6 +167,10 @@ async function main() {
|
|
|
166
167
|
console.log(`[INFO] Updating ${target} runtime...`);
|
|
167
168
|
const config = mergedConfig(target, args);
|
|
168
169
|
runNodeScript(setupScript(target), setupArgs(target, config, args));
|
|
170
|
+
writeRuntimeInstallRecord(target, {
|
|
171
|
+
packageName: packageJson.name,
|
|
172
|
+
packageVersion: packageJson.version,
|
|
173
|
+
});
|
|
169
174
|
if (!args["skip-check"]) {
|
|
170
175
|
runNodeScript(checkScript(target), []);
|
|
171
176
|
}
|
package/scripts/update-utils.mjs
CHANGED
|
@@ -8,6 +8,33 @@ export function extractVersionFromNpmMetadata(metadata) {
|
|
|
8
8
|
return latest.trim();
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
function versionParts(version) {
|
|
12
|
+
return String(version || "")
|
|
13
|
+
.trim()
|
|
14
|
+
.replace(/^v/i, "")
|
|
15
|
+
.split("-")[0]
|
|
16
|
+
.split(".")
|
|
17
|
+
.map((part) => Number.parseInt(part, 10) || 0);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function compareSemver(a, b) {
|
|
21
|
+
const left = versionParts(a);
|
|
22
|
+
const right = versionParts(b);
|
|
23
|
+
const length = Math.max(left.length, right.length, 3);
|
|
24
|
+
for (let i = 0; i < length; i += 1) {
|
|
25
|
+
const l = left[i] || 0;
|
|
26
|
+
const r = right[i] || 0;
|
|
27
|
+
if (l > r) return 1;
|
|
28
|
+
if (l < r) return -1;
|
|
29
|
+
}
|
|
30
|
+
return 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function isNewerVersion(candidate, current) {
|
|
34
|
+
if (!candidate || !current) return false;
|
|
35
|
+
return compareSemver(candidate, current) > 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
11
38
|
export function selectUpdateTargets(target = "all", installed = {}) {
|
|
12
39
|
const normalized = String(target || "all").trim().toLowerCase();
|
|
13
40
|
if (normalized === "all") {
|