oh-langfuse 0.1.79 → 0.1.80

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.
@@ -8,24 +8,24 @@ import { shouldSuppressAgentLogLine } from "./log-filter-utils.mjs";
8
8
  import { writeRuntimeInstallRecord } from "./runtime-state-utils.mjs";
9
9
  import { resolveOpencodeCli } from "./resolve-opencode-cli.mjs";
10
10
 
11
- const USER_ID_PATTERN = /^[a-z](?:\d{8}|wx\d{7})$/;
12
- const USER_ID_PATTERN_TEXT = "^[a-z](?:\\d{8}|wx\\d{7})$";
13
- const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
14
- const packageJson = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8"));
15
- const DEFAULT_LANGFUSE_BASE_URL = "https://metrics.openharmonyinsight.cn";
16
- const LEGACY_LANGFUSE_BASE_URLS = new Set([
17
- "http://120.46.221.227:3000",
18
- "https://120.46.221.227:3000",
19
- ]);
20
-
21
- function normalizeLegacyBaseUrl(baseUrl) {
22
- const value = String(baseUrl || "").trim().replace(/\/+$/, "");
23
- return LEGACY_LANGFUSE_BASE_URLS.has(value) ? DEFAULT_LANGFUSE_BASE_URL : baseUrl;
24
- }
25
-
26
- function normalizeUserId(v) {
27
- return String(v || "").trim();
28
- }
11
+ const USER_ID_PATTERN = /^[a-z](?:\d{8}|wx\d{7})$/;
12
+ const USER_ID_PATTERN_TEXT = "^[a-z](?:\\d{8}|wx\\d{7})$";
13
+ const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
14
+ const packageJson = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8"));
15
+ const DEFAULT_LANGFUSE_BASE_URL = "https://metrics.openharmonyinsight.cn";
16
+ const LEGACY_LANGFUSE_BASE_URLS = new Set([
17
+ "http://120.46.221.227:3000",
18
+ "https://120.46.221.227:3000",
19
+ ]);
20
+
21
+ function normalizeLegacyBaseUrl(baseUrl) {
22
+ const value = String(baseUrl || "").trim().replace(/\/+$/, "");
23
+ return LEGACY_LANGFUSE_BASE_URLS.has(value) ? DEFAULT_LANGFUSE_BASE_URL : baseUrl;
24
+ }
25
+
26
+ function normalizeUserId(v) {
27
+ return String(v || "").trim();
28
+ }
29
29
 
30
30
  function assertValidUserId(userId) {
31
31
  if (!USER_ID_PATTERN.test(normalizeUserId(userId))) {
@@ -144,6 +144,7 @@ export function getPatchedLangfuseDistIndexJs() {
144
144
  'import os from "node:os";',
145
145
  'import path from "node:path";',
146
146
  'import { execFileSync } from "node:child_process";',
147
+ 'import { pathToFileURL } from "node:url";',
147
148
  'import { NodeSDK } from "@opentelemetry/sdk-node";',
148
149
  'import { trace } from "@opentelemetry/api";',
149
150
  "",
@@ -687,7 +688,7 @@ export function getPatchedLangfuseDistIndexJs() {
687
688
  " }).filter((event) => event.skill_name);",
688
689
  "};",
689
690
  "",
690
- "const collectKnownSkillNames = async () => {",
691
+ "const collectKnownSkillNames = async (extraPaths) => {",
691
692
  " const dirs = [",
692
693
  " path.join(process.cwd(), '.opencode', 'skill'),",
693
694
  " path.join(process.cwd(), '.opencode', 'skills'),",
@@ -696,6 +697,7 @@ export function getPatchedLangfuseDistIndexJs() {
696
697
  " path.join(os.homedir(), '.opencode', 'skill'),",
697
698
  " path.join(os.homedir(), '.opencode', 'skills'),",
698
699
  " ];",
700
+ " if (Array.isArray(extraPaths)) for (const p of extraPaths) if (typeof p === 'string' && p) dirs.push(p);",
699
701
  " const names = new Set();",
700
702
  " for (const dir of dirs) {",
701
703
  " try {",
@@ -719,7 +721,7 @@ export function getPatchedLangfuseDistIndexJs() {
719
721
  "export const LangfusePlugin = async ({ client }) => {",
720
722
  " const publicKey = process.env.LANGFUSE_PUBLIC_KEY;",
721
723
  " const secretKey = process.env.LANGFUSE_SECRET_KEY;",
722
- ' const baseUrl = process.env.LANGFUSE_BASEURL ?? "https://metrics.openharmonyinsight.cn";',
724
+ ' const baseUrl = process.env.LANGFUSE_BASEURL ?? "https://metrics.openharmonyinsight.cn";',
723
725
  ' const environment = process.env.LANGFUSE_ENVIRONMENT ?? "development";',
724
726
  "",
725
727
  " const userIdFromConfig = await readConfiguredUserId();",
@@ -745,7 +747,7 @@ export function getPatchedLangfuseDistIndexJs() {
745
747
  ' log("warn", `OTEL SDK start failed: ${err?.message ?? err}`);',
746
748
  " });",
747
749
  " const getMetricsTracer = () => trace.getTracer('oh-langfuse-opencode-metrics');",
748
- " const knownSkillNames = await collectKnownSkillNames();",
750
+ " let knownSkillNames = await collectKnownSkillNames();",
749
751
  " const startupSkillUsages = detectOpencodeSkillUsages(process.argv.join('\\n'), knownSkillNames);",
750
752
  " const startupPromptText = limitObservationText(promptFromArgv());",
751
753
  " const messageTextById = new Map();",
@@ -923,6 +925,26 @@ export function getPatchedLangfuseDistIndexJs() {
923
925
  " if (!config.experimental?.openTelemetry) {",
924
926
  ' log("warn", "OpenTelemetry experimental feature is disabled in Opencode config - tracing disabled");',
925
927
  " }",
928
+ " try {",
929
+ " const discoverPath = path.join(os.homedir(), '.config', 'oh-langfuse', 'opencode-skills-discover.mjs');",
930
+ " let roots = [];",
931
+ " try {",
932
+ " const mod = await import(pathToFileURL(discoverPath).href);",
933
+ " if (mod && typeof mod.collectSkillRoots === 'function') roots = mod.collectSkillRoots(process.cwd());",
934
+ " } catch (e) { /* discover script absent or import failed: skip augmentation */ }",
935
+ " if (roots.length) {",
936
+ " config.skills = config.skills || {};",
937
+ " config.skills.paths = Array.isArray(config.skills.paths) ? [...config.skills.paths] : [];",
938
+ " for (const r of roots) if (!config.skills.paths.includes(r)) config.skills.paths.push(r);",
939
+ " config.permission = config.permission || {};",
940
+ " config.permission.skill = config.permission.skill || {};",
941
+ " config.permission.skill['*'] = 'allow';",
942
+ " }",
943
+ " knownSkillNames = await collectKnownSkillNames(config.skills && config.skills.paths);",
944
+ " if (knownSkillNames.length) log('info', `OpenCode skills discovered -> ${knownSkillNames.length}`);",
945
+ " } catch (e) {",
946
+ " log('warn', `skills config hook failed: ${e && e.message || e}`);",
947
+ " }",
926
948
  " },",
927
949
  " event: async ({ event }) => {",
928
950
  " const eventType = pickEventString(event?.name, event?.type, eventPayload(event)?.name, eventPayload(event)?.type);",
@@ -1034,6 +1056,29 @@ function unixAutoUpdateCommand(target) {
1034
1056
  return `${shQuote(writeAutoUpdateHelper(target))} || true`;
1035
1057
  }
1036
1058
 
1059
+ function skillsDiscoverScriptPath() {
1060
+ return path.join(rootDir, "scripts", "opencode-skills-discover.mjs");
1061
+ }
1062
+
1063
+ function skillsDiscoverInstallPath() {
1064
+ return path.join(os.homedir(), ".config", "oh-langfuse", "opencode-skills-discover.mjs");
1065
+ }
1066
+
1067
+ function installSkillsDiscoverHelper() {
1068
+ const dest = skillsDiscoverInstallPath();
1069
+ ensureDir(path.dirname(dest));
1070
+ fs.copyFileSync(skillsDiscoverScriptPath(), dest);
1071
+ return dest;
1072
+ }
1073
+
1074
+ // installSkillsDiscoverHelper() (above) copies the skills-discover script to
1075
+ // ~/.config/oh-langfuse/opencode-skills-discover.mjs. The opencode plugin's
1076
+ // config hook imports that script (see getPatchedLangfuseDistIndexJs) and calls
1077
+ // its collectSkillRoots(cwd) to register skill repos near cwd into config.skills.paths,
1078
+ // PATH-independently. The earlier shim/OPENCODE_CONFIG_CONTENT approach was removed:
1079
+ // it was shadowed by system-level opencode wrappers (e.g. DevEco Studio) and is
1080
+ // redundant with the plugin config hook, which runs inside opencode regardless of PATH.
1081
+
1037
1082
  function windowsGitPathBootstrap() {
1038
1083
  return [
1039
1084
  "for /f \"delims=\" %%G in ('where.exe git.exe 2^>nul') do (",
@@ -1046,7 +1091,7 @@ function windowsGitPathBootstrap() {
1046
1091
  ];
1047
1092
  }
1048
1093
 
1049
- function writeWindowsUpdateCheckScript(dir) {
1094
+ function writeWindowsUpdateCheckScript(dir) {
1050
1095
  if (process.platform !== "win32") return null;
1051
1096
  ensureDir(dir);
1052
1097
  const p = path.join(dir, "oh-langfuse-opencode-update-check.ps1");
@@ -1065,25 +1110,25 @@ function writeWindowsUpdateCheckScript(dir) {
1065
1110
  ""
1066
1111
  ].join("\r\n");
1067
1112
  fs.writeFileSync(p, "\ufeff" + ps, "utf8");
1068
- return p;
1069
- }
1070
-
1071
- function readRealOpencodeFromManagedShim(shimPath) {
1072
- try {
1073
- if (!shimPath || !fs.existsSync(shimPath)) return "";
1074
- const text = fs.readFileSync(shimPath, "utf8");
1075
- const cmdMatch = text.match(/set\s+"OH_LANGFUSE_REAL_OPENCODE=([^"]+)"/i);
1076
- if (cmdMatch?.[1] && fs.existsSync(cmdMatch[1])) return path.resolve(cmdMatch[1]);
1077
- const shMatch = text.match(/^OH_LANGFUSE_REAL_OPENCODE=(?:"([^"]+)"|'([^']+)'|([^\r\n]+))/m);
1078
- const candidate = shMatch?.[1] || shMatch?.[2] || shMatch?.[3];
1079
- if (candidate && fs.existsSync(candidate.trim())) return path.resolve(candidate.trim());
1080
- } catch {
1081
- // Best effort only; setup can still fall back to ordinary CLI discovery.
1082
- }
1083
- return "";
1084
- }
1085
-
1086
- function writeOpencodeCommandShim(opencodeDir, { publicKey, secretKey, baseUrl, userId, realOpencodeCli }) {
1113
+ return p;
1114
+ }
1115
+
1116
+ function readRealOpencodeFromManagedShim(shimPath) {
1117
+ try {
1118
+ if (!shimPath || !fs.existsSync(shimPath)) return "";
1119
+ const text = fs.readFileSync(shimPath, "utf8");
1120
+ const cmdMatch = text.match(/set\s+"OH_LANGFUSE_REAL_OPENCODE=([^"]+)"/i);
1121
+ if (cmdMatch?.[1] && fs.existsSync(cmdMatch[1])) return path.resolve(cmdMatch[1]);
1122
+ const shMatch = text.match(/^OH_LANGFUSE_REAL_OPENCODE=(?:"([^"]+)"|'([^']+)'|([^\r\n]+))/m);
1123
+ const candidate = shMatch?.[1] || shMatch?.[2] || shMatch?.[3];
1124
+ if (candidate && fs.existsSync(candidate.trim())) return path.resolve(candidate.trim());
1125
+ } catch {
1126
+ // Best effort only; setup can still fall back to ordinary CLI discovery.
1127
+ }
1128
+ return "";
1129
+ }
1130
+
1131
+ function writeOpencodeCommandShim(opencodeDir, { publicKey, secretKey, baseUrl, userId, realOpencodeCli }) {
1087
1132
  const shimDir = path.join(opencodeDir, "bin");
1088
1133
  ensureDir(shimDir);
1089
1134
  if (process.platform === "win32") {
@@ -1527,9 +1572,9 @@ async function main() {
1527
1572
  args.publicKey || process.env.LANGFUSE_PUBLIC_KEY || "pk-lf-da0c90a7-6e93-4eb7-bb86-c1047c8d187d";
1528
1573
  const secretKey =
1529
1574
  args.secretKey || process.env.LANGFUSE_SECRET_KEY || "sk-lf-0269b85d-bfdc-442c-bfa3-e737954e3315";
1530
- const baseUrl = normalizeLegacyBaseUrl(
1531
- args.langfuseBaseUrl || process.env.LANGFUSE_BASEURL || DEFAULT_LANGFUSE_BASE_URL
1532
- );
1575
+ const baseUrl = normalizeLegacyBaseUrl(
1576
+ args.langfuseBaseUrl || process.env.LANGFUSE_BASEURL || DEFAULT_LANGFUSE_BASE_URL
1577
+ );
1533
1578
  const userId = normalizeUserId(args.userId || args.userid || "");
1534
1579
  if (!userId || typeof userId !== "string") {
1535
1580
  throw new Error("缺少参数:--userId=你的工号");
@@ -1578,6 +1623,17 @@ async function main() {
1578
1623
  patchDistIndexJs(distIndexInPlugins);
1579
1624
  }
1580
1625
 
1626
+ // Install the skills-discover script the plugin's config hook imports at runtime.
1627
+ // Wrapped so a missing source file (e.g. not in the published package's files
1628
+ // allowlist) degrades gracefully instead of failing the whole setup.
1629
+ try {
1630
+ const installedDiscover = installSkillsDiscoverHelper();
1631
+ console.log(`Skills discover helper ready: ${installedDiscover}`);
1632
+ } catch (e) {
1633
+ console.log(`Skills discover helper skipped: ${e?.message || e}`);
1634
+ console.log("Skill-repo auto-discovery will not be available until setup is re-run from a complete package.");
1635
+ }
1636
+
1581
1637
  // Write plugin user config to support userId injection.
1582
1638
  if (userId) {
1583
1639
  const cfg = writeLangfusePluginUserConfig({ userId });
@@ -1600,12 +1656,12 @@ async function main() {
1600
1656
  writeJsonPretty(opencodeJsonPath, merged);
1601
1657
  let realOpencodeCli = resolveOpencodeCli(args.cmd);
1602
1658
  const shimDir = path.join(opencodeDir, "bin");
1603
- if (realOpencodeCli) {
1604
- const relativeToShim = path.relative(path.resolve(shimDir), path.resolve(realOpencodeCli));
1605
- if (relativeToShim && !relativeToShim.startsWith("..") && !path.isAbsolute(relativeToShim)) {
1606
- realOpencodeCli = readRealOpencodeFromManagedShim(realOpencodeCli);
1607
- }
1608
- }
1659
+ if (realOpencodeCli) {
1660
+ const relativeToShim = path.relative(path.resolve(shimDir), path.resolve(realOpencodeCli));
1661
+ if (relativeToShim && !relativeToShim.startsWith("..") && !path.isAbsolute(relativeToShim)) {
1662
+ realOpencodeCli = readRealOpencodeFromManagedShim(realOpencodeCli);
1663
+ }
1664
+ }
1609
1665
  console.log(`已更新:${opencodeJsonPath}`);
1610
1666
 
1611
1667
  const launcher = writeWindowsLauncherCmd(opencodeDir, { publicKey, secretKey, baseUrl, userId });
@@ -0,0 +1,125 @@
1
+ // Auto-discover OpenCode skill repositories located as a child or sibling of the
2
+ // current working directory, and print an opencode config fragment (JSON, single
3
+ // line) to stdout. The generated opencode shim captures this output into the
4
+ // OPENCODE_CONFIG_CONTENT env var so opencode loads it as an additional merged
5
+ // config, registering those skill folders and allowing all skills regardless of
6
+ // where opencode was launched from.
7
+ //
8
+ // This script is intentionally self-contained (only node built-ins) because it
9
+ // is copied to ~/.config/oh-langfuse/opencode-skills-discover.mjs during setup
10
+ // and invoked standalone via `node <path>` from the generated opencode shim.
11
+ //
12
+ // Discovery rules (matches the typical "launch from a sibling or parent of the
13
+ // skills repo" usage pattern):
14
+ // - cwd/skills, cwd/.opencode/skills, cwd/.opencode/skill (cwd IS a skills repo)
15
+ // - cwd/<child>/skills (cwd is a parent of a skills repo)
16
+ // - ../<sibling>/skills (cwd is a sibling of a skills repo)
17
+ // A `<x>/skills` folder counts when it contains at least one SKILL.md (directly
18
+ // or one level deep, matching the openharmony-skills layout). opencode itself
19
+ // then scans **/SKILL.md inside each registered path.
20
+
21
+ import fs from "node:fs";
22
+ import path from "node:path";
23
+ import os from "node:os";
24
+ import { pathToFileURL } from "node:url";
25
+
26
+ function dirExists(p) {
27
+ try {
28
+ return fs.statSync(p).isDirectory();
29
+ } catch {
30
+ return false;
31
+ }
32
+ }
33
+
34
+ function hasSkillFile(skillsDir) {
35
+ if (!dirExists(skillsDir)) return false;
36
+ let entries;
37
+ try {
38
+ entries = fs.readdirSync(skillsDir, { withFileTypes: true });
39
+ } catch {
40
+ return false;
41
+ }
42
+ for (const entry of entries) {
43
+ if (entry.isFile() && entry.name === "SKILL.md") return true;
44
+ if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
45
+ try {
46
+ fs.accessSync(path.join(skillsDir, entry.name, "SKILL.md"));
47
+ return true;
48
+ } catch {
49
+ // keep scanning siblings
50
+ }
51
+ }
52
+ return false;
53
+ }
54
+
55
+ function safeReaddir(p) {
56
+ try {
57
+ return fs.readdirSync(p, { withFileTypes: true });
58
+ } catch {
59
+ return [];
60
+ }
61
+ }
62
+
63
+ export function collectSkillRoots(cwd) {
64
+ const roots = new Set();
65
+
66
+ // 1. cwd itself hosts a skills folder (cwd is a skills repo root).
67
+ for (const sub of ["skills", path.join(".opencode", "skills"), path.join(".opencode", "skill")]) {
68
+ const cand = path.join(cwd, sub);
69
+ if (hasSkillFile(cand)) roots.add(path.resolve(cand));
70
+ }
71
+
72
+ // 2. cwd's children host a skills folder (cwd is a parent of skills repos).
73
+ for (const entry of safeReaddir(cwd)) {
74
+ if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
75
+ const cand = path.join(cwd, entry.name, "skills");
76
+ if (hasSkillFile(cand)) roots.add(path.resolve(cand));
77
+ }
78
+
79
+ // 3. cwd's siblings host a skills folder (cwd is a sibling of skills repos).
80
+ const parent = path.dirname(cwd);
81
+ if (parent && parent !== cwd) {
82
+ for (const entry of safeReaddir(parent)) {
83
+ if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
84
+ const cand = path.join(parent, entry.name, "skills");
85
+ if (hasSkillFile(cand)) roots.add(path.resolve(cand));
86
+ }
87
+ }
88
+
89
+ return [...roots];
90
+ }
91
+
92
+ export function buildConfig(cwd) {
93
+ const roots = collectSkillRoots(cwd);
94
+ return {
95
+ $schema: "https://opencode.ai/config.json",
96
+ skills: { paths: roots },
97
+ permission: { skill: { "*": "allow" } }
98
+ };
99
+ }
100
+
101
+ function main() {
102
+ const config = buildConfig(process.cwd());
103
+ // Single line so the calling shim can capture stdout into one env var.
104
+ process.stdout.write(JSON.stringify(config));
105
+ }
106
+
107
+ // Only run main() when invoked directly via `node <path>`, not when imported
108
+ // (the opencode plugin's config hook imports this module to call
109
+ // collectSkillRoots/buildConfig without side effects).
110
+ function isMain() {
111
+ try {
112
+ return import.meta.url === pathToFileURL(process.argv[1] || "").href;
113
+ } catch {
114
+ return false;
115
+ }
116
+ }
117
+
118
+ if (isMain()) {
119
+ try {
120
+ main();
121
+ } catch {
122
+ // Stay silent on failure: if nothing is captured, OPENCODE_CONFIG_CONTENT
123
+ // stays unset and opencode uses its default config loading.
124
+ }
125
+ }
@@ -9,12 +9,12 @@ import { resolveOpencodeCli } from "./resolve-opencode-cli.mjs";
9
9
  const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
10
10
  const packageJson = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8"));
11
11
 
12
- const DEFAULT_LANGFUSE_BASE_URL = "https://metrics.openharmonyinsight.cn";
13
- const LEGACY_LANGFUSE_BASE_URLS = new Set([
14
- "http://120.46.221.227:3000",
15
- "https://120.46.221.227:3000",
16
- ]);
17
- const DEFAULT_LANGFUSE_PUBLIC_KEY = "pk-lf-da0c90a7-6e93-4eb7-bb86-c1047c8d187d";
12
+ const DEFAULT_LANGFUSE_BASE_URL = "https://metrics.openharmonyinsight.cn";
13
+ const LEGACY_LANGFUSE_BASE_URLS = new Set([
14
+ "http://120.46.221.227:3000",
15
+ "https://120.46.221.227:3000",
16
+ ]);
17
+ const DEFAULT_LANGFUSE_PUBLIC_KEY = "pk-lf-da0c90a7-6e93-4eb7-bb86-c1047c8d187d";
18
18
  const DEFAULT_LANGFUSE_SECRET_KEY = "sk-lf-0269b85d-bfdc-442c-bfa3-e737954e3315";
19
19
  const OPENCODE_NATIVE_AGENT_TURN_NAMES = new Set(["session.llm:ai.streamText.doStream"]);
20
20
 
@@ -149,7 +149,7 @@ function findOnPath(names) {
149
149
  return "";
150
150
  }
151
151
 
152
- function localCodexCliCandidates() {
152
+ function localCodexCliCandidates() {
153
153
  if (process.platform !== "win32") return ["codex"];
154
154
  const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local");
155
155
  const codexBin = path.join(localAppData, "OpenAI", "Codex", "bin");
@@ -167,20 +167,20 @@ function localCodexCliCandidates() {
167
167
  // Fall back to PATH probing below.
168
168
  }
169
169
  return [...candidates, "codex.cmd", "codex.exe", "codex"].filter(Boolean);
170
- }
171
-
172
- function normalizeLegacyBaseUrl(baseUrl) {
173
- const value = String(baseUrl || "").trim().replace(/\/+$/, "");
174
- return LEGACY_LANGFUSE_BASE_URLS.has(value) ? DEFAULT_LANGFUSE_BASE_URL : baseUrl;
175
- }
176
-
177
- function configFromArgs(args) {
178
- return {
179
- baseUrl: String(
180
- normalizeLegacyBaseUrl(
181
- args.langfuseBaseUrl || args.langfuseHost || args.host || process.env.LANGFUSE_BASEURL || process.env.LANGFUSE_HOST || DEFAULT_LANGFUSE_BASE_URL
182
- )
183
- ),
170
+ }
171
+
172
+ function normalizeLegacyBaseUrl(baseUrl) {
173
+ const value = String(baseUrl || "").trim().replace(/\/+$/, "");
174
+ return LEGACY_LANGFUSE_BASE_URLS.has(value) ? DEFAULT_LANGFUSE_BASE_URL : baseUrl;
175
+ }
176
+
177
+ function configFromArgs(args) {
178
+ return {
179
+ baseUrl: String(
180
+ normalizeLegacyBaseUrl(
181
+ args.langfuseBaseUrl || args.langfuseHost || args.host || process.env.LANGFUSE_BASEURL || process.env.LANGFUSE_HOST || DEFAULT_LANGFUSE_BASE_URL
182
+ )
183
+ ),
184
184
  publicKey: String(args.publicKey || process.env.LANGFUSE_PUBLIC_KEY || DEFAULT_LANGFUSE_PUBLIC_KEY),
185
185
  secretKey: String(args.secretKey || process.env.LANGFUSE_SECRET_KEY || DEFAULT_LANGFUSE_SECRET_KEY),
186
186
  userId: String(args.userId || args.userid || process.env.LANGFUSE_USER_ID || process.env.CC_USER_ID || "").trim(),
@@ -270,7 +270,7 @@ function basicAuth(publicKey, secretKey) {
270
270
  return `Basic ${Buffer.from(`${publicKey}:${secretKey}`, "utf8").toString("base64")}`;
271
271
  }
272
272
 
273
- async function langfuseGet(config, pathname, params = {}) {
273
+ async function langfuseGet(config, pathname, params = {}) {
274
274
  const url = new URL(`${apiBase(config.baseUrl)}${pathname}`);
275
275
  for (const [key, value] of Object.entries(params)) {
276
276
  if (value !== undefined && value !== null && String(value) !== "") url.searchParams.set(key, String(value));
@@ -290,22 +290,22 @@ async function langfuseGet(config, pathname, params = {}) {
290
290
  error.status = response.status;
291
291
  throw error;
292
292
  }
293
- return await response.json();
294
- }
295
-
296
- function isLangfuseQueryShapeError(error) {
297
- if (!error) return false;
298
- return error.status === 400 || error.status === 422;
299
- }
300
-
301
- async function langfuseGetLenient(config, pathname, params = {}) {
302
- try {
303
- return await langfuseGet(config, pathname, params);
304
- } catch (error) {
305
- if (!isLangfuseQueryShapeError(error)) throw error;
306
- const fallback = {};
307
- for (const key of ["limit", "page", "userId", "fields"]) {
308
- if (params[key] !== undefined) fallback[key] = params[key];
293
+ return await response.json();
294
+ }
295
+
296
+ function isLangfuseQueryShapeError(error) {
297
+ if (!error) return false;
298
+ return error.status === 400 || error.status === 422;
299
+ }
300
+
301
+ async function langfuseGetLenient(config, pathname, params = {}) {
302
+ try {
303
+ return await langfuseGet(config, pathname, params);
304
+ } catch (error) {
305
+ if (!isLangfuseQueryShapeError(error)) throw error;
306
+ const fallback = {};
307
+ for (const key of ["limit", "page", "userId", "fields"]) {
308
+ if (params[key] !== undefined) fallback[key] = params[key];
309
309
  }
310
310
  return await langfuseGet(config, pathname, fallback);
311
311
  }
@@ -371,29 +371,29 @@ function metadataValue(item, key) {
371
371
  return undefined;
372
372
  }
373
373
 
374
- function directMetadataValue(item, key) {
375
- const metadata = item?.metadata || {};
376
- return metadata[key];
377
- }
378
-
379
- function observationIOValue(item, key) {
380
- const metadata = item?.metadata || {};
381
- const attrs = metadata.attributes || {};
382
- for (const value of [
383
- item?.[key],
384
- metadata[key],
385
- attrs[`langfuse.observation.${key}`],
386
- attrs[`langfuse.trace.${key}`],
387
- attrs[`${key}.value`],
388
- ]) {
389
- if (value !== undefined && value !== null && String(value).trim() !== "") return value;
390
- }
391
- return undefined;
392
- }
393
-
394
- function hasMetadataKey(item, key) {
395
- return metadataValue(item, key) !== undefined;
396
- }
374
+ function directMetadataValue(item, key) {
375
+ const metadata = item?.metadata || {};
376
+ return metadata[key];
377
+ }
378
+
379
+ function observationIOValue(item, key) {
380
+ const metadata = item?.metadata || {};
381
+ const attrs = metadata.attributes || {};
382
+ for (const value of [
383
+ item?.[key],
384
+ metadata[key],
385
+ attrs[`langfuse.observation.${key}`],
386
+ attrs[`langfuse.trace.${key}`],
387
+ attrs[`${key}.value`],
388
+ ]) {
389
+ if (value !== undefined && value !== null && String(value).trim() !== "") return value;
390
+ }
391
+ return undefined;
392
+ }
393
+
394
+ function hasMetadataKey(item, key) {
395
+ return metadataValue(item, key) !== undefined;
396
+ }
397
397
 
398
398
  function metricInteractionId(item, target) {
399
399
  return target === "opencode"
@@ -424,18 +424,18 @@ function expectedAgentTurnName(target) {
424
424
  async function observationsForTrace(config, traceId, since) {
425
425
  if (!traceId) return [];
426
426
  const params = { limit: 100, fields: "core,basic,usage", traceId };
427
- try {
428
- return dataArray(await langfuseGet(config, "/v2/observations", params));
429
- } catch (error) {
430
- if (!isLangfuseQueryShapeError(error) && error.status !== 404) throw error;
431
- }
432
- try {
433
- return dataArray(await langfuseGet(config, "/observations", { limit: 100, traceId }));
434
- } catch (error) {
435
- if (!isLangfuseQueryShapeError(error)) throw error;
436
- }
437
- const fallback = dataArray(await langfuseGetLenient(config, "/observations", { limit: 100, fromTimestamp: since.toISOString() }));
438
- return fallback.filter((item) => item.traceId === traceId || item.trace_id === traceId);
427
+ try {
428
+ return dataArray(await langfuseGet(config, "/v2/observations", params));
429
+ } catch (error) {
430
+ if (!isLangfuseQueryShapeError(error) && error.status !== 404) throw error;
431
+ }
432
+ try {
433
+ return dataArray(await langfuseGet(config, "/observations", { limit: 100, traceId }));
434
+ } catch (error) {
435
+ if (!isLangfuseQueryShapeError(error)) throw error;
436
+ }
437
+ const fallback = dataArray(await langfuseGetLenient(config, "/observations", { limit: 100, fromTimestamp: since.toISOString() }));
438
+ return fallback.filter((item) => item.traceId === traceId || item.trace_id === traceId);
439
439
  }
440
440
 
441
441
  function mergeMetricCandidates(items) {
@@ -456,12 +456,12 @@ async function recentMetricCandidates(config, since) {
456
456
  const candidates = [];
457
457
  for (const pathname of ["/traces"]) {
458
458
  for (const params of [{ ...baseParams, userId: config.userId }, baseParams]) {
459
- try {
460
- candidates.push(...dataArray(await langfuseGetLenient(config, pathname, params)));
461
- } catch (error) {
462
- if (error.name === "AbortError" || error.status === 404 || isLangfuseQueryShapeError(error)) continue;
463
- throw error;
464
- }
459
+ try {
460
+ candidates.push(...dataArray(await langfuseGetLenient(config, pathname, params)));
461
+ } catch (error) {
462
+ if (error.name === "AbortError" || error.status === 404 || isLangfuseQueryShapeError(error)) continue;
463
+ throw error;
464
+ }
465
465
  }
466
466
  }
467
467
  return mergeMetricCandidates(candidates);
@@ -539,20 +539,20 @@ async function verifyMetricObservations(config, found, { since, target, marker =
539
539
  throw new Error(`Metric verification failed for ${target}: Agent Turn is missing repo context ${key}.`);
540
540
  }
541
541
  }
542
- if (target === "opencode") {
543
- for (const key of ["interaction_id", "interaction_count", "token_metrics_available", "tool_call_count", "skill_use_count", "input_tokens", "output_tokens", "total_tokens"]) {
544
- if (metadataValue(item, key) === undefined) {
545
- throw new Error(`Metric verification failed for ${target}: effective metadata is missing ${key}.`);
546
- }
547
- }
548
- if (item?.name === expectedName) {
549
- for (const key of ["input", "output"]) {
550
- if (observationIOValue(item, key) === undefined) {
551
- throw new Error(`Metric verification failed for ${target}: ${expectedName} is missing ${key}.`);
552
- }
553
- }
554
- }
555
- }
542
+ if (target === "opencode") {
543
+ for (const key of ["interaction_id", "interaction_count", "token_metrics_available", "tool_call_count", "skill_use_count", "input_tokens", "output_tokens", "total_tokens"]) {
544
+ if (metadataValue(item, key) === undefined) {
545
+ throw new Error(`Metric verification failed for ${target}: effective metadata is missing ${key}.`);
546
+ }
547
+ }
548
+ if (item?.name === expectedName) {
549
+ for (const key of ["input", "output"]) {
550
+ if (observationIOValue(item, key) === undefined) {
551
+ throw new Error(`Metric verification failed for ${target}: ${expectedName} is missing ${key}.`);
552
+ }
553
+ }
554
+ }
555
+ }
556
556
  const tokenAvailable = metadataValue(item, "token_metrics_available");
557
557
  for (const tokenKey of ["input_tokens", "output_tokens", "total_tokens"]) {
558
558
  const value = metadataValue(item, tokenKey);
@@ -581,12 +581,12 @@ async function findLangfuseMarker(config, marker, { since, target }) {
581
581
 
582
582
  for (const params of traceQueries) {
583
583
  let traces;
584
- try {
585
- traces = await langfuseGetLenient(config, "/traces", params);
586
- } catch (error) {
587
- if (error.status === 404 || isLangfuseQueryShapeError(error)) continue;
588
- throw error;
589
- }
584
+ try {
585
+ traces = await langfuseGetLenient(config, "/traces", params);
586
+ } catch (error) {
587
+ if (error.status === 404 || isLangfuseQueryShapeError(error)) continue;
588
+ throw error;
589
+ }
590
590
  for (const trace of dataArray(traces)) {
591
591
  if (containsMarker(trace, marker)) return { kind: "trace-list", target, id: idOf(trace), item: trace };
592
592
  }
@@ -614,10 +614,10 @@ async function findLangfuseMarker(config, marker, { since, target }) {
614
614
  for (const observation of dataArray(observations)) {
615
615
  if (containsMarker(observation, marker)) return { kind: pathname, target, id: idOf(observation), item: observation };
616
616
  }
617
- } catch (error) {
618
- if (error.status === 404 || isLangfuseQueryShapeError(error)) continue;
619
- throw error;
620
- }
617
+ } catch (error) {
618
+ if (error.status === 404 || isLangfuseQueryShapeError(error)) continue;
619
+ throw error;
620
+ }
621
621
  }
622
622
 
623
623
  return null;