oh-langfuse 0.1.59 → 0.1.61
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 +245 -245
- package/bin/cli.js +22 -22
- package/codex_langfuse_notify.py +422 -56
- package/package.json +6 -6
- package/scripts/auto-update-runtime.mjs +28 -15
- package/scripts/codex-langfuse-setup.mjs +127 -117
- package/scripts/langfuse-setup.mjs +71 -71
- package/scripts/opencode-langfuse-run.mjs +23 -23
- package/scripts/opencode-langfuse-setup.mjs +140 -64
- package/scripts/real-self-verify.mjs +114 -104
- package/scripts/update-langfuse-runtime.mjs +7 -6
- package/scripts/verify-update-runtime.mjs +162 -162
- package/scripts/verify-update-utils.mjs +60 -60
|
@@ -9,9 +9,10 @@ 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 = "http://120.46.221.227:3000";
|
|
13
|
-
const DEFAULT_LANGFUSE_PUBLIC_KEY = "pk-lf-da0c90a7-6e93-4eb7-bb86-c1047c8d187d";
|
|
14
|
-
const DEFAULT_LANGFUSE_SECRET_KEY = "sk-lf-0269b85d-bfdc-442c-bfa3-e737954e3315";
|
|
12
|
+
const DEFAULT_LANGFUSE_BASE_URL = "http://120.46.221.227:3000";
|
|
13
|
+
const DEFAULT_LANGFUSE_PUBLIC_KEY = "pk-lf-da0c90a7-6e93-4eb7-bb86-c1047c8d187d";
|
|
14
|
+
const DEFAULT_LANGFUSE_SECRET_KEY = "sk-lf-0269b85d-bfdc-442c-bfa3-e737954e3315";
|
|
15
|
+
const OPENCODE_NATIVE_AGENT_TURN_NAMES = new Set(["session.llm:ai.streamText.doStream"]);
|
|
15
16
|
|
|
16
17
|
function parseArgs(argv) {
|
|
17
18
|
const args = { _: [] };
|
|
@@ -136,36 +137,36 @@ function printRunResult(label, result) {
|
|
|
136
137
|
}
|
|
137
138
|
}
|
|
138
139
|
|
|
139
|
-
function findOnPath(names) {
|
|
140
|
-
for (const name of names) {
|
|
141
|
-
const probe = run(name, ["--version"], { timeoutMs: 15000 });
|
|
142
|
-
if (probe.status === 0) return name;
|
|
143
|
-
}
|
|
144
|
-
return "";
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function localCodexCliCandidates() {
|
|
148
|
-
if (process.platform !== "win32") return ["codex"];
|
|
149
|
-
const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local");
|
|
150
|
-
const codexBin = path.join(localAppData, "OpenAI", "Codex", "bin");
|
|
151
|
-
const candidates = [
|
|
152
|
-
process.env.CODEX_CLI_PATH || "",
|
|
153
|
-
path.join(codexBin, "codex.exe"),
|
|
154
|
-
];
|
|
155
|
-
try {
|
|
156
|
-
if (fs.existsSync(codexBin)) {
|
|
157
|
-
for (const ent of fs.readdirSync(codexBin, { withFileTypes: true })) {
|
|
158
|
-
if (ent.isDirectory()) candidates.push(path.join(codexBin, ent.name, "codex.exe"));
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
} catch {
|
|
162
|
-
// Fall back to PATH probing below.
|
|
163
|
-
}
|
|
164
|
-
return [...candidates, "codex.cmd", "codex.exe", "codex"].filter(Boolean);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function configFromArgs(args) {
|
|
168
|
-
return {
|
|
140
|
+
function findOnPath(names) {
|
|
141
|
+
for (const name of names) {
|
|
142
|
+
const probe = run(name, ["--version"], { timeoutMs: 15000 });
|
|
143
|
+
if (probe.status === 0) return name;
|
|
144
|
+
}
|
|
145
|
+
return "";
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function localCodexCliCandidates() {
|
|
149
|
+
if (process.platform !== "win32") return ["codex"];
|
|
150
|
+
const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local");
|
|
151
|
+
const codexBin = path.join(localAppData, "OpenAI", "Codex", "bin");
|
|
152
|
+
const candidates = [
|
|
153
|
+
process.env.CODEX_CLI_PATH || "",
|
|
154
|
+
path.join(codexBin, "codex.exe"),
|
|
155
|
+
];
|
|
156
|
+
try {
|
|
157
|
+
if (fs.existsSync(codexBin)) {
|
|
158
|
+
for (const ent of fs.readdirSync(codexBin, { withFileTypes: true })) {
|
|
159
|
+
if (ent.isDirectory()) candidates.push(path.join(codexBin, ent.name, "codex.exe"));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
} catch {
|
|
163
|
+
// Fall back to PATH probing below.
|
|
164
|
+
}
|
|
165
|
+
return [...candidates, "codex.cmd", "codex.exe", "codex"].filter(Boolean);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function configFromArgs(args) {
|
|
169
|
+
return {
|
|
169
170
|
baseUrl:
|
|
170
171
|
String(args.langfuseBaseUrl || args.langfuseHost || args.host || process.env.LANGFUSE_BASEURL || process.env.LANGFUSE_HOST || DEFAULT_LANGFUSE_BASE_URL),
|
|
171
172
|
publicKey: String(args.publicKey || process.env.LANGFUSE_PUBLIC_KEY || DEFAULT_LANGFUSE_PUBLIC_KEY),
|
|
@@ -233,9 +234,9 @@ function triggerOpenCode(prompt, args, env) {
|
|
|
233
234
|
return last;
|
|
234
235
|
}
|
|
235
236
|
|
|
236
|
-
function triggerCodex(prompt, args, env) {
|
|
237
|
-
const cmd = String(args.codexCmd || findOnPath(localCodexCliCandidates()));
|
|
238
|
-
if (!cmd) return { status: 127, stdout: "", stderr: "Codex CLI not found. Set --codexCmd=<path>.", command: "codex", args: [] };
|
|
237
|
+
function triggerCodex(prompt, args, env) {
|
|
238
|
+
const cmd = String(args.codexCmd || findOnPath(localCodexCliCandidates()));
|
|
239
|
+
if (!cmd) return { status: 127, stdout: "", stderr: "Codex CLI not found. Set --codexCmd=<path>.", command: "codex", args: [] };
|
|
239
240
|
const candidates = [
|
|
240
241
|
["exec", prompt],
|
|
241
242
|
["exec", "--skip-git-repo-check", prompt],
|
|
@@ -322,31 +323,34 @@ function metadataValue(item, key) {
|
|
|
322
323
|
const attrs = metadata.attributes || {};
|
|
323
324
|
const prefixed = `langfuse.observation.metadata.${key}`;
|
|
324
325
|
if (attrs[prefixed] !== undefined) return attrs[prefixed];
|
|
325
|
-
const ohKey = `oh.langfuse.${key}`;
|
|
326
|
-
if (attrs[ohKey] !== undefined) return attrs[ohKey];
|
|
327
|
-
if (metadata.source === "opencode" || attrs["oh.langfuse.source"] === "opencode") {
|
|
328
|
-
|
|
329
|
-
if (key === "
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
"ai.usage.
|
|
326
|
+
const ohKey = `oh.langfuse.${key}`;
|
|
327
|
+
if (attrs[ohKey] !== undefined) return attrs[ohKey];
|
|
328
|
+
if (metadata.source === "opencode" || attrs["oh.langfuse.source"] === "opencode") {
|
|
329
|
+
const usage = item?.usage || {};
|
|
330
|
+
if (key === "interaction_id") return `opencode:${metadata.user_id || attrs["oh.langfuse.user_id"] || "unknown"}:${item?.id || item?.traceId || item?.trace_id || "unknown"}`;
|
|
331
|
+
if (key === "interaction_count") return 1;
|
|
332
|
+
if (key === "token_metrics_available") {
|
|
333
|
+
return usage.input !== undefined || usage.output !== undefined || usage.total !== undefined || [
|
|
334
|
+
"ai.usage.inputTokens",
|
|
335
|
+
"ai.usage.outputTokens",
|
|
336
|
+
"ai.usage.totalTokens",
|
|
334
337
|
"ai.usage.promptTokens",
|
|
335
338
|
"ai.usage.completionTokens",
|
|
336
339
|
].some((attrKey) => attrs[attrKey] !== undefined);
|
|
337
340
|
}
|
|
338
|
-
if (key === "tool_call_count" || key === "skill_use_count") return 0;
|
|
339
|
-
const tokenAttrMap = {
|
|
340
|
-
input_tokens: ["ai.usage.inputTokens", "ai.usage.promptTokens"],
|
|
341
|
-
output_tokens: ["ai.usage.outputTokens", "ai.usage.completionTokens"],
|
|
342
|
-
total_tokens: ["ai.usage.totalTokens"],
|
|
343
|
-
cache_read_tokens: ["ai.usage.cachedInputTokens", "ai.usage.inputTokenDetails.cacheReadTokens"],
|
|
344
|
-
reasoning_tokens: ["ai.usage.reasoningTokens", "ai.usage.outputTokenDetails.reasoningTokens"],
|
|
345
|
-
};
|
|
346
|
-
for (const attrKey of tokenAttrMap[key] || []) {
|
|
347
|
-
if (
|
|
348
|
-
|
|
349
|
-
|
|
341
|
+
if (key === "tool_call_count" || key === "skill_use_count") return 0;
|
|
342
|
+
const tokenAttrMap = {
|
|
343
|
+
input_tokens: ["input", "ai.usage.inputTokens", "ai.usage.promptTokens"],
|
|
344
|
+
output_tokens: ["output", "ai.usage.outputTokens", "ai.usage.completionTokens"],
|
|
345
|
+
total_tokens: ["total", "ai.usage.totalTokens"],
|
|
346
|
+
cache_read_tokens: ["ai.usage.cachedInputTokens", "ai.usage.inputTokenDetails.cacheReadTokens"],
|
|
347
|
+
reasoning_tokens: ["ai.usage.reasoningTokens", "ai.usage.outputTokenDetails.reasoningTokens"],
|
|
348
|
+
};
|
|
349
|
+
for (const attrKey of tokenAttrMap[key] || []) {
|
|
350
|
+
if (usage[attrKey] !== undefined) return usage[attrKey];
|
|
351
|
+
if (attrs[attrKey] !== undefined) return attrs[attrKey];
|
|
352
|
+
}
|
|
353
|
+
}
|
|
350
354
|
return undefined;
|
|
351
355
|
}
|
|
352
356
|
|
|
@@ -366,45 +370,51 @@ function metricInteractionId(item, target) {
|
|
|
366
370
|
}
|
|
367
371
|
|
|
368
372
|
function isAgentTurnObservation(item) {
|
|
369
|
-
|
|
373
|
+
const metadata = item?.metadata || {};
|
|
374
|
+
const attrs = metadata.attributes || {};
|
|
375
|
+
const isOpencode = metadata.source === "opencode" || attrs["oh.langfuse.source"] === "opencode";
|
|
376
|
+
if (/^(Claude|OpenCode|Codex) Agent Turn$/.test(String(item?.name || "")) || item?.name === "Agent Turn" || (!isOpencode && (metadataValue(item, "interaction_count") === 1 || metadataValue(item, "interaction_count") === "1"))) {
|
|
370
377
|
return true;
|
|
371
378
|
}
|
|
372
|
-
|
|
373
|
-
const
|
|
374
|
-
const
|
|
375
|
-
const hasModel = attrs["ai.model.id"] !== undefined || attrs["ai.model.provider"] !== undefined;
|
|
376
|
-
const isTool = attrs["ai.toolCall.name"] !== undefined || attrs["ai.toolCall.id"] !== undefined;
|
|
379
|
+
if (isOpencode && OPENCODE_NATIVE_AGENT_TURN_NAMES.has(String(item?.name || ""))) return true;
|
|
380
|
+
const hasModel = attrs["ai.model.id"] !== undefined || attrs["ai.model.provider"] !== undefined;
|
|
381
|
+
const isTool = attrs["ai.toolCall.name"] !== undefined || attrs["ai.toolCall.id"] !== undefined;
|
|
377
382
|
return isOpencode && hasModel && !isTool && item?.type === "GENERATION";
|
|
378
383
|
}
|
|
379
|
-
|
|
380
|
-
function expectedAgentTurnName(target) {
|
|
381
|
-
if (target === "claude") return "Claude Agent Turn";
|
|
382
|
-
if (target === "opencode") return "OpenCode Agent Turn";
|
|
383
|
-
if (target === "codex") return "Codex Agent Turn";
|
|
384
|
-
return "Agent Turn";
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
async function observationsForTrace(config, traceId, since) {
|
|
388
|
-
if (!traceId) return [];
|
|
389
|
-
const params = { limit: 100, traceId };
|
|
390
|
-
try {
|
|
391
|
-
return dataArray(await langfuseGet(config, "/observations", params));
|
|
392
|
-
} catch (error) {
|
|
393
|
-
if (error.status !== 400) throw error;
|
|
394
|
-
}
|
|
384
|
+
|
|
385
|
+
function expectedAgentTurnName(target) {
|
|
386
|
+
if (target === "claude") return "Claude Agent Turn";
|
|
387
|
+
if (target === "opencode") return "OpenCode Agent Turn";
|
|
388
|
+
if (target === "codex") return "Codex Agent Turn";
|
|
389
|
+
return "Agent Turn";
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
async function observationsForTrace(config, traceId, since) {
|
|
393
|
+
if (!traceId) return [];
|
|
394
|
+
const params = { limit: 100, fields: "core,basic,usage", traceId };
|
|
395
|
+
try {
|
|
396
|
+
return dataArray(await langfuseGet(config, "/v2/observations", params));
|
|
397
|
+
} catch (error) {
|
|
398
|
+
if (error.status !== 400 && error.status !== 404) throw error;
|
|
399
|
+
}
|
|
400
|
+
try {
|
|
401
|
+
return dataArray(await langfuseGet(config, "/observations", { limit: 100, traceId }));
|
|
402
|
+
} catch (error) {
|
|
403
|
+
if (error.status !== 400) throw error;
|
|
404
|
+
}
|
|
395
405
|
const fallback = dataArray(await langfuseGetLenient(config, "/observations", { limit: 100, fromTimestamp: since.toISOString() }));
|
|
396
406
|
return fallback.filter((item) => item.traceId === traceId || item.trace_id === traceId);
|
|
397
407
|
}
|
|
398
408
|
|
|
399
409
|
function mergeMetricCandidates(items) {
|
|
400
410
|
const out = [];
|
|
401
|
-
const seen = new Set();
|
|
402
|
-
for (const item of items || []) {
|
|
403
|
-
if (!item || typeof item !== "object") continue;
|
|
404
|
-
const key =
|
|
405
|
-
if (seen.has(key)) continue;
|
|
406
|
-
seen.add(key);
|
|
407
|
-
out.push(item);
|
|
411
|
+
const seen = new Set();
|
|
412
|
+
for (const item of items || []) {
|
|
413
|
+
if (!item || typeof item !== "object") continue;
|
|
414
|
+
const key = idOf(item) || [item.name || "", item.traceId || item.trace_id || ""].join(":");
|
|
415
|
+
if (seen.has(key)) continue;
|
|
416
|
+
seen.add(key);
|
|
417
|
+
out.push(item);
|
|
408
418
|
}
|
|
409
419
|
return out;
|
|
410
420
|
}
|
|
@@ -452,17 +462,17 @@ async function verifyMetricObservations(config, found, { since, target, marker =
|
|
|
452
462
|
throw new Error(`Metric verification failed for ${target}: Skill Use observations should not be emitted as standalone metrics.`);
|
|
453
463
|
}
|
|
454
464
|
|
|
455
|
-
if (!interactions.length) {
|
|
456
|
-
throw new Error(`Metric verification failed for ${target}: Agent Turn observation was not found for trace ${traceId || found.id}.`);
|
|
465
|
+
if (!interactions.length) {
|
|
466
|
+
throw new Error(`Metric verification failed for ${target}: Agent Turn observation was not found for trace ${traceId || found.id}.`);
|
|
457
467
|
}
|
|
458
468
|
|
|
459
469
|
const expectedName = expectedAgentTurnName(target);
|
|
460
|
-
if (!interactions.some((item) => item?.name === expectedName)) {
|
|
470
|
+
if (target !== "opencode" && !interactions.some((item) => item?.name === expectedName)) {
|
|
461
471
|
const names = interactions.map((item) => item?.name || "<unnamed>").join(", ");
|
|
462
472
|
throw new Error(`Metric verification failed for ${target}: expected observation name ${expectedName}, found ${names}.`);
|
|
463
473
|
}
|
|
464
|
-
|
|
465
|
-
const byInteractionId = new Map();
|
|
474
|
+
|
|
475
|
+
const byInteractionId = new Map();
|
|
466
476
|
const seenInteractionIds = new Set();
|
|
467
477
|
for (const item of interactions) {
|
|
468
478
|
const interactionId = metricInteractionId(item, target);
|
|
@@ -477,13 +487,13 @@ async function verifyMetricObservations(config, found, { since, target, marker =
|
|
|
477
487
|
throw new Error(`Metric verification failed for ${target}: Agent Turn is missing ${key}.`);
|
|
478
488
|
}
|
|
479
489
|
}
|
|
480
|
-
if (target === "opencode") {
|
|
481
|
-
for (const key of ["interaction_id", "interaction_count", "token_metrics_available", "tool_call_count", "skill_use_count", "input_tokens", "output_tokens", "total_tokens"]) {
|
|
482
|
-
if (
|
|
483
|
-
throw new Error(`Metric verification failed for ${target}:
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
}
|
|
490
|
+
if (target === "opencode") {
|
|
491
|
+
for (const key of ["interaction_id", "interaction_count", "token_metrics_available", "tool_call_count", "skill_use_count", "input_tokens", "output_tokens", "total_tokens"]) {
|
|
492
|
+
if (metadataValue(item, key) === undefined) {
|
|
493
|
+
throw new Error(`Metric verification failed for ${target}: effective metadata is missing ${key}.`);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
487
497
|
const tokenAvailable = metadataValue(item, "token_metrics_available");
|
|
488
498
|
for (const tokenKey of ["input_tokens", "output_tokens", "total_tokens"]) {
|
|
489
499
|
const value = metadataValue(item, tokenKey);
|
|
@@ -632,12 +642,12 @@ async function main() {
|
|
|
632
642
|
}
|
|
633
643
|
}
|
|
634
644
|
|
|
635
|
-
const found = await pollLangfuse(config, marker, { ...args, since, target });
|
|
636
|
-
console.log(`[OK] Langfuse marker found for ${target}: ${found.kind} ${found.id || ""}`.trim());
|
|
637
|
-
const metrics = await verifyMetricObservations(config, found, { since, target, marker });
|
|
638
|
-
console.log(`[OK] Langfuse metrics found for ${target}: ${expectedAgentTurnName(target)} x${metrics.interactionCount}`);
|
|
639
|
-
results.push({ target, marker, langfuse: { kind: found.kind, id: found.id || "", traceId: metrics.traceId }, metrics });
|
|
640
|
-
}
|
|
645
|
+
const found = await pollLangfuse(config, marker, { ...args, since, target });
|
|
646
|
+
console.log(`[OK] Langfuse marker found for ${target}: ${found.kind} ${found.id || ""}`.trim());
|
|
647
|
+
const metrics = await verifyMetricObservations(config, found, { since, target, marker });
|
|
648
|
+
console.log(`[OK] Langfuse metrics found for ${target}: ${expectedAgentTurnName(target)} x${metrics.interactionCount}`);
|
|
649
|
+
results.push({ target, marker, langfuse: { kind: found.kind, id: found.id || "", traceId: metrics.traceId }, metrics });
|
|
650
|
+
}
|
|
641
651
|
|
|
642
652
|
console.log("");
|
|
643
653
|
console.log(JSON.stringify({ ok: true, package: packageJson.name, marker, results }, null, 2));
|
|
@@ -126,6 +126,7 @@ function runNodeScript(script, args, options = {}) {
|
|
|
126
126
|
const result = spawnSync(process.execPath, [path.join(rootDir, "scripts", script), ...args], {
|
|
127
127
|
cwd: rootDir,
|
|
128
128
|
stdio: "inherit",
|
|
129
|
+
encoding: "utf8",
|
|
129
130
|
windowsHide: true,
|
|
130
131
|
timeout: options.timeoutMs || 900000,
|
|
131
132
|
});
|
|
@@ -254,9 +255,9 @@ async function main() {
|
|
|
254
255
|
printUpdateSummary(targets);
|
|
255
256
|
}
|
|
256
257
|
|
|
257
|
-
main()
|
|
258
|
-
.then(() => process.exit(0))
|
|
259
|
-
.catch((error) => {
|
|
260
|
-
console.error(`[FAIL] ${error?.message || String(error)}`);
|
|
261
|
-
process.exit(1);
|
|
262
|
-
});
|
|
258
|
+
main()
|
|
259
|
+
.then(() => process.exit(0))
|
|
260
|
+
.catch((error) => {
|
|
261
|
+
console.error(`[FAIL] ${error?.message || String(error)}`);
|
|
262
|
+
process.exit(1);
|
|
263
|
+
});
|
|
@@ -1,162 +1,162 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import os from "node:os";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { extractVersionFromNpmMetadata } from "./update-utils.mjs";
|
|
5
|
-
import { readRuntimeState, runtimeStatePath } from "./runtime-state-utils.mjs";
|
|
6
|
-
import {
|
|
7
|
-
buildVerifyUpdateResults,
|
|
8
|
-
normalizeVerifyTargets,
|
|
9
|
-
targetLabel,
|
|
10
|
-
verifyUpdateExitCode,
|
|
11
|
-
} from "./verify-update-utils.mjs";
|
|
12
|
-
|
|
13
|
-
const colorEnabled = process.stdout.isTTY && process.env.NO_COLOR !== "1";
|
|
14
|
-
const ansi = (code) => (colorEnabled ? `\x1b[${code}m` : "");
|
|
15
|
-
const t = {
|
|
16
|
-
reset: ansi(0),
|
|
17
|
-
bold: ansi(1),
|
|
18
|
-
green: ansi("92"),
|
|
19
|
-
gold: ansi("93"),
|
|
20
|
-
red: ansi("91"),
|
|
21
|
-
cyan: ansi("96"),
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
function paint(text, ...styles) {
|
|
25
|
-
if (!colorEnabled) return text;
|
|
26
|
-
return `${styles.join("")}${text}${t.reset}`;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function parseArgs(argv) {
|
|
30
|
-
const args = { _: [] };
|
|
31
|
-
for (const raw of argv) {
|
|
32
|
-
if (!raw.startsWith("--")) {
|
|
33
|
-
args._.push(raw);
|
|
34
|
-
continue;
|
|
35
|
-
}
|
|
36
|
-
const eq = raw.indexOf("=");
|
|
37
|
-
if (eq === -1) args[raw.slice(2)] = true;
|
|
38
|
-
else args[raw.slice(2, eq)] = raw.slice(eq + 1);
|
|
39
|
-
}
|
|
40
|
-
return args;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function stripBom(s) {
|
|
44
|
-
return typeof s === "string" && s.charCodeAt(0) === 0xfeff ? s.slice(1) : s;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function readJsonIfExists(p) {
|
|
48
|
-
try {
|
|
49
|
-
if (!fs.existsSync(p)) return null;
|
|
50
|
-
const text = stripBom(fs.readFileSync(p, "utf8"));
|
|
51
|
-
if (!text.trim()) return null;
|
|
52
|
-
return JSON.parse(text);
|
|
53
|
-
} catch {
|
|
54
|
-
return null;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function codexHome(home = os.homedir()) {
|
|
59
|
-
return process.env.CODEX_HOME || path.join(home, ".codex");
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function detectInstalledTargets(home = os.homedir()) {
|
|
63
|
-
return {
|
|
64
|
-
claude: fs.existsSync(path.join(home, ".claude", "hooks", "langfuse_hook.py")),
|
|
65
|
-
opencode:
|
|
66
|
-
fs.existsSync(path.join(home, ".config", "opencode", "opencode.json")) ||
|
|
67
|
-
fs.existsSync(path.join(home, ".config", "opencode", "plugins", "opencode-plugin-langfuse")),
|
|
68
|
-
codex: fs.existsSync(path.join(codexHome(home), "hooks", "codex_langfuse_notify.py")),
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
async function latestVersion(packageName, registry = "https://registry.npmjs.org") {
|
|
73
|
-
const base = registry.replace(/\/+$/, "");
|
|
74
|
-
const response = await fetch(`${base}/${packageName}`, { headers: { accept: "application/json" } });
|
|
75
|
-
if (!response.ok) throw new Error(`npm registry ${response.status} ${response.statusText}`);
|
|
76
|
-
return extractVersionFromNpmMetadata(await response.json());
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function targetsToVerify(targetArg, installed, runtimeTargets) {
|
|
80
|
-
const requested = normalizeVerifyTargets(targetArg);
|
|
81
|
-
if (String(targetArg || "all").trim().toLowerCase() !== "all") return { targets: requested, skipped: [] };
|
|
82
|
-
|
|
83
|
-
const targets = requested.filter((target) => installed[target] || runtimeTargets[target]);
|
|
84
|
-
const skipped = requested
|
|
85
|
-
.filter((target) => !installed[target] && !runtimeTargets[target])
|
|
86
|
-
.map((target) => ({ target, reason: "not_detected" }));
|
|
87
|
-
return { targets, skipped };
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function printResult(result) {
|
|
91
|
-
const label = targetLabel(result.target);
|
|
92
|
-
if (result.status === "current") {
|
|
93
|
-
console.log(paint(`[OK] ${label} Langfuse runtime is current: ${result.packageName}@${result.installedVersion}`, t.bold, t.green));
|
|
94
|
-
if (result.updatedAt) console.log(`[INFO] ${label} updatedAt: ${result.updatedAt}`);
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
if (result.status === "outdated") {
|
|
98
|
-
console.log(
|
|
99
|
-
paint(
|
|
100
|
-
`[FAIL] ${label} Langfuse runtime is outdated: installed ${result.packageName}@${result.installedVersion}, latest ${result.packageName}@${result.latestVersion}`,
|
|
101
|
-
t.bold,
|
|
102
|
-
t.red,
|
|
103
|
-
),
|
|
104
|
-
);
|
|
105
|
-
const fix =
|
|
106
|
-
result.packageName === "oh-aicoding-tool"
|
|
107
|
-
? `npx oh-aicoding-tool@latest langfuse update ${result.target}`
|
|
108
|
-
: `npx oh-langfuse@latest update ${result.target}`;
|
|
109
|
-
console.log(paint(`[FIX] Run: ${fix}`, t.bold, t.cyan));
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
console.log(paint(`[FAIL] ${label} Langfuse runtime record is missing.`, t.bold, t.red));
|
|
113
|
-
console.log(`[INFO] Runtime state: ${runtimeStatePath()}`);
|
|
114
|
-
console.log(paint(`[FIX] Run: npx oh-langfuse@latest update ${result.target}`, t.bold, t.cyan));
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
async function main() {
|
|
118
|
-
const args = parseArgs(process.argv.slice(2));
|
|
119
|
-
const targetArg = args._[0] || "all";
|
|
120
|
-
const state = readRuntimeState();
|
|
121
|
-
const runtimeTargets = state.targets || {};
|
|
122
|
-
const installed = detectInstalledTargets();
|
|
123
|
-
const { targets, skipped } = targetsToVerify(targetArg, installed, runtimeTargets);
|
|
124
|
-
|
|
125
|
-
for (const item of skipped) {
|
|
126
|
-
console.log(paint(`[SKIP] ${targetLabel(item.target)} not installed`, t.bold, t.gold));
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if (!targets.length) {
|
|
130
|
-
console.log(paint("[FAIL] No installed Langfuse runtime targets were found.", t.bold, t.red));
|
|
131
|
-
console.log(paint("[FIX] Run setup or update first: npx oh-langfuse@latest update all", t.bold, t.cyan));
|
|
132
|
-
return 2;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
let latestVersions = {};
|
|
136
|
-
try {
|
|
137
|
-
const packageNames = new Set(["oh-langfuse"]);
|
|
138
|
-
for (const target of targets) {
|
|
139
|
-
const packageName = String(runtimeTargets[target]?.packageName || "oh-langfuse").trim() || "oh-langfuse";
|
|
140
|
-
packageNames.add(packageName);
|
|
141
|
-
}
|
|
142
|
-
for (const packageName of packageNames) {
|
|
143
|
-
latestVersions[packageName] = await latestVersion(packageName, args.npmRegistry);
|
|
144
|
-
}
|
|
145
|
-
} catch (error) {
|
|
146
|
-
console.error(paint(`[FAIL] Could not query npm latest version: ${error.message}`, t.bold, t.red));
|
|
147
|
-
return 3;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const results = buildVerifyUpdateResults({ targets, runtimeTargets, latestVersions });
|
|
151
|
-
for (const result of results) printResult(result);
|
|
152
|
-
return verifyUpdateExitCode(results);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
main()
|
|
156
|
-
.then((code) => {
|
|
157
|
-
process.exit(code);
|
|
158
|
-
})
|
|
159
|
-
.catch((error) => {
|
|
160
|
-
console.error(paint(`[FAIL] ${error?.message || String(error)}`, t.bold, t.red));
|
|
161
|
-
process.exit(3);
|
|
162
|
-
});
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { extractVersionFromNpmMetadata } from "./update-utils.mjs";
|
|
5
|
+
import { readRuntimeState, runtimeStatePath } from "./runtime-state-utils.mjs";
|
|
6
|
+
import {
|
|
7
|
+
buildVerifyUpdateResults,
|
|
8
|
+
normalizeVerifyTargets,
|
|
9
|
+
targetLabel,
|
|
10
|
+
verifyUpdateExitCode,
|
|
11
|
+
} from "./verify-update-utils.mjs";
|
|
12
|
+
|
|
13
|
+
const colorEnabled = process.stdout.isTTY && process.env.NO_COLOR !== "1";
|
|
14
|
+
const ansi = (code) => (colorEnabled ? `\x1b[${code}m` : "");
|
|
15
|
+
const t = {
|
|
16
|
+
reset: ansi(0),
|
|
17
|
+
bold: ansi(1),
|
|
18
|
+
green: ansi("92"),
|
|
19
|
+
gold: ansi("93"),
|
|
20
|
+
red: ansi("91"),
|
|
21
|
+
cyan: ansi("96"),
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function paint(text, ...styles) {
|
|
25
|
+
if (!colorEnabled) return text;
|
|
26
|
+
return `${styles.join("")}${text}${t.reset}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function parseArgs(argv) {
|
|
30
|
+
const args = { _: [] };
|
|
31
|
+
for (const raw of argv) {
|
|
32
|
+
if (!raw.startsWith("--")) {
|
|
33
|
+
args._.push(raw);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
const eq = raw.indexOf("=");
|
|
37
|
+
if (eq === -1) args[raw.slice(2)] = true;
|
|
38
|
+
else args[raw.slice(2, eq)] = raw.slice(eq + 1);
|
|
39
|
+
}
|
|
40
|
+
return args;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function stripBom(s) {
|
|
44
|
+
return typeof s === "string" && s.charCodeAt(0) === 0xfeff ? s.slice(1) : s;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function readJsonIfExists(p) {
|
|
48
|
+
try {
|
|
49
|
+
if (!fs.existsSync(p)) return null;
|
|
50
|
+
const text = stripBom(fs.readFileSync(p, "utf8"));
|
|
51
|
+
if (!text.trim()) return null;
|
|
52
|
+
return JSON.parse(text);
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function codexHome(home = os.homedir()) {
|
|
59
|
+
return process.env.CODEX_HOME || path.join(home, ".codex");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function detectInstalledTargets(home = os.homedir()) {
|
|
63
|
+
return {
|
|
64
|
+
claude: fs.existsSync(path.join(home, ".claude", "hooks", "langfuse_hook.py")),
|
|
65
|
+
opencode:
|
|
66
|
+
fs.existsSync(path.join(home, ".config", "opencode", "opencode.json")) ||
|
|
67
|
+
fs.existsSync(path.join(home, ".config", "opencode", "plugins", "opencode-plugin-langfuse")),
|
|
68
|
+
codex: fs.existsSync(path.join(codexHome(home), "hooks", "codex_langfuse_notify.py")),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function latestVersion(packageName, registry = "https://registry.npmjs.org") {
|
|
73
|
+
const base = registry.replace(/\/+$/, "");
|
|
74
|
+
const response = await fetch(`${base}/${packageName}`, { headers: { accept: "application/json" } });
|
|
75
|
+
if (!response.ok) throw new Error(`npm registry ${response.status} ${response.statusText}`);
|
|
76
|
+
return extractVersionFromNpmMetadata(await response.json());
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function targetsToVerify(targetArg, installed, runtimeTargets) {
|
|
80
|
+
const requested = normalizeVerifyTargets(targetArg);
|
|
81
|
+
if (String(targetArg || "all").trim().toLowerCase() !== "all") return { targets: requested, skipped: [] };
|
|
82
|
+
|
|
83
|
+
const targets = requested.filter((target) => installed[target] || runtimeTargets[target]);
|
|
84
|
+
const skipped = requested
|
|
85
|
+
.filter((target) => !installed[target] && !runtimeTargets[target])
|
|
86
|
+
.map((target) => ({ target, reason: "not_detected" }));
|
|
87
|
+
return { targets, skipped };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function printResult(result) {
|
|
91
|
+
const label = targetLabel(result.target);
|
|
92
|
+
if (result.status === "current") {
|
|
93
|
+
console.log(paint(`[OK] ${label} Langfuse runtime is current: ${result.packageName}@${result.installedVersion}`, t.bold, t.green));
|
|
94
|
+
if (result.updatedAt) console.log(`[INFO] ${label} updatedAt: ${result.updatedAt}`);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (result.status === "outdated") {
|
|
98
|
+
console.log(
|
|
99
|
+
paint(
|
|
100
|
+
`[FAIL] ${label} Langfuse runtime is outdated: installed ${result.packageName}@${result.installedVersion}, latest ${result.packageName}@${result.latestVersion}`,
|
|
101
|
+
t.bold,
|
|
102
|
+
t.red,
|
|
103
|
+
),
|
|
104
|
+
);
|
|
105
|
+
const fix =
|
|
106
|
+
result.packageName === "oh-aicoding-tool"
|
|
107
|
+
? `npx oh-aicoding-tool@latest langfuse update ${result.target}`
|
|
108
|
+
: `npx oh-langfuse@latest update ${result.target}`;
|
|
109
|
+
console.log(paint(`[FIX] Run: ${fix}`, t.bold, t.cyan));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
console.log(paint(`[FAIL] ${label} Langfuse runtime record is missing.`, t.bold, t.red));
|
|
113
|
+
console.log(`[INFO] Runtime state: ${runtimeStatePath()}`);
|
|
114
|
+
console.log(paint(`[FIX] Run: npx oh-langfuse@latest update ${result.target}`, t.bold, t.cyan));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function main() {
|
|
118
|
+
const args = parseArgs(process.argv.slice(2));
|
|
119
|
+
const targetArg = args._[0] || "all";
|
|
120
|
+
const state = readRuntimeState();
|
|
121
|
+
const runtimeTargets = state.targets || {};
|
|
122
|
+
const installed = detectInstalledTargets();
|
|
123
|
+
const { targets, skipped } = targetsToVerify(targetArg, installed, runtimeTargets);
|
|
124
|
+
|
|
125
|
+
for (const item of skipped) {
|
|
126
|
+
console.log(paint(`[SKIP] ${targetLabel(item.target)} not installed`, t.bold, t.gold));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!targets.length) {
|
|
130
|
+
console.log(paint("[FAIL] No installed Langfuse runtime targets were found.", t.bold, t.red));
|
|
131
|
+
console.log(paint("[FIX] Run setup or update first: npx oh-langfuse@latest update all", t.bold, t.cyan));
|
|
132
|
+
return 2;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
let latestVersions = {};
|
|
136
|
+
try {
|
|
137
|
+
const packageNames = new Set(["oh-langfuse"]);
|
|
138
|
+
for (const target of targets) {
|
|
139
|
+
const packageName = String(runtimeTargets[target]?.packageName || "oh-langfuse").trim() || "oh-langfuse";
|
|
140
|
+
packageNames.add(packageName);
|
|
141
|
+
}
|
|
142
|
+
for (const packageName of packageNames) {
|
|
143
|
+
latestVersions[packageName] = await latestVersion(packageName, args.npmRegistry);
|
|
144
|
+
}
|
|
145
|
+
} catch (error) {
|
|
146
|
+
console.error(paint(`[FAIL] Could not query npm latest version: ${error.message}`, t.bold, t.red));
|
|
147
|
+
return 3;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const results = buildVerifyUpdateResults({ targets, runtimeTargets, latestVersions });
|
|
151
|
+
for (const result of results) printResult(result);
|
|
152
|
+
return verifyUpdateExitCode(results);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
main()
|
|
156
|
+
.then((code) => {
|
|
157
|
+
process.exit(code);
|
|
158
|
+
})
|
|
159
|
+
.catch((error) => {
|
|
160
|
+
console.error(paint(`[FAIL] ${error?.message || String(error)}`, t.bold, t.red));
|
|
161
|
+
process.exit(3);
|
|
162
|
+
});
|