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