oh-langfuse 0.1.70 → 0.1.72
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-setup.mjs +16 -26
- package/scripts/langfuse-setup.mjs +14 -23
- package/scripts/opencode-langfuse-setup.mjs +78 -32
- package/scripts/real-self-verify.mjs +37 -15
- 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.72",
|
|
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
|
+
}
|
|
@@ -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})$/;
|
|
@@ -217,19 +218,11 @@ function listLocalCodexCliCandidates() {
|
|
|
217
218
|
return candidates;
|
|
218
219
|
}
|
|
219
220
|
|
|
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) {
|
|
221
|
+
function listCliCandidatesFromPath(target) {
|
|
222
|
+
return listSystemCliCandidates(target);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function sortWindowsCliCandidates(candidates) {
|
|
233
226
|
const extPriority = (candidate) => {
|
|
234
227
|
const ext = path.extname(String(candidate || "")).toLowerCase();
|
|
235
228
|
if (ext === ".cmd" || ext === ".bat" || ext === ".exe") return 0;
|
|
@@ -238,16 +231,13 @@ function sortWindowsCliCandidates(candidates) {
|
|
|
238
231
|
return [...candidates].sort((a, b) => extPriority(a) - extPriority(b));
|
|
239
232
|
}
|
|
240
233
|
|
|
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
|
-
];
|
|
234
|
+
function resolveAgentCli({ target, preferred = "", shimDir = "" }) {
|
|
235
|
+
const candidates = [
|
|
236
|
+
preferred,
|
|
237
|
+
...(target === "codex" ? listLocalCodexCliCandidates() : []),
|
|
238
|
+
...defaultWindowsNpmCliCandidates(target),
|
|
239
|
+
...listCliCandidatesFromPath(target)
|
|
240
|
+
];
|
|
251
241
|
for (const candidate of candidates) {
|
|
252
242
|
const found = existingCliCandidate(candidate, shimDir);
|
|
253
243
|
if (found) return found;
|
|
@@ -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;
|
|
@@ -413,16 +413,42 @@ function getPatchedLangfuseDistIndexJs() {
|
|
|
413
413
|
" toolCallId: callId || '',",
|
|
414
414
|
" };",
|
|
415
415
|
"};",
|
|
416
|
-
"const tokenMetricsFromPart = (part) => {",
|
|
417
|
-
" const tokens = part?.tokens ?? part?.usage ?? {};",
|
|
418
|
-
" return {",
|
|
419
|
-
" input: metricNumber(tokens.input ?? tokens.input_tokens ?? tokens.inputTokens),",
|
|
416
|
+
"const tokenMetricsFromPart = (part) => {",
|
|
417
|
+
" const tokens = part?.tokens ?? part?.usage ?? {};",
|
|
418
|
+
" return {",
|
|
419
|
+
" input: metricNumber(tokens.input ?? tokens.input_tokens ?? tokens.inputTokens),",
|
|
420
420
|
" output: metricNumber(tokens.output ?? tokens.output_tokens ?? tokens.outputTokens),",
|
|
421
421
|
" total: metricNumber(tokens.total ?? tokens.total_tokens ?? tokens.totalTokens),",
|
|
422
422
|
" cacheRead: metricNumber(tokens.cache?.read ?? tokens.cacheRead ?? tokens.cache_read_tokens ?? tokens.cachedInputTokens),",
|
|
423
|
-
" reasoning: metricNumber(tokens.reasoning ?? tokens.reasoning_tokens ?? tokens.reasoningTokens),",
|
|
424
|
-
" };",
|
|
425
|
-
"};",
|
|
423
|
+
" reasoning: metricNumber(tokens.reasoning ?? tokens.reasoning_tokens ?? tokens.reasoningTokens),",
|
|
424
|
+
" };",
|
|
425
|
+
"};",
|
|
426
|
+
"const OBSERVATION_TEXT_LIMIT = 20000;",
|
|
427
|
+
"const limitObservationText = (text) => {",
|
|
428
|
+
" const normalized = typeof text === 'string' ? text.trim() : '';",
|
|
429
|
+
" if (!normalized) return '';",
|
|
430
|
+
" return normalized.length > OBSERVATION_TEXT_LIMIT ? `${normalized.slice(0, OBSERVATION_TEXT_LIMIT)}\\n...[truncated]` : normalized;",
|
|
431
|
+
"};",
|
|
432
|
+
"const textFromContent = (value, depth = 0) => {",
|
|
433
|
+
" if (depth > 6 || value === null || value === undefined) return '';",
|
|
434
|
+
" if (typeof value === 'string') return value.trim();",
|
|
435
|
+
" if (Array.isArray(value)) return value.map((item) => textFromContent(item, depth + 1)).filter(Boolean).join('\\n').trim();",
|
|
436
|
+
" if (typeof value !== 'object') return '';",
|
|
437
|
+
" for (const key of ['text', 'content', 'prompt', 'message', 'input', 'output', 'parts']) {",
|
|
438
|
+
" const text = textFromContent(value[key], depth + 1);",
|
|
439
|
+
" if (text) return text;",
|
|
440
|
+
" }",
|
|
441
|
+
" return '';",
|
|
442
|
+
"};",
|
|
443
|
+
"const promptFromArgv = () => {",
|
|
444
|
+
" const args = process.argv.slice(2);",
|
|
445
|
+
" if (!args.includes('run')) return '';",
|
|
446
|
+
" for (let i = args.length - 1; i >= 0; i -= 1) {",
|
|
447
|
+
" const arg = args[i];",
|
|
448
|
+
" if (typeof arg === 'string' && arg.trim() && !arg.startsWith('-')) return arg.trim();",
|
|
449
|
+
" }",
|
|
450
|
+
" return '';",
|
|
451
|
+
"};",
|
|
426
452
|
"",
|
|
427
453
|
"const normalizeSkillNames = (names) => {",
|
|
428
454
|
" if (!Array.isArray(names)) return [];",
|
|
@@ -643,11 +669,14 @@ function getPatchedLangfuseDistIndexJs() {
|
|
|
643
669
|
" const sdkStartPromise = Promise.resolve().then(() => sdk.start()).catch((err) => {",
|
|
644
670
|
' log("warn", `OTEL SDK start failed: ${err?.message ?? err}`);',
|
|
645
671
|
" });",
|
|
646
|
-
" const getMetricsTracer = () => trace.getTracer('oh-langfuse-opencode-metrics');",
|
|
647
|
-
" const knownSkillNames = await collectKnownSkillNames();",
|
|
648
|
-
" const startupSkillUsages = detectOpencodeSkillUsages(process.argv.join('\\n'), knownSkillNames);",
|
|
649
|
-
" const
|
|
650
|
-
" const
|
|
672
|
+
" const getMetricsTracer = () => trace.getTracer('oh-langfuse-opencode-metrics');",
|
|
673
|
+
" const knownSkillNames = await collectKnownSkillNames();",
|
|
674
|
+
" const startupSkillUsages = detectOpencodeSkillUsages(process.argv.join('\\n'), knownSkillNames);",
|
|
675
|
+
" const startupPromptText = limitObservationText(promptFromArgv());",
|
|
676
|
+
" const messageTextById = new Map();",
|
|
677
|
+
" const messageInputById = new Map();",
|
|
678
|
+
" const lastUserTextBySessionId = new Map();",
|
|
679
|
+
" const skillUsagesByMessageId = new Map();",
|
|
651
680
|
" const skillUsagesBySessionId = new Map();",
|
|
652
681
|
" const startupSkillSessionIds = new Set();",
|
|
653
682
|
" const toolCallIdsByMessageId = new Map();",
|
|
@@ -717,9 +746,17 @@ function getPatchedLangfuseDistIndexJs() {
|
|
|
717
746
|
" const recordInteractionMetric = async (event) => {",
|
|
718
747
|
" const payload = eventPayload(event);",
|
|
719
748
|
" const part = eventPart(event);",
|
|
720
|
-
" const partType = part?.type ?? '';",
|
|
721
|
-
" const sessionId = pickEventString(part?.sessionID, part?.sessionId, payload?.sessionID, payload?.sessionId, payload?.session?.id, event?.sessionID, event?.sessionId);",
|
|
722
|
-
" const messageId = pickEventString(part?.messageID, part?.messageId, payload?.messageID, payload?.messageId, payload?.message?.id, event?.messageID, event?.messageId);",
|
|
749
|
+
" const partType = part?.type ?? '';",
|
|
750
|
+
" const sessionId = pickEventString(part?.sessionID, part?.sessionId, payload?.sessionID, payload?.sessionId, payload?.session?.id, event?.sessionID, event?.sessionId);",
|
|
751
|
+
" const messageId = pickEventString(part?.messageID, part?.messageId, payload?.messageID, payload?.messageId, payload?.message?.id, event?.messageID, event?.messageId);",
|
|
752
|
+
" const role = pickEventString(part?.role, payload?.role, payload?.message?.role, event?.role).toLowerCase();",
|
|
753
|
+
" const eventText = textFromContent(part) || textFromContent(payload?.message) || textFromContent(payload);",
|
|
754
|
+
" if (eventText && role === 'user') {",
|
|
755
|
+
" const inputText = limitObservationText(eventText);",
|
|
756
|
+
" if (messageId) messageInputById.set(messageId, inputText);",
|
|
757
|
+
" if (sessionId) lastUserTextBySessionId.set(sessionId, inputText);",
|
|
758
|
+
" return;",
|
|
759
|
+
" }",
|
|
723
760
|
" if (sessionId && startupSkillUsages.length && !startupSkillSessionIds.has(sessionId)) {",
|
|
724
761
|
" startupSkillSessionIds.add(sessionId);",
|
|
725
762
|
" rememberSkillUsages(skillUsagesBySessionId, sessionId, startupSkillUsages);",
|
|
@@ -733,11 +770,11 @@ function getPatchedLangfuseDistIndexJs() {
|
|
|
733
770
|
" rememberToolActivity(toolCallIdsBySessionId, sessionId, toolActivity, 'toolCallCount');",
|
|
734
771
|
" rememberToolActivity(toolResultIdsByMessageId, messageId, toolActivity, 'toolResultCount');",
|
|
735
772
|
" rememberToolActivity(toolResultIdsBySessionId, sessionId, toolActivity, 'toolResultCount');",
|
|
736
|
-
" if (
|
|
737
|
-
" messageTextById.set(messageId,
|
|
738
|
-
" const textSkillUsages = detectOpencodeSkillUsages(
|
|
739
|
-
" rememberSkillUsages(skillUsagesByMessageId, messageId, textSkillUsages);",
|
|
740
|
-
" rememberSkillUsages(skillUsagesBySessionId, sessionId, textSkillUsages);",
|
|
773
|
+
" if (eventText && messageId && (role === 'assistant' || partType === 'text')) {",
|
|
774
|
+
" messageTextById.set(messageId, limitObservationText(eventText));",
|
|
775
|
+
" const textSkillUsages = detectOpencodeSkillUsages(eventText, knownSkillNames);",
|
|
776
|
+
" rememberSkillUsages(skillUsagesByMessageId, messageId, textSkillUsages);",
|
|
777
|
+
" rememberSkillUsages(skillUsagesBySessionId, sessionId, textSkillUsages);",
|
|
741
778
|
" return;",
|
|
742
779
|
" }",
|
|
743
780
|
" if (partType !== 'step-finish' || !messageId || emittedMessageIds.has(messageId)) return;",
|
|
@@ -745,10 +782,11 @@ function getPatchedLangfuseDistIndexJs() {
|
|
|
745
782
|
" const tokenMetrics = tokenMetricsFromPart(part);",
|
|
746
783
|
" const total = tokenMetrics.total ?? (tokenMetrics.input !== undefined && tokenMetrics.output !== undefined ? tokenMetrics.input + tokenMetrics.output : undefined);",
|
|
747
784
|
" const tokenAvailable = [tokenMetrics.input, tokenMetrics.output, total, tokenMetrics.cacheRead, tokenMetrics.reasoning].some((value) => value !== undefined);",
|
|
748
|
-
" await sdkStartPromise;",
|
|
749
|
-
" const span = getMetricsTracer().startSpan('OpenCode Agent Turn');",
|
|
750
|
-
" const text = messageTextById.get(messageId) || '';",
|
|
751
|
-
" const
|
|
785
|
+
" await sdkStartPromise;",
|
|
786
|
+
" const span = getMetricsTracer().startSpan('OpenCode Agent Turn');",
|
|
787
|
+
" const text = messageTextById.get(messageId) || '';",
|
|
788
|
+
" const inputText = messageInputById.get(messageId) || lastUserTextBySessionId.get(sessionId) || startupPromptText || '';",
|
|
789
|
+
" const skillUsages = dedupeSkillUsages([...(skillUsagesByMessageId.get(messageId) ?? []), ...(skillUsagesBySessionId.get(sessionId) ?? [])]);",
|
|
752
790
|
" const interactionId = `opencode:${userId || \"unknown\"}:${sessionId || \"unknown\"}:${messageId}`;",
|
|
753
791
|
" const skillUseEvents = buildSkillUseEvents(interactionId, skillUsages);",
|
|
754
792
|
" const skillNames = uniqueSkillNames(skillUsages);",
|
|
@@ -781,14 +819,22 @@ function getPatchedLangfuseDistIndexJs() {
|
|
|
781
819
|
' if (skillInvocationModes.length) span.setAttribute("langfuse.observation.metadata.skill_invocation_modes", skillInvocationModes);',
|
|
782
820
|
' if (skillAgentPaths.length) span.setAttribute("langfuse.observation.metadata.skill_agent_paths", skillAgentPaths);',
|
|
783
821
|
' if (tokenMetrics.input !== undefined) span.setAttribute("langfuse.observation.metadata.input_tokens", tokenMetrics.input);',
|
|
784
|
-
' if (tokenMetrics.output !== undefined) span.setAttribute("langfuse.observation.metadata.output_tokens", tokenMetrics.output);',
|
|
785
|
-
' if (total !== undefined) span.setAttribute("langfuse.observation.metadata.total_tokens", total);',
|
|
786
|
-
' if (tokenMetrics.cacheRead !== undefined) span.setAttribute("langfuse.observation.metadata.cache_read_tokens", tokenMetrics.cacheRead);',
|
|
787
|
-
' if (tokenMetrics.reasoning !== undefined) span.setAttribute("langfuse.observation.metadata.reasoning_tokens", tokenMetrics.reasoning);',
|
|
788
|
-
' if (
|
|
789
|
-
'
|
|
790
|
-
|
|
791
|
-
|
|
822
|
+
' if (tokenMetrics.output !== undefined) span.setAttribute("langfuse.observation.metadata.output_tokens", tokenMetrics.output);',
|
|
823
|
+
' if (total !== undefined) span.setAttribute("langfuse.observation.metadata.total_tokens", total);',
|
|
824
|
+
' if (tokenMetrics.cacheRead !== undefined) span.setAttribute("langfuse.observation.metadata.cache_read_tokens", tokenMetrics.cacheRead);',
|
|
825
|
+
' if (tokenMetrics.reasoning !== undefined) span.setAttribute("langfuse.observation.metadata.reasoning_tokens", tokenMetrics.reasoning);',
|
|
826
|
+
' if (inputText) span.setAttribute("langfuse.observation.input", inputText);',
|
|
827
|
+
' if (inputText) span.setAttribute("langfuse.trace.input", inputText);',
|
|
828
|
+
' if (inputText) span.setAttribute("input.value", inputText);',
|
|
829
|
+
' if (text) span.setAttribute("langfuse.observation.output", text);',
|
|
830
|
+
' if (text) span.setAttribute("langfuse.trace.output", text);',
|
|
831
|
+
' if (text) span.setAttribute("output.value", text);',
|
|
832
|
+
' if (inputText) span.setAttribute("langfuse.observation.metadata.input_text_preview", inputText.slice(0, 512));',
|
|
833
|
+
' if (text) span.setAttribute("langfuse.observation.metadata.output_text_preview", text.slice(0, 512));',
|
|
834
|
+
' writeRepoContextMetrics(span, collectRepoContext(process.cwd(), "process"));',
|
|
835
|
+
" span.end();",
|
|
836
|
+
" messageTextById.delete(messageId);",
|
|
837
|
+
" messageInputById.delete(messageId);",
|
|
792
838
|
" skillUsagesByMessageId.delete(messageId);",
|
|
793
839
|
" skillUsagesBySessionId.delete(sessionId);",
|
|
794
840
|
" toolCallIdsByMessageId.delete(messageId);",
|
|
@@ -354,14 +354,29 @@ function metadataValue(item, key) {
|
|
|
354
354
|
return undefined;
|
|
355
355
|
}
|
|
356
356
|
|
|
357
|
-
function directMetadataValue(item, key) {
|
|
358
|
-
const metadata = item?.metadata || {};
|
|
359
|
-
return metadata[key];
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
function
|
|
363
|
-
|
|
364
|
-
}
|
|
357
|
+
function directMetadataValue(item, key) {
|
|
358
|
+
const metadata = item?.metadata || {};
|
|
359
|
+
return metadata[key];
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function observationIOValue(item, key) {
|
|
363
|
+
const metadata = item?.metadata || {};
|
|
364
|
+
const attrs = metadata.attributes || {};
|
|
365
|
+
for (const value of [
|
|
366
|
+
item?.[key],
|
|
367
|
+
metadata[key],
|
|
368
|
+
attrs[`langfuse.observation.${key}`],
|
|
369
|
+
attrs[`langfuse.trace.${key}`],
|
|
370
|
+
attrs[`${key}.value`],
|
|
371
|
+
]) {
|
|
372
|
+
if (value !== undefined && value !== null && String(value).trim() !== "") return value;
|
|
373
|
+
}
|
|
374
|
+
return undefined;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function hasMetadataKey(item, key) {
|
|
378
|
+
return metadataValue(item, key) !== undefined;
|
|
379
|
+
}
|
|
365
380
|
|
|
366
381
|
function metricInteractionId(item, target) {
|
|
367
382
|
return target === "opencode"
|
|
@@ -507,13 +522,20 @@ async function verifyMetricObservations(config, found, { since, target, marker =
|
|
|
507
522
|
throw new Error(`Metric verification failed for ${target}: Agent Turn is missing repo context ${key}.`);
|
|
508
523
|
}
|
|
509
524
|
}
|
|
510
|
-
if (target === "opencode") {
|
|
511
|
-
for (const key of ["interaction_id", "interaction_count", "token_metrics_available", "tool_call_count", "skill_use_count", "input_tokens", "output_tokens", "total_tokens"]) {
|
|
512
|
-
if (metadataValue(item, key) === undefined) {
|
|
513
|
-
throw new Error(`Metric verification failed for ${target}: effective metadata is missing ${key}.`);
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
|
|
525
|
+
if (target === "opencode") {
|
|
526
|
+
for (const key of ["interaction_id", "interaction_count", "token_metrics_available", "tool_call_count", "skill_use_count", "input_tokens", "output_tokens", "total_tokens"]) {
|
|
527
|
+
if (metadataValue(item, key) === undefined) {
|
|
528
|
+
throw new Error(`Metric verification failed for ${target}: effective metadata is missing ${key}.`);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
if (item?.name === expectedName) {
|
|
532
|
+
for (const key of ["input", "output"]) {
|
|
533
|
+
if (observationIOValue(item, key) === undefined) {
|
|
534
|
+
throw new Error(`Metric verification failed for ${target}: ${expectedName} is missing ${key}.`);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
517
539
|
const tokenAvailable = metadataValue(item, "token_metrics_available");
|
|
518
540
|
for (const tokenKey of ["input_tokens", "output_tokens", "total_tokens"]) {
|
|
519
541
|
const value = metadataValue(item, tokenKey);
|
|
@@ -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")) || {};
|