oh-langfuse 0.1.30 → 0.1.32

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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  `oh-langfuse` 是用于给 Claude Code、OpenCode 和 Codex 配置 Langfuse 追踪的命令行工具。它提供交互式安装向导,也支持 `setup` / `check` 直接命令,方便在用户机器上安装、修复和校验配置。
4
4
 
5
- 当前 npm 版本:`0.1.25`
5
+ 当前 npm 版本:`0.1.32`
6
6
 
7
7
  ## 能做什么
8
8
 
@@ -34,6 +34,23 @@ 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
+
37
54
  本地开发运行:
38
55
 
39
56
  ```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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-langfuse",
3
- "version": "0.1.30",
3
+ "version": "0.1.32",
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",
@@ -23,7 +24,9 @@
23
24
  "scripts/opencode-langfuse-setup.mjs",
24
25
  "scripts/resolve-opencode-cli.mjs",
25
26
  "scripts/real-self-verify.mjs",
27
+ "scripts/log-filter-utils.mjs",
26
28
  "scripts/metrics-utils.mjs",
29
+ "scripts/runtime-state-utils.mjs",
27
30
  "scripts/update-langfuse-runtime.mjs",
28
31
  "scripts/update-utils.mjs",
29
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
- console.log(`Updated Codex notify hook: ${configPath}`);
194
- console.log(`Installed hook script: ${destHook}`);
195
- console.log(`Wrote Langfuse config: ${path.join(langfuseDir, "config.json")}`);
196
-
197
- console.log("Done. Restart Codex so the updated notify command is loaded.");
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
- console.log(`已更新:${settingsPath}`);
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));
@@ -0,0 +1,18 @@
1
+ function envShowsWarnings(env = process.env) {
2
+ return /^(1|true|yes|on)$/i.test(String(env.OH_LANGFUSE_SHOW_WARN || env.LANGFUSE_SHOW_WARN || ""));
3
+ }
4
+
5
+ export function shouldSuppressAgentLogLine(line, options = {}) {
6
+ const showWarnings = options.showWarnings ?? envShowsWarnings(options.env);
7
+ if (showWarnings) return false;
8
+ const text = String(line || "").trim();
9
+ return /(?:^|\s)(npm\s+warn|warn(?:ing)?\b|\[warn\])/i.test(text);
10
+ }
11
+
12
+ export function filterAgentLogText(text, options = {}) {
13
+ const source = String(text || "");
14
+ return source
15
+ .split(/\r?\n/)
16
+ .filter((line) => line !== "" && !shouldSuppressAgentLogLine(line, options))
17
+ .join("\n");
18
+ }
@@ -1,11 +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";
7
+ import { shouldSuppressAgentLogLine } from "./log-filter-utils.mjs";
8
+ import { writeRuntimeInstallRecord } from "./runtime-state-utils.mjs";
6
9
 
7
10
  const USER_ID_PATTERN = /^[a-z](?:\d{8}|wx\d{7})$/;
8
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"));
9
14
 
10
15
  function normalizeUserId(v) {
11
16
  return String(v || "").trim();
@@ -114,7 +119,8 @@ function getPatchedLangfuseDistIndexJs() {
114
119
  // This replaces opencode-plugin-langfuse/dist/index.js to inject userId into spans.
115
120
  // It is intentionally self-contained (no extra build steps).
116
121
  return [
117
- 'import { LangfuseSpanProcessor } from "@langfuse/otel";',
122
+ 'import { LangfuseSpanProcessor } from "@langfuse/otel";',
123
+ 'import { configureGlobalLogger, LogLevel } from "@langfuse/core";',
118
124
  'import { promises as fs } from "node:fs";',
119
125
  'import os from "node:os";',
120
126
  'import path from "node:path";',
@@ -128,7 +134,7 @@ function getPatchedLangfuseDistIndexJs() {
128
134
  ' "config.json"',
129
135
  ");",
130
136
  "",
131
- "const readConfiguredUserId = async () => {",
137
+ "const readConfiguredUserId = async () => {",
132
138
  " try {",
133
139
  ' const content = await fs.readFile(USER_CONFIG_PATH, "utf8");',
134
140
  " // tolerate UTF-8 BOM",
@@ -144,8 +150,14 @@ function getPatchedLangfuseDistIndexJs() {
144
150
  " } catch {",
145
151
  " return undefined;",
146
152
  " }",
147
- "};",
148
- "",
153
+ "};",
154
+ "",
155
+ "const showWarnings = /^(1|true|yes|on)$/i.test(String(process.env.OH_LANGFUSE_SHOW_WARN || process.env.LANGFUSE_SHOW_WARN || ''));",
156
+ "if (!showWarnings) {",
157
+ " process.env.LANGFUSE_LOG_LEVEL = 'ERROR';",
158
+ " configureGlobalLogger({ level: LogLevel.ERROR });",
159
+ "}",
160
+ "",
149
161
  "const createUserIdSpanProcessor = (userId) => ({",
150
162
  " onStart: (span) => {",
151
163
  ' span.setAttribute("langfuse.user.id", userId);',
@@ -360,11 +372,12 @@ function getPatchedLangfuseDistIndexJs() {
360
372
  "",
361
373
  " const userIdFromConfig = await readConfiguredUserId();",
362
374
  " const userIdEnv = process.env.LANGFUSE_USER_ID?.trim();",
363
- " const userId = userIdFromConfig || userIdEnv;",
364
- "",
365
- " const log = (level, message) => {",
366
- ' client.app.log({ body: { service: "langfuse-otel", level, message } });',
367
- " };",
375
+ " const userId = userIdFromConfig || userIdEnv;",
376
+ "",
377
+ " const log = (level, message) => {",
378
+ " if (level === 'warn' && !showWarnings) return;",
379
+ ' client.app.log({ body: { service: "langfuse-otel", level, message } });',
380
+ " };",
368
381
  "",
369
382
  " if (!publicKey || !secretKey) {",
370
383
  ' log("warn", "Missing LANGFUSE_PUBLIC_KEY or LANGFUSE_SECRET_KEY - tracing disabled");',
@@ -551,11 +564,15 @@ function writeWindowsLauncherCmd(opencodeDir, { publicKey, secretKey, baseUrl, u
551
564
  if (process.platform !== "win32") return null;
552
565
  const p = path.join(opencodeDir, "launch-opencode-langfuse.cmd");
553
566
  const cmd = ["@echo off", "REM Auto-generated by scripts/opencode-langfuse-setup.mjs"];
554
- cmd.push(`set LANGFUSE_PUBLIC_KEY=${publicKey}`);
555
- cmd.push(`set LANGFUSE_SECRET_KEY=${secretKey}`);
556
- cmd.push(`set LANGFUSE_BASEURL=${baseUrl}`);
557
- if (userId) cmd.push(`set LANGFUSE_USER_ID=${userId}`);
558
- cmd.push('if exist "%USERPROFILE%\\.opencode\\bin\\opencode.exe" (');
567
+ cmd.push(`set LANGFUSE_PUBLIC_KEY=${publicKey}`);
568
+ cmd.push(`set LANGFUSE_SECRET_KEY=${secretKey}`);
569
+ cmd.push(`set LANGFUSE_BASEURL=${baseUrl}`);
570
+ if (userId) cmd.push(`set LANGFUSE_USER_ID=${userId}`);
571
+ cmd.push("where npx >nul 2>nul");
572
+ cmd.push("if %ERRORLEVEL% EQU 0 (");
573
+ cmd.push(" call npx -y oh-langfuse@latest auto-update opencode --skip-check");
574
+ cmd.push(")");
575
+ cmd.push('if exist "%USERPROFILE%\\.opencode\\bin\\opencode.exe" (');
559
576
  cmd.push(' "%USERPROFILE%\\.opencode\\bin\\opencode.exe" %*');
560
577
  cmd.push(" exit /b %ERRORLEVEL%");
561
578
  cmd.push(")");
@@ -579,6 +596,9 @@ function writeUnixLauncherSh(opencodeDir, { publicKey, secretKey, baseUrl, userI
579
596
  `export LANGFUSE_SECRET_KEY=${shQuote(secretKey)}`,
580
597
  `export LANGFUSE_BASEURL=${shQuote(baseUrl)}`,
581
598
  userId ? `export LANGFUSE_USER_ID=${shQuote(userId)}` : null,
599
+ 'if command -v npx >/dev/null 2>&1; then',
600
+ ' npx -y oh-langfuse@latest auto-update opencode --skip-check || true',
601
+ "fi",
582
602
  'if [ -x "$HOME/.opencode/bin/opencode" ]; then',
583
603
  ' exec "$HOME/.opencode/bin/opencode" "$@"',
584
604
  "fi",
@@ -676,7 +696,11 @@ function runNpmInstallCapture(npmArgs) {
676
696
  child.stderr?.on("data", (chunk) => {
677
697
  const text = String(chunk);
678
698
  stderr += text;
679
- process.stderr.write(text);
699
+ const filtered = text
700
+ .split(/(\r?\n)/)
701
+ .filter((part) => part === "\n" || part === "\r\n" || !shouldSuppressAgentLogLine(part))
702
+ .join("");
703
+ if (filtered) process.stderr.write(filtered);
680
704
  });
681
705
  child.on("error", (error) => {
682
706
  clearInterval(timer);
@@ -893,6 +917,11 @@ async function main() {
893
917
  }
894
918
 
895
919
  console.log(paint("完成。", colors.green, colors.bold));
920
+ const runtimeState = writeRuntimeInstallRecord("opencode", {
921
+ packageName: packageJson.name,
922
+ packageVersion: packageJson.version,
923
+ });
924
+ console.log(`Runtime version recorded: ${runtimeState}`);
896
925
  printCommandHint("可运行以下命令校验:", "npx oh-langfuse@latest check opencode");
897
926
  if (process.platform !== "win32") {
898
927
  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
  }
@@ -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") {