oh-langfuse 0.1.78 → 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.
- package/SELF_VERIFY.md +25 -25
- package/bin/cli.js +23 -14
- package/codex_langfuse_notify.py +162 -2
- package/langfuse_hook.py +165 -2
- package/package.json +2 -1
- package/scripts/auto-update-runtime.mjs +14 -14
- package/scripts/cli-detection-utils.mjs +114 -114
- package/scripts/codex-langfuse-check.mjs +65 -65
- package/scripts/codex-langfuse-setup.mjs +127 -116
- package/scripts/json-utils.mjs +99 -99
- package/scripts/langfuse-check.mjs +50 -50
- package/scripts/langfuse-setup.mjs +139 -128
- package/scripts/opencode-langfuse-check.mjs +208 -199
- package/scripts/opencode-langfuse-run.mjs +12 -1
- package/scripts/opencode-langfuse-setup.mjs +88 -6
- package/scripts/opencode-skills-discover.mjs +125 -0
- package/scripts/real-self-verify.mjs +97 -85
- package/scripts/resolve-opencode-cli.mjs +53 -53
- package/scripts/update-langfuse-runtime.mjs +39 -22
- package/setup-langfuse.bat +19 -19
- package/setup-langfuse.sh +19 -19
|
@@ -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,7 +9,11 @@ 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 = "
|
|
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
|
+
]);
|
|
13
17
|
const DEFAULT_LANGFUSE_PUBLIC_KEY = "pk-lf-da0c90a7-6e93-4eb7-bb86-c1047c8d187d";
|
|
14
18
|
const DEFAULT_LANGFUSE_SECRET_KEY = "sk-lf-0269b85d-bfdc-442c-bfa3-e737954e3315";
|
|
15
19
|
const OPENCODE_NATIVE_AGENT_TURN_NAMES = new Set(["session.llm:ai.streamText.doStream"]);
|
|
@@ -165,10 +169,18 @@ function localCodexCliCandidates() {
|
|
|
165
169
|
return [...candidates, "codex.cmd", "codex.exe", "codex"].filter(Boolean);
|
|
166
170
|
}
|
|
167
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
|
+
|
|
168
177
|
function configFromArgs(args) {
|
|
169
178
|
return {
|
|
170
|
-
baseUrl:
|
|
171
|
-
|
|
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
|
+
),
|
|
172
184
|
publicKey: String(args.publicKey || process.env.LANGFUSE_PUBLIC_KEY || DEFAULT_LANGFUSE_PUBLIC_KEY),
|
|
173
185
|
secretKey: String(args.secretKey || process.env.LANGFUSE_SECRET_KEY || DEFAULT_LANGFUSE_SECRET_KEY),
|
|
174
186
|
userId: String(args.userId || args.userid || process.env.LANGFUSE_USER_ID || process.env.CC_USER_ID || "").trim(),
|
|
@@ -258,7 +270,7 @@ function basicAuth(publicKey, secretKey) {
|
|
|
258
270
|
return `Basic ${Buffer.from(`${publicKey}:${secretKey}`, "utf8").toString("base64")}`;
|
|
259
271
|
}
|
|
260
272
|
|
|
261
|
-
async function langfuseGet(config, pathname, params = {}) {
|
|
273
|
+
async function langfuseGet(config, pathname, params = {}) {
|
|
262
274
|
const url = new URL(`${apiBase(config.baseUrl)}${pathname}`);
|
|
263
275
|
for (const [key, value] of Object.entries(params)) {
|
|
264
276
|
if (value !== undefined && value !== null && String(value) !== "") url.searchParams.set(key, String(value));
|
|
@@ -278,22 +290,22 @@ async function langfuseGet(config, pathname, params = {}) {
|
|
|
278
290
|
error.status = response.status;
|
|
279
291
|
throw error;
|
|
280
292
|
}
|
|
281
|
-
return await response.json();
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
function isLangfuseQueryShapeError(error) {
|
|
285
|
-
if (!error) return false;
|
|
286
|
-
return error.status === 400 || error.status === 422;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
async function langfuseGetLenient(config, pathname, params = {}) {
|
|
290
|
-
try {
|
|
291
|
-
return await langfuseGet(config, pathname, params);
|
|
292
|
-
} catch (error) {
|
|
293
|
-
if (!isLangfuseQueryShapeError(error)) throw error;
|
|
294
|
-
const fallback = {};
|
|
295
|
-
for (const key of ["limit", "page", "userId", "fields"]) {
|
|
296
|
-
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];
|
|
297
309
|
}
|
|
298
310
|
return await langfuseGet(config, pathname, fallback);
|
|
299
311
|
}
|
|
@@ -359,29 +371,29 @@ function metadataValue(item, key) {
|
|
|
359
371
|
return undefined;
|
|
360
372
|
}
|
|
361
373
|
|
|
362
|
-
function directMetadataValue(item, key) {
|
|
363
|
-
const metadata = item?.metadata || {};
|
|
364
|
-
return metadata[key];
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
function observationIOValue(item, key) {
|
|
368
|
-
const metadata = item?.metadata || {};
|
|
369
|
-
const attrs = metadata.attributes || {};
|
|
370
|
-
for (const value of [
|
|
371
|
-
item?.[key],
|
|
372
|
-
metadata[key],
|
|
373
|
-
attrs[`langfuse.observation.${key}`],
|
|
374
|
-
attrs[`langfuse.trace.${key}`],
|
|
375
|
-
attrs[`${key}.value`],
|
|
376
|
-
]) {
|
|
377
|
-
if (value !== undefined && value !== null && String(value).trim() !== "") return value;
|
|
378
|
-
}
|
|
379
|
-
return undefined;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
function hasMetadataKey(item, key) {
|
|
383
|
-
return metadataValue(item, key) !== undefined;
|
|
384
|
-
}
|
|
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
|
+
}
|
|
385
397
|
|
|
386
398
|
function metricInteractionId(item, target) {
|
|
387
399
|
return target === "opencode"
|
|
@@ -412,18 +424,18 @@ function expectedAgentTurnName(target) {
|
|
|
412
424
|
async function observationsForTrace(config, traceId, since) {
|
|
413
425
|
if (!traceId) return [];
|
|
414
426
|
const params = { limit: 100, fields: "core,basic,usage", traceId };
|
|
415
|
-
try {
|
|
416
|
-
return dataArray(await langfuseGet(config, "/v2/observations", params));
|
|
417
|
-
} catch (error) {
|
|
418
|
-
if (!isLangfuseQueryShapeError(error) && error.status !== 404) throw error;
|
|
419
|
-
}
|
|
420
|
-
try {
|
|
421
|
-
return dataArray(await langfuseGet(config, "/observations", { limit: 100, traceId }));
|
|
422
|
-
} catch (error) {
|
|
423
|
-
if (!isLangfuseQueryShapeError(error)) throw error;
|
|
424
|
-
}
|
|
425
|
-
const fallback = dataArray(await langfuseGetLenient(config, "/observations", { limit: 100, fromTimestamp: since.toISOString() }));
|
|
426
|
-
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);
|
|
427
439
|
}
|
|
428
440
|
|
|
429
441
|
function mergeMetricCandidates(items) {
|
|
@@ -444,12 +456,12 @@ async function recentMetricCandidates(config, since) {
|
|
|
444
456
|
const candidates = [];
|
|
445
457
|
for (const pathname of ["/traces"]) {
|
|
446
458
|
for (const params of [{ ...baseParams, userId: config.userId }, baseParams]) {
|
|
447
|
-
try {
|
|
448
|
-
candidates.push(...dataArray(await langfuseGetLenient(config, pathname, params)));
|
|
449
|
-
} catch (error) {
|
|
450
|
-
if (error.name === "AbortError" || error.status === 404 || isLangfuseQueryShapeError(error)) continue;
|
|
451
|
-
throw error;
|
|
452
|
-
}
|
|
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
|
+
}
|
|
453
465
|
}
|
|
454
466
|
}
|
|
455
467
|
return mergeMetricCandidates(candidates);
|
|
@@ -527,20 +539,20 @@ async function verifyMetricObservations(config, found, { since, target, marker =
|
|
|
527
539
|
throw new Error(`Metric verification failed for ${target}: Agent Turn is missing repo context ${key}.`);
|
|
528
540
|
}
|
|
529
541
|
}
|
|
530
|
-
if (target === "opencode") {
|
|
531
|
-
for (const key of ["interaction_id", "interaction_count", "token_metrics_available", "tool_call_count", "skill_use_count", "input_tokens", "output_tokens", "total_tokens"]) {
|
|
532
|
-
if (metadataValue(item, key) === undefined) {
|
|
533
|
-
throw new Error(`Metric verification failed for ${target}: effective metadata is missing ${key}.`);
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
if (item?.name === expectedName) {
|
|
537
|
-
for (const key of ["input", "output"]) {
|
|
538
|
-
if (observationIOValue(item, key) === undefined) {
|
|
539
|
-
throw new Error(`Metric verification failed for ${target}: ${expectedName} is missing ${key}.`);
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
}
|
|
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
|
+
}
|
|
544
556
|
const tokenAvailable = metadataValue(item, "token_metrics_available");
|
|
545
557
|
for (const tokenKey of ["input_tokens", "output_tokens", "total_tokens"]) {
|
|
546
558
|
const value = metadataValue(item, tokenKey);
|
|
@@ -569,12 +581,12 @@ async function findLangfuseMarker(config, marker, { since, target }) {
|
|
|
569
581
|
|
|
570
582
|
for (const params of traceQueries) {
|
|
571
583
|
let traces;
|
|
572
|
-
try {
|
|
573
|
-
traces = await langfuseGetLenient(config, "/traces", params);
|
|
574
|
-
} catch (error) {
|
|
575
|
-
if (error.status === 404 || isLangfuseQueryShapeError(error)) continue;
|
|
576
|
-
throw error;
|
|
577
|
-
}
|
|
584
|
+
try {
|
|
585
|
+
traces = await langfuseGetLenient(config, "/traces", params);
|
|
586
|
+
} catch (error) {
|
|
587
|
+
if (error.status === 404 || isLangfuseQueryShapeError(error)) continue;
|
|
588
|
+
throw error;
|
|
589
|
+
}
|
|
578
590
|
for (const trace of dataArray(traces)) {
|
|
579
591
|
if (containsMarker(trace, marker)) return { kind: "trace-list", target, id: idOf(trace), item: trace };
|
|
580
592
|
}
|
|
@@ -602,10 +614,10 @@ async function findLangfuseMarker(config, marker, { since, target }) {
|
|
|
602
614
|
for (const observation of dataArray(observations)) {
|
|
603
615
|
if (containsMarker(observation, marker)) return { kind: pathname, target, id: idOf(observation), item: observation };
|
|
604
616
|
}
|
|
605
|
-
} catch (error) {
|
|
606
|
-
if (error.status === 404 || isLangfuseQueryShapeError(error)) continue;
|
|
607
|
-
throw error;
|
|
608
|
-
}
|
|
617
|
+
} catch (error) {
|
|
618
|
+
if (error.status === 404 || isLangfuseQueryShapeError(error)) continue;
|
|
619
|
+
throw error;
|
|
620
|
+
}
|
|
609
621
|
}
|
|
610
622
|
|
|
611
623
|
return null;
|
|
@@ -1,71 +1,71 @@
|
|
|
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";
|
|
5
|
-
import { listSystemCliCandidates } from "./cli-detection-utils.mjs";
|
|
6
|
-
|
|
7
|
-
function isKnownBrokenWindowsOpencodeCandidate(candidate) {
|
|
8
|
-
if (process.platform !== "win32") return false;
|
|
9
|
-
const normalized = String(candidate || "").replace(/\//g, "\\").toLowerCase();
|
|
10
|
-
return normalized.endsWith("\\node_modules\\opencode-ai\\bin\\opencode.exe");
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function usableCandidate(candidate) {
|
|
14
|
-
return candidate && !isKnownBrokenWindowsOpencodeCandidate(candidate) && fs.existsSync(candidate);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
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";
|
|
6
|
+
|
|
7
|
+
function isKnownBrokenWindowsOpencodeCandidate(candidate) {
|
|
8
|
+
if (process.platform !== "win32") return false;
|
|
9
|
+
const normalized = String(candidate || "").replace(/\//g, "\\").toLowerCase();
|
|
10
|
+
return normalized.endsWith("\\node_modules\\opencode-ai\\bin\\opencode.exe");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function usableCandidate(candidate) {
|
|
14
|
+
return candidate && !isKnownBrokenWindowsOpencodeCandidate(candidate) && fs.existsSync(candidate);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
18
|
* OpenCode CLI 的官方安装脚本默认会把二进制放到 ~/.opencode/bin;
|
|
19
19
|
* npm 全局安装则可能出现在 %AppData%\npm\opencode.cmd;PATH 也可能只认识 `opencode`。
|
|
20
20
|
* @param {string|undefined} preferred --cmd=xxx 或可执行文件路径
|
|
21
21
|
* @returns {string|null} 可执行的绝对路径,或仅在 PATH 中解析到则用 `opencode`
|
|
22
22
|
*/
|
|
23
23
|
export function resolveOpencodeCli(preferred) {
|
|
24
|
-
if (typeof preferred === "string" && preferred.trim()) {
|
|
25
|
-
const p = preferred.trim();
|
|
26
|
-
if (usableCandidate(p)) return path.resolve(p);
|
|
27
|
-
}
|
|
24
|
+
if (typeof preferred === "string" && preferred.trim()) {
|
|
25
|
+
const p = preferred.trim();
|
|
26
|
+
if (usableCandidate(p)) return path.resolve(p);
|
|
27
|
+
}
|
|
28
28
|
|
|
29
29
|
const home = os.homedir();
|
|
30
30
|
|
|
31
31
|
/** @type {string[]} */
|
|
32
|
-
const candidates = [];
|
|
33
|
-
if (process.platform === "win32") {
|
|
34
|
-
candidates.push(path.join(home, ".opencode", "bin", "opencode.exe"));
|
|
35
|
-
if (process.env.APPDATA) {
|
|
36
|
-
candidates.push(path.join(process.env.APPDATA, "npm", "node_modules", "opencode-ai", "node_modules", "opencode-windows-x64", "bin", "opencode.exe"));
|
|
37
|
-
candidates.push(path.join(process.env.APPDATA, "npm", "node_modules", "opencode-ai", "node_modules", "opencode-windows-x64-baseline", "bin", "opencode.exe"));
|
|
38
|
-
candidates.push(path.join(process.env.APPDATA, "npm", "node_modules", "opencode-windows-x64", "bin", "opencode.exe"));
|
|
39
|
-
candidates.push(path.join(process.env.APPDATA, "npm", "node_modules", "opencode-windows-x64-baseline", "bin", "opencode.exe"));
|
|
40
|
-
}
|
|
41
|
-
if (process.env.LOCALAPPDATA) {
|
|
42
|
-
candidates.push(path.join(process.env.LOCALAPPDATA, "Programs", "opencode", "opencode.exe"));
|
|
43
|
-
}
|
|
44
|
-
candidates.push(path.join(home, ".config", "opencode", "bin", "opencode.exe"));
|
|
45
|
-
candidates.push(path.join(home, ".config", "opencode", "bin", "opencode.cmd"));
|
|
46
|
-
candidates.push(path.join(home, ".opencode", "bin", "opencode.cmd"));
|
|
47
|
-
candidates.push(path.join(home, "bin", "opencode.exe"));
|
|
48
|
-
if (process.env.APPDATA) {
|
|
49
|
-
candidates.push(path.join(process.env.APPDATA, "npm", "opencode.cmd"));
|
|
50
|
-
candidates.push(path.join(process.env.APPDATA, "npm", "opencode"));
|
|
51
|
-
}
|
|
52
|
-
candidates.push(path.join(home, "scoop", "shims", "opencode.exe"));
|
|
53
|
-
} else {
|
|
54
|
-
candidates.push(path.join(home, ".opencode", "bin", "opencode"));
|
|
55
|
-
candidates.push(path.join(home, ".local", "bin", "opencode"));
|
|
56
|
-
candidates.push(...listSystemCliCandidates("opencode"));
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
for (const c of candidates) {
|
|
60
|
-
if (usableCandidate(c)) return c;
|
|
61
|
-
}
|
|
32
|
+
const candidates = [];
|
|
33
|
+
if (process.platform === "win32") {
|
|
34
|
+
candidates.push(path.join(home, ".opencode", "bin", "opencode.exe"));
|
|
35
|
+
if (process.env.APPDATA) {
|
|
36
|
+
candidates.push(path.join(process.env.APPDATA, "npm", "node_modules", "opencode-ai", "node_modules", "opencode-windows-x64", "bin", "opencode.exe"));
|
|
37
|
+
candidates.push(path.join(process.env.APPDATA, "npm", "node_modules", "opencode-ai", "node_modules", "opencode-windows-x64-baseline", "bin", "opencode.exe"));
|
|
38
|
+
candidates.push(path.join(process.env.APPDATA, "npm", "node_modules", "opencode-windows-x64", "bin", "opencode.exe"));
|
|
39
|
+
candidates.push(path.join(process.env.APPDATA, "npm", "node_modules", "opencode-windows-x64-baseline", "bin", "opencode.exe"));
|
|
40
|
+
}
|
|
41
|
+
if (process.env.LOCALAPPDATA) {
|
|
42
|
+
candidates.push(path.join(process.env.LOCALAPPDATA, "Programs", "opencode", "opencode.exe"));
|
|
43
|
+
}
|
|
44
|
+
candidates.push(path.join(home, ".config", "opencode", "bin", "opencode.exe"));
|
|
45
|
+
candidates.push(path.join(home, ".config", "opencode", "bin", "opencode.cmd"));
|
|
46
|
+
candidates.push(path.join(home, ".opencode", "bin", "opencode.cmd"));
|
|
47
|
+
candidates.push(path.join(home, "bin", "opencode.exe"));
|
|
48
|
+
if (process.env.APPDATA) {
|
|
49
|
+
candidates.push(path.join(process.env.APPDATA, "npm", "opencode.cmd"));
|
|
50
|
+
candidates.push(path.join(process.env.APPDATA, "npm", "opencode"));
|
|
51
|
+
}
|
|
52
|
+
candidates.push(path.join(home, "scoop", "shims", "opencode.exe"));
|
|
53
|
+
} else {
|
|
54
|
+
candidates.push(path.join(home, ".opencode", "bin", "opencode"));
|
|
55
|
+
candidates.push(path.join(home, ".local", "bin", "opencode"));
|
|
56
|
+
candidates.push(...listSystemCliCandidates("opencode"));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
for (const c of candidates) {
|
|
60
|
+
if (usableCandidate(c)) return c;
|
|
61
|
+
}
|
|
62
62
|
|
|
63
63
|
if (process.platform === "win32") {
|
|
64
64
|
const r = spawnSync("where.exe", ["opencode"], { encoding: "utf8", windowsHide: true });
|
|
65
65
|
if (r.status === 0 && r.stdout) {
|
|
66
|
-
const line = r.stdout.trim().split(/\r?\n/)[0]?.trim();
|
|
67
|
-
if (usableCandidate(line)) return line;
|
|
68
|
-
}
|
|
66
|
+
const line = r.stdout.trim().split(/\r?\n/)[0]?.trim();
|
|
67
|
+
if (usableCandidate(line)) return line;
|
|
68
|
+
}
|
|
69
69
|
} else {
|
|
70
70
|
const r = spawnSync("which", ["opencode"], { encoding: "utf8" });
|
|
71
71
|
if (r.status === 0 && r.stdout) {
|
|
@@ -1,16 +1,20 @@
|
|
|
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 { hasSystemCli } from "./cli-detection-utils.mjs";
|
|
7
|
-
import { buildUpdatePlan, extractVersionFromNpmMetadata } from "./update-utils.mjs";
|
|
8
|
-
import { writeRuntimeInstallRecord } from "./runtime-state-utils.mjs";
|
|
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";
|
|
9
9
|
|
|
10
10
|
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
11
11
|
const packageJson = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8"));
|
|
12
12
|
|
|
13
|
-
const DEFAULT_LANGFUSE_BASE_URL = "
|
|
13
|
+
const DEFAULT_LANGFUSE_BASE_URL = "https://metrics.openharmonyinsight.cn";
|
|
14
|
+
const LEGACY_LANGFUSE_BASE_URLS = new Set([
|
|
15
|
+
"http://120.46.221.227:3000",
|
|
16
|
+
"https://120.46.221.227:3000",
|
|
17
|
+
]);
|
|
14
18
|
const DEFAULT_LANGFUSE_PUBLIC_KEY = "pk-lf-da0c90a7-6e93-4eb7-bb86-c1047c8d187d";
|
|
15
19
|
const DEFAULT_LANGFUSE_SECRET_KEY = "sk-lf-0269b85d-bfdc-442c-bfa3-e737954e3315";
|
|
16
20
|
|
|
@@ -60,21 +64,21 @@ function readJsonIfExists(p) {
|
|
|
60
64
|
}
|
|
61
65
|
}
|
|
62
66
|
|
|
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
|
-
}
|
|
67
|
+
function detectInstalledTargets(home = os.homedir()) {
|
|
68
|
+
const codexHome = process.env.CODEX_HOME || path.join(home, ".codex");
|
|
69
|
+
return {
|
|
70
|
+
claude:
|
|
71
|
+
fs.existsSync(path.join(home, ".claude", "hooks", "langfuse_hook.py")) ||
|
|
72
|
+
hasSystemCli("claude"),
|
|
73
|
+
opencode:
|
|
74
|
+
fs.existsSync(path.join(home, ".config", "opencode", "opencode.json")) ||
|
|
75
|
+
fs.existsSync(path.join(home, ".config", "opencode", "plugins", "opencode-plugin-langfuse")) ||
|
|
76
|
+
hasSystemCli("opencode"),
|
|
77
|
+
codex:
|
|
78
|
+
fs.existsSync(path.join(codexHome, "hooks", "codex_langfuse_notify.py")) ||
|
|
79
|
+
hasSystemCli("codex"),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
78
82
|
|
|
79
83
|
function claudeConfig(home) {
|
|
80
84
|
const settings = readJsonIfExists(path.join(home, ".claude", "settings.json")) || {};
|
|
@@ -108,11 +112,24 @@ function opencodeConfig(home) {
|
|
|
108
112
|
};
|
|
109
113
|
}
|
|
110
114
|
|
|
115
|
+
function normalizeLegacyBaseUrl(baseUrl) {
|
|
116
|
+
const value = String(baseUrl || "").trim().replace(/\/+$/, "");
|
|
117
|
+
return LEGACY_LANGFUSE_BASE_URLS.has(value) ? DEFAULT_LANGFUSE_BASE_URL : baseUrl;
|
|
118
|
+
}
|
|
119
|
+
|
|
111
120
|
function mergedConfig(target, args) {
|
|
112
121
|
const home = os.homedir();
|
|
113
122
|
const existing = target === "claude" ? claudeConfig(home) : target === "codex" ? codexConfig(home) : opencodeConfig(home);
|
|
123
|
+
const baseUrl = normalizeLegacyBaseUrl(
|
|
124
|
+
args.langfuseBaseUrl ||
|
|
125
|
+
args.langfuseHost ||
|
|
126
|
+
args.host ||
|
|
127
|
+
existing.baseUrl ||
|
|
128
|
+
process.env.LANGFUSE_BASEURL ||
|
|
129
|
+
DEFAULT_LANGFUSE_BASE_URL
|
|
130
|
+
);
|
|
114
131
|
const config = {
|
|
115
|
-
baseUrl
|
|
132
|
+
baseUrl,
|
|
116
133
|
publicKey: args.publicKey || existing.publicKey || process.env.LANGFUSE_PUBLIC_KEY || DEFAULT_LANGFUSE_PUBLIC_KEY,
|
|
117
134
|
secretKey: args.secretKey || existing.secretKey || process.env.LANGFUSE_SECRET_KEY || DEFAULT_LANGFUSE_SECRET_KEY,
|
|
118
135
|
userId: args.userId || args.userid || existing.userId || process.env.LANGFUSE_USER_ID || process.env.CC_USER_ID || "",
|
package/setup-langfuse.bat
CHANGED
|
@@ -57,9 +57,9 @@ echo.
|
|
|
57
57
|
call npm run claude:setup -- --userId=%USER_ID% --pyPath=%PY_PATH%
|
|
58
58
|
if errorlevel 1 exit /b 1
|
|
59
59
|
|
|
60
|
-
echo.
|
|
61
|
-
echo Check:
|
|
62
|
-
call npx oh-langfuse@latest check claude
|
|
60
|
+
echo.
|
|
61
|
+
echo Check:
|
|
62
|
+
call npx oh-langfuse@latest check claude
|
|
63
63
|
exit /b %errorlevel%
|
|
64
64
|
|
|
65
65
|
:OPENCODE
|
|
@@ -72,11 +72,11 @@ echo - NO auto-launch: open opencode in a NEW terminal afterwards
|
|
|
72
72
|
echo.
|
|
73
73
|
|
|
74
74
|
set "OC_USER_ID="
|
|
75
|
-
set /p OC_USER_ID=Enter userId (required) ^>
|
|
76
|
-
if "%OC_USER_ID%"=="" (
|
|
77
|
-
echo [ERROR] userId is required.
|
|
78
|
-
exit /b 1
|
|
79
|
-
)
|
|
75
|
+
set /p OC_USER_ID=Enter userId (required) ^>
|
|
76
|
+
if "%OC_USER_ID%"=="" (
|
|
77
|
+
echo [ERROR] userId is required.
|
|
78
|
+
exit /b 1
|
|
79
|
+
)
|
|
80
80
|
|
|
81
81
|
echo.
|
|
82
82
|
echo Write LANGFUSE_* to Windows user env vars (HKCU)?
|
|
@@ -98,7 +98,7 @@ set "OC_CMD="
|
|
|
98
98
|
set /p OC_CMD=OpenCode CLI path (optional; press Enter to auto-detect) ^>
|
|
99
99
|
|
|
100
100
|
set "SETUP_ARGS="
|
|
101
|
-
set "SETUP_ARGS=%SETUP_ARGS% --userId=%OC_USER_ID%"
|
|
101
|
+
set "SETUP_ARGS=%SETUP_ARGS% --userId=%OC_USER_ID%"
|
|
102
102
|
if /i "%OC_SET_ENV%"=="n" set "SETUP_ARGS=%SETUP_ARGS% --no-set-env"
|
|
103
103
|
if /i "%OC_SET_ENV%"=="no" set "SETUP_ARGS=%SETUP_ARGS% --no-set-env"
|
|
104
104
|
if /i "%OC_SKIP_INSTALL%"=="y" set "SETUP_ARGS=%SETUP_ARGS% --skip-plugin-install"
|
|
@@ -120,13 +120,13 @@ if errorlevel 1 goto :OPENCODE_FAIL
|
|
|
120
120
|
echo.
|
|
121
121
|
echo ============================================
|
|
122
122
|
echo OpenCode setup finished.
|
|
123
|
-
echo Next:
|
|
124
|
-
echo 1) Open a NEW terminal (HKCU env vars take effect).
|
|
125
|
-
echo 2) Run: opencode
|
|
126
|
-
echo Optional launcher:
|
|
127
|
-
echo %USERPROFILE%\.config\opencode\launch-opencode-langfuse.cmd
|
|
128
|
-
echo 3) Check: npx oh-langfuse@latest check opencode
|
|
129
|
-
echo ============================================
|
|
123
|
+
echo Next:
|
|
124
|
+
echo 1) Open a NEW terminal (HKCU env vars take effect).
|
|
125
|
+
echo 2) Run: opencode
|
|
126
|
+
echo Optional launcher:
|
|
127
|
+
echo %USERPROFILE%\.config\opencode\launch-opencode-langfuse.cmd
|
|
128
|
+
echo 3) Check: npx oh-langfuse@latest check opencode
|
|
129
|
+
echo ============================================
|
|
130
130
|
exit /b 0
|
|
131
131
|
|
|
132
132
|
:CODEX
|
|
@@ -150,9 +150,9 @@ echo.
|
|
|
150
150
|
call npm run codex:setup -- --userId=%CODEX_USER_ID% --pyPath=%CODEX_PY_PATH%
|
|
151
151
|
if errorlevel 1 exit /b 1
|
|
152
152
|
|
|
153
|
-
echo.
|
|
154
|
-
echo Check:
|
|
155
|
-
call npx oh-langfuse@latest check codex
|
|
153
|
+
echo.
|
|
154
|
+
echo Check:
|
|
155
|
+
call npx oh-langfuse@latest check codex
|
|
156
156
|
exit /b %errorlevel%
|
|
157
157
|
|
|
158
158
|
:NO_NPM
|