oh-aicoding-tool 0.1.2 → 0.1.5
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 +79 -80
- package/bin/cli.js +257 -384
- package/package.json +27 -55
- package/CODEX_LANGFUSE_PLAN.md +0 -62
- package/bin/langfuse-cli.js +0 -718
- package/codex_langfuse_notify.py +0 -591
- package/langfuse_hook.py +0 -603
- package/opencode-ohai-report/.claude/commands/report-ai-issue.md +0 -60
- package/opencode-ohai-report/.opencode/commands/report-ai-issue.md +0 -30
- package/opencode-ohai-report/.opencode/plugins/oh-ai-report.ts +0 -569
- package/opencode-ohai-report/README.md +0 -45
- package/opencode-ohai-report/bin/cli.js +0 -421
- package/opencode-ohai-report/docs/opencode-ai-issue-collection-architecture.md +0 -313
- package/opencode-ohai-report/docs/opencode-ai-issue-collection-best-practices.md +0 -476
- package/opencode-ohai-report/docs/opencode-ai-issue-collection-phase1-summary.md +0 -405
- package/opencode-ohai-report/examples/issue_output.json +0 -4
- package/opencode-ohai-report/package.json +0 -40
- package/opencode-ohai-report/scripts/claude_report_hook.py +0 -257
- package/opencode-ohai-report/scripts/create_issue.py +0 -34
- package/opencode-ohai-report/scripts/install-claude-plugin.ps1 +0 -254
- package/opencode-ohai-report/scripts/install-opencode-plugin.ps1 +0 -264
- package/opencode-ohai-report/scripts/install-opencode-plugin.sh +0 -218
- package/opencode-ohai-report/scripts/merge-claude-settings.py +0 -99
- package/opencode-ohai-report/tools/ohai-report/README.md +0 -151
- package/opencode-ohai-report/tools/ohai-report/examples/issue-input.json +0 -26
- package/opencode-ohai-report/tools/ohai-report/ohai_report/__init__.py +0 -5
- package/opencode-ohai-report/tools/ohai-report/ohai_report/__main__.py +0 -9
- package/opencode-ohai-report/tools/ohai-report/ohai_report/cli.py +0 -319
- package/opencode-ohai-report/tools/ohai-report/ohai_report/git_context.py +0 -32
- package/opencode-ohai-report/tools/ohai-report/ohai_report/gitcode_defaults.py +0 -14
- package/opencode-ohai-report/tools/ohai-report/ohai_report/issue_markdown.py +0 -313
- package/opencode-ohai-report/tools/ohai-report/ohai_report/metadata.py +0 -360
- package/opencode-ohai-report/tools/ohai-report/ohai_report/observability/__init__.py +0 -1
- package/opencode-ohai-report/tools/ohai-report/ohai_report/observability/langfuse.py +0 -38
- package/opencode-ohai-report/tools/ohai-report/ohai_report/payload.py +0 -64
- package/opencode-ohai-report/tools/ohai-report/ohai_report/schema.py +0 -80
- package/opencode-ohai-report/tools/ohai-report/ohai_report/sinks/__init__.py +0 -1
- package/opencode-ohai-report/tools/ohai-report/ohai_report/sinks/base.py +0 -15
- package/opencode-ohai-report/tools/ohai-report/ohai_report/sinks/gitcode.py +0 -405
- package/opencode-ohai-report/tools/ohai-report/ohai_report/sinks/local.py +0 -21
- package/opencode-ohai-report/tools/ohai-report/ohai_report/sinks/webhook.py +0 -354
- package/opencode-ohai-report/tools/ohai-report/ohai_report/webhook_defaults.py +0 -9
- package/opencode-ohai-report/tools/ohai-report/ohai_report/workspace.py +0 -61
- package/opencode-ohai-report/tools/ohai-report/ohai_report.py +0 -10
- package/opencode-ohai-report/tools/ohai-report/schemas/report_issue.schema.json +0 -166
- package/scripts/codex-langfuse-check.mjs +0 -101
- package/scripts/codex-langfuse-setup.mjs +0 -181
- package/scripts/langfuse-check.mjs +0 -90
- package/scripts/langfuse-setup.mjs +0 -278
- package/scripts/opencode-langfuse-check.mjs +0 -94
- package/scripts/opencode-langfuse-run.mjs +0 -96
- package/scripts/opencode-langfuse-setup.mjs +0 -478
- package/scripts/resolve-opencode-cli.mjs +0 -58
- package/setup-langfuse.bat +0 -163
- package/setup-langfuse.sh +0 -130
|
@@ -1,101 +0,0 @@
|
|
|
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
|
-
|
|
6
|
-
function stripBom(s) {
|
|
7
|
-
if (typeof s !== "string" || s.length === 0) return s;
|
|
8
|
-
return s.charCodeAt(0) === 0xfeff ? s.slice(1) : s;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function readJsonIfExists(p) {
|
|
12
|
-
if (!fs.existsSync(p)) return null;
|
|
13
|
-
const txt = stripBom(fs.readFileSync(p, "utf8"));
|
|
14
|
-
if (!txt.trim()) return null;
|
|
15
|
-
return JSON.parse(txt);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function findLatestSession(sessionsDir) {
|
|
19
|
-
if (!fs.existsSync(sessionsDir)) return null;
|
|
20
|
-
let latest = null;
|
|
21
|
-
let latestMtime = -1;
|
|
22
|
-
const stack = [sessionsDir];
|
|
23
|
-
while (stack.length) {
|
|
24
|
-
const dir = stack.pop();
|
|
25
|
-
for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
26
|
-
const p = path.join(dir, ent.name);
|
|
27
|
-
if (ent.isDirectory()) {
|
|
28
|
-
stack.push(p);
|
|
29
|
-
} else if (ent.isFile() && ent.name.endsWith(".jsonl")) {
|
|
30
|
-
const mtime = fs.statSync(p).mtimeMs;
|
|
31
|
-
if (mtime > latestMtime) {
|
|
32
|
-
latest = p;
|
|
33
|
-
latestMtime = mtime;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
return latest;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function commandOk(cmd, args) {
|
|
42
|
-
const r = spawnSync(cmd, args, { encoding: "utf8" });
|
|
43
|
-
return { ok: !r.error && r.status === 0, detail: (r.stdout || r.stderr || "").trim() };
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function venvPython(codexHome) {
|
|
47
|
-
return process.platform === "win32"
|
|
48
|
-
? path.join(codexHome, "langfuse-venv", "Scripts", "python.exe")
|
|
49
|
-
: path.join(codexHome, "langfuse-venv", "bin", "python");
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function configHasNotify(configText) {
|
|
53
|
-
return /^\s*notify\s*=.*codex_langfuse_notify\.py/m.test(configText);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function main() {
|
|
57
|
-
const codexHome = process.env.CODEX_HOME || path.join(os.homedir(), ".codex");
|
|
58
|
-
const configPath = path.join(codexHome, "config.toml");
|
|
59
|
-
const hookPath = path.join(codexHome, "hooks", "codex_langfuse_notify.py");
|
|
60
|
-
const langfuseConfigPath = path.join(codexHome, "langfuse", "config.json");
|
|
61
|
-
const sessionsDir = path.join(codexHome, "sessions");
|
|
62
|
-
const latestSession = findLatestSession(sessionsDir);
|
|
63
|
-
const langfuseConfig = readJsonIfExists(langfuseConfigPath);
|
|
64
|
-
const configText = fs.existsSync(configPath) ? stripBom(fs.readFileSync(configPath, "utf8")) : "";
|
|
65
|
-
const hookPython = venvPython(codexHome);
|
|
66
|
-
const python = commandOk(process.platform === "win32" ? "python" : "python3", ["--version"]);
|
|
67
|
-
const langfuseImport = commandOk(hookPython, ["-c", "import langfuse; print('langfuse ok')"]);
|
|
68
|
-
|
|
69
|
-
const results = [
|
|
70
|
-
{ item: "Codex home", ok: fs.existsSync(codexHome), detail: codexHome },
|
|
71
|
-
{ item: "config.toml", ok: fs.existsSync(configPath), detail: configPath },
|
|
72
|
-
{
|
|
73
|
-
item: "notify hook configured",
|
|
74
|
-
ok: configHasNotify(configText),
|
|
75
|
-
detail: configHasNotify(configText) ? "OK" : "missing notify entry for codex_langfuse_notify.py",
|
|
76
|
-
},
|
|
77
|
-
{ item: "hook script", ok: fs.existsSync(hookPath), detail: hookPath },
|
|
78
|
-
{ item: "Langfuse config", ok: !!langfuseConfig, detail: langfuseConfigPath },
|
|
79
|
-
{
|
|
80
|
-
item: "Langfuse keys",
|
|
81
|
-
ok: !!(langfuseConfig?.publicKey && langfuseConfig?.secretKey),
|
|
82
|
-
detail: langfuseConfig?.publicKey ? "configured" : "missing",
|
|
83
|
-
},
|
|
84
|
-
{ item: "sessions directory", ok: fs.existsSync(sessionsDir), detail: sessionsDir },
|
|
85
|
-
{ item: "latest session JSONL", ok: !!latestSession, detail: latestSession || "not found" },
|
|
86
|
-
{ item: "Python", ok: python.ok, detail: python.detail || "not found" },
|
|
87
|
-
{ item: "Langfuse venv Python", ok: fs.existsSync(hookPython), detail: hookPython },
|
|
88
|
-
{ item: "Python langfuse package", ok: langfuseImport.ok, detail: langfuseImport.detail || "not importable from venv" },
|
|
89
|
-
];
|
|
90
|
-
|
|
91
|
-
const w = Math.max(...results.map((r) => r.item.length)) + 2;
|
|
92
|
-
const pad = (s, n) => (s.length >= n ? s : s + " ".repeat(n - s.length));
|
|
93
|
-
for (const r of results) {
|
|
94
|
-
const status = r.ok ? "OK " : "BAD";
|
|
95
|
-
console.log(`${status} ${pad(r.item, w)} ${r.detail}`);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
process.exit(results.some((r) => !r.ok) ? 2 : 0);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
main();
|
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import fsp from "node:fs/promises";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import os from "node:os";
|
|
5
|
-
import { execFileSync } from "node:child_process";
|
|
6
|
-
import { fileURLToPath } from "node:url";
|
|
7
|
-
|
|
8
|
-
function parseArgs(argv) {
|
|
9
|
-
const args = {};
|
|
10
|
-
for (const raw of argv) {
|
|
11
|
-
if (!raw.startsWith("--")) continue;
|
|
12
|
-
const eq = raw.indexOf("=");
|
|
13
|
-
if (eq === -1) args[raw.slice(2)] = true;
|
|
14
|
-
else args[raw.slice(2, eq)] = raw.slice(eq + 1);
|
|
15
|
-
}
|
|
16
|
-
return args;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function ensureDir(p) {
|
|
20
|
-
fs.mkdirSync(p, { recursive: true });
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function stripBom(s) {
|
|
24
|
-
if (typeof s !== "string" || s.length === 0) return s;
|
|
25
|
-
return s.charCodeAt(0) === 0xfeff ? s.slice(1) : s;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function readJsonIfExists(p) {
|
|
29
|
-
if (!fs.existsSync(p)) return null;
|
|
30
|
-
const txt = stripBom(fs.readFileSync(p, "utf8"));
|
|
31
|
-
if (!txt.trim()) return null;
|
|
32
|
-
return JSON.parse(txt);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function writeJsonPretty(p, obj) {
|
|
36
|
-
fs.writeFileSync(p, JSON.stringify(obj, null, 2) + os.EOL, "utf8");
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function tomlString(s) {
|
|
40
|
-
return JSON.stringify(String(s));
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function tomlArray(items) {
|
|
44
|
-
return `[${items.map(tomlString).join(", ")}]`;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function pythonExecutableInVenv(venvDir) {
|
|
48
|
-
return process.platform === "win32"
|
|
49
|
-
? path.join(venvDir, "Scripts", "python.exe")
|
|
50
|
-
: path.join(venvDir, "bin", "python");
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function pipInstallArgs(args) {
|
|
54
|
-
const pipIndexUrl = args.pipIndexUrl || args.pipindexurl || process.env.CODE_TOOL_PIP_INDEX_URL || "";
|
|
55
|
-
const pipArgs = ["-m", "pip", "install", "-U", "langfuse"];
|
|
56
|
-
if (pipIndexUrl) pipArgs.push("-i", pipIndexUrl);
|
|
57
|
-
return pipArgs;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function createOrUpdateLangfuseVenv({ baseDir, args }) {
|
|
61
|
-
const venvDir = path.join(baseDir, "langfuse-venv");
|
|
62
|
-
const pythonCmd = process.platform === "win32" ? "python" : "python3";
|
|
63
|
-
const venvPython = pythonExecutableInVenv(venvDir);
|
|
64
|
-
|
|
65
|
-
if (!fs.existsSync(venvPython)) {
|
|
66
|
-
console.log(`Creating Python virtual environment: ${venvDir}`);
|
|
67
|
-
try {
|
|
68
|
-
execFileSync(pythonCmd, ["-m", "venv", venvDir], { stdio: "inherit" });
|
|
69
|
-
} catch (e) {
|
|
70
|
-
if (process.platform !== "win32") {
|
|
71
|
-
throw new Error("Failed to create Python venv. Please install python3-venv first, for example: sudo apt install python3-venv");
|
|
72
|
-
}
|
|
73
|
-
throw new Error("Failed to create Python venv. Please confirm Python venv support is available.");
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
console.log("Installing/updating Python package in venv: langfuse");
|
|
78
|
-
const installArgs = pipInstallArgs(args);
|
|
79
|
-
try {
|
|
80
|
-
execFileSync(venvPython, installArgs, { stdio: "inherit" });
|
|
81
|
-
} catch (e) {
|
|
82
|
-
throw new Error(`Failed to install langfuse in venv: ${venvPython} ${installArgs.join(" ")}`);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
return venvPython;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function updateCodexNotify(configPath, notifyCommand) {
|
|
89
|
-
let content = fs.existsSync(configPath) ? fs.readFileSync(configPath, "utf8") : "";
|
|
90
|
-
content = stripBom(content);
|
|
91
|
-
if (fs.existsSync(configPath)) {
|
|
92
|
-
const backupPath = `${configPath}.bak-${Date.now()}`;
|
|
93
|
-
fs.copyFileSync(configPath, backupPath);
|
|
94
|
-
console.log(`Backed up existing Codex config: ${backupPath}`);
|
|
95
|
-
}
|
|
96
|
-
const lines = content.split(/\r?\n/);
|
|
97
|
-
const notifyLine = `notify = ${tomlArray(notifyCommand)}`;
|
|
98
|
-
let inTopLevel = true;
|
|
99
|
-
let replaced = false;
|
|
100
|
-
const next = [];
|
|
101
|
-
|
|
102
|
-
for (const line of lines) {
|
|
103
|
-
if (/^\s*\[/.test(line)) inTopLevel = false;
|
|
104
|
-
if (inTopLevel && /^\s*notify\s*=/.test(line)) {
|
|
105
|
-
if (!replaced) next.push(notifyLine);
|
|
106
|
-
replaced = true;
|
|
107
|
-
continue;
|
|
108
|
-
}
|
|
109
|
-
next.push(line);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
if (!replaced) {
|
|
113
|
-
while (next.length && next[next.length - 1].trim() === "") next.pop();
|
|
114
|
-
next.push(notifyLine, "");
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
fs.writeFileSync(configPath, next.join(os.EOL), "utf8");
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
async function main() {
|
|
121
|
-
const args = parseArgs(process.argv.slice(2));
|
|
122
|
-
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
123
|
-
|
|
124
|
-
const publicKey =
|
|
125
|
-
args.publicKey || process.env.LANGFUSE_PUBLIC_KEY || "pk-lf-da0c90a7-6e93-4eb7-bb86-c1047c8d187d";
|
|
126
|
-
const secretKey =
|
|
127
|
-
args.secretKey || process.env.LANGFUSE_SECRET_KEY || "sk-lf-0269b85d-bfdc-442c-bfa3-e737954e3315";
|
|
128
|
-
const baseUrl =
|
|
129
|
-
args.langfuseBaseUrl ||
|
|
130
|
-
args.langfuseHost ||
|
|
131
|
-
args.host ||
|
|
132
|
-
process.env.LANGFUSE_BASEURL ||
|
|
133
|
-
process.env.LANGFUSE_HOST ||
|
|
134
|
-
"http://120.46.221.227:3000";
|
|
135
|
-
const userId = args.userId || args.userid || process.env.LANGFUSE_USER_ID || process.env.CC_USER_ID || "";
|
|
136
|
-
|
|
137
|
-
if (!publicKey || !secretKey) {
|
|
138
|
-
throw new Error("Missing Langfuse keys: provide --publicKey and --secretKey or set LANGFUSE_PUBLIC_KEY/LANGFUSE_SECRET_KEY.");
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const codexHome = process.env.CODEX_HOME || path.join(os.homedir(), ".codex");
|
|
142
|
-
const hooksDir = path.join(codexHome, "hooks");
|
|
143
|
-
const langfuseDir = path.join(codexHome, "langfuse");
|
|
144
|
-
const configPath = path.join(codexHome, "config.toml");
|
|
145
|
-
const sourceHook = args.pyPath || args.pypath || path.join(rootDir, "codex_langfuse_notify.py");
|
|
146
|
-
const destHook = path.join(hooksDir, "codex_langfuse_notify.py");
|
|
147
|
-
|
|
148
|
-
if (!fs.existsSync(sourceHook)) {
|
|
149
|
-
throw new Error(`Cannot find Codex Langfuse notify hook: ${sourceHook}`);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
ensureDir(hooksDir);
|
|
153
|
-
ensureDir(langfuseDir);
|
|
154
|
-
await fsp.copyFile(sourceHook, destHook);
|
|
155
|
-
|
|
156
|
-
const existingConfig = readJsonIfExists(path.join(langfuseDir, "config.json")) ?? {};
|
|
157
|
-
writeJsonPretty(path.join(langfuseDir, "config.json"), {
|
|
158
|
-
...existingConfig,
|
|
159
|
-
baseUrl,
|
|
160
|
-
publicKey,
|
|
161
|
-
secretKey,
|
|
162
|
-
userId,
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
const pythonCmd = process.platform === "win32" ? "python" : "python3";
|
|
166
|
-
const notifyPython = args["skip-pip-install"]
|
|
167
|
-
? pythonCmd
|
|
168
|
-
: createOrUpdateLangfuseVenv({ baseDir: codexHome, args });
|
|
169
|
-
updateCodexNotify(configPath, [notifyPython, destHook]);
|
|
170
|
-
|
|
171
|
-
console.log(`Updated Codex notify hook: ${configPath}`);
|
|
172
|
-
console.log(`Installed hook script: ${destHook}`);
|
|
173
|
-
console.log(`Wrote Langfuse config: ${path.join(langfuseDir, "config.json")}`);
|
|
174
|
-
|
|
175
|
-
console.log("Done. Restart Codex so the updated notify command is loaded.");
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
main().catch((err) => {
|
|
179
|
-
console.error(err?.message || String(err));
|
|
180
|
-
process.exit(1);
|
|
181
|
-
});
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import os from "node:os";
|
|
4
|
-
|
|
5
|
-
function readJsonIfExists(p) {
|
|
6
|
-
if (!fs.existsSync(p)) return null;
|
|
7
|
-
const txt = fs.readFileSync(p, "utf8");
|
|
8
|
-
if (!txt.trim()) return null;
|
|
9
|
-
return JSON.parse(txt);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function main() {
|
|
13
|
-
const userHome = os.homedir();
|
|
14
|
-
const claudeDir = path.join(userHome, ".claude");
|
|
15
|
-
const hooksDir = path.join(claudeDir, "hooks");
|
|
16
|
-
const settingsPath = path.join(claudeDir, "settings.json");
|
|
17
|
-
|
|
18
|
-
const results = [];
|
|
19
|
-
|
|
20
|
-
results.push({
|
|
21
|
-
item: "hooks 目录",
|
|
22
|
-
ok: fs.existsSync(hooksDir),
|
|
23
|
-
detail: hooksDir
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
const pyPath = path.join(hooksDir, "langfuse_hook.py");
|
|
27
|
-
results.push({
|
|
28
|
-
item: "hook 脚本",
|
|
29
|
-
ok: fs.existsSync(pyPath),
|
|
30
|
-
detail: pyPath
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
const settings = readJsonIfExists(settingsPath);
|
|
34
|
-
results.push({
|
|
35
|
-
item: "settings.json",
|
|
36
|
-
ok: !!settings,
|
|
37
|
-
detail: settingsPath
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
const env = settings?.env;
|
|
41
|
-
const neededEnv = [
|
|
42
|
-
"TRACE_TO_LANGFUSE",
|
|
43
|
-
"LANGFUSE_PUBLIC_KEY",
|
|
44
|
-
"LANGFUSE_SECRET_KEY",
|
|
45
|
-
"LANGFUSE_HOST",
|
|
46
|
-
"CC_LANGFUSE_BASE_URL",
|
|
47
|
-
"LANGFUSE_BASEURL"
|
|
48
|
-
];
|
|
49
|
-
|
|
50
|
-
const missingEnv = neededEnv.filter((k) => !env || typeof env[k] === "undefined");
|
|
51
|
-
results.push({
|
|
52
|
-
item: "env 必要字段",
|
|
53
|
-
ok: missingEnv.length === 0,
|
|
54
|
-
detail: missingEnv.length ? `缺少:${missingEnv.join(", ")}` : "OK"
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
const stopHooks = settings?.hooks?.Stop;
|
|
58
|
-
let hasStop = false;
|
|
59
|
-
if (Array.isArray(stopHooks)) {
|
|
60
|
-
for (const entry of stopHooks) {
|
|
61
|
-
const hooks = entry?.hooks;
|
|
62
|
-
if (!Array.isArray(hooks)) continue;
|
|
63
|
-
for (const h of hooks) {
|
|
64
|
-
if (h?.type === "command" && typeof h?.command === "string" && h.command.includes("langfuse_hook.py")) {
|
|
65
|
-
hasStop = true;
|
|
66
|
-
break;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
if (hasStop) break;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
results.push({
|
|
73
|
-
item: "hooks.Stop 配置",
|
|
74
|
-
ok: hasStop,
|
|
75
|
-
detail: hasStop ? "OK" : "未找到指向 langfuse_hook.py 的 Stop hook"
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
const pad = (s, n) => (s.length >= n ? s : s + " ".repeat(n - s.length));
|
|
79
|
-
const w = Math.max(...results.map((r) => r.item.length)) + 2;
|
|
80
|
-
for (const r of results) {
|
|
81
|
-
const status = r.ok ? "OK " : "BAD";
|
|
82
|
-
console.log(`${status} ${pad(r.item, w)} ${r.detail}`);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const failed = results.filter((r) => !r.ok);
|
|
86
|
-
process.exit(failed.length ? 2 : 0);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
main();
|
|
90
|
-
|
|
@@ -1,278 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import fsp from "node:fs/promises";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import os from "node:os";
|
|
5
|
-
import { execFileSync } from "node:child_process";
|
|
6
|
-
|
|
7
|
-
function parseArgs(argv) {
|
|
8
|
-
const args = {};
|
|
9
|
-
for (const raw of argv) {
|
|
10
|
-
if (!raw.startsWith("--")) continue;
|
|
11
|
-
const eq = raw.indexOf("=");
|
|
12
|
-
if (eq === -1) {
|
|
13
|
-
args[raw.slice(2)] = true;
|
|
14
|
-
} else {
|
|
15
|
-
args[raw.slice(2, eq)] = raw.slice(eq + 1);
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
return args;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function ensureDir(p) {
|
|
22
|
-
fs.mkdirSync(p, { recursive: true });
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function readJsonIfExists(p) {
|
|
26
|
-
if (!fs.existsSync(p)) return null;
|
|
27
|
-
const txt = fs.readFileSync(p, "utf8");
|
|
28
|
-
if (!txt.trim()) return null;
|
|
29
|
-
return JSON.parse(txt);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function writeJsonPretty(p, obj) {
|
|
33
|
-
fs.writeFileSync(p, JSON.stringify(obj, null, 2) + os.EOL, "utf8");
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function isObject(x) {
|
|
37
|
-
return x && typeof x === "object" && !Array.isArray(x);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function deepMerge(target, src) {
|
|
41
|
-
if (!isObject(target) || !isObject(src)) return src;
|
|
42
|
-
const out = { ...target };
|
|
43
|
-
for (const [k, v] of Object.entries(src)) {
|
|
44
|
-
if (isObject(v) && isObject(out[k])) out[k] = deepMerge(out[k], v);
|
|
45
|
-
else out[k] = v;
|
|
46
|
-
}
|
|
47
|
-
return out;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function psQuote(s) {
|
|
51
|
-
return `'${String(s).replace(/'/g, "''")}'`;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function extractZipWithPowerShell({ zipPath, destDir }) {
|
|
55
|
-
const tmpZip = path.join(os.tmpdir(), `cc-langfuse-hook-${Date.now()}.zip`);
|
|
56
|
-
const useTmp = !zipPath;
|
|
57
|
-
const actualZip = useTmp ? tmpZip : zipPath;
|
|
58
|
-
const cmdParts = ["$ErrorActionPreference = 'Stop';"];
|
|
59
|
-
if (useTmp) {
|
|
60
|
-
throw new Error("内部错误:extractZipWithPowerShell 需要 zipPath");
|
|
61
|
-
}
|
|
62
|
-
cmdParts.push(`Expand-Archive -LiteralPath ${psQuote(actualZip)} -DestinationPath ${psQuote(destDir)} -Force;`);
|
|
63
|
-
const cmd = cmdParts.join(" ");
|
|
64
|
-
|
|
65
|
-
try {
|
|
66
|
-
execFileSync("powershell", ["-NoProfile", "-Command", cmd], { stdio: "inherit" });
|
|
67
|
-
} catch {
|
|
68
|
-
throw new Error("解压 hooks zip 失败:请确认 zip 文件存在,且 PowerShell 支持 Expand-Archive。");
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function downloadAndExtractZipWithPowerShell({ zipUrl, destDir }) {
|
|
73
|
-
const tmpZip = path.join(os.tmpdir(), `cc-langfuse-hook-${Date.now()}.zip`);
|
|
74
|
-
const cmd = [
|
|
75
|
-
"$ErrorActionPreference = 'Stop';",
|
|
76
|
-
`Invoke-WebRequest -Uri ${psQuote(zipUrl)} -OutFile ${psQuote(tmpZip)};`,
|
|
77
|
-
`Expand-Archive -LiteralPath ${psQuote(tmpZip)} -DestinationPath ${psQuote(destDir)} -Force;`,
|
|
78
|
-
`Remove-Item -LiteralPath ${psQuote(tmpZip)} -Force;`
|
|
79
|
-
].join(" ");
|
|
80
|
-
|
|
81
|
-
try {
|
|
82
|
-
execFileSync("powershell", ["-NoProfile", "-Command", cmd], { stdio: "inherit" });
|
|
83
|
-
} catch (e) {
|
|
84
|
-
const msg = e?.message ? String(e.message) : "";
|
|
85
|
-
if (msg.includes("(401)") || msg.toLowerCase().includes("unauthorized")) {
|
|
86
|
-
throw new Error(
|
|
87
|
-
[
|
|
88
|
-
"下载 hooks zip 失败:HTTP 401 未授权(该附件需要登录态)。",
|
|
89
|
-
"解决办法:用浏览器登录后手动下载 zip 到本机,然后改用参数:",
|
|
90
|
-
" --zipPath=本地zip完整路径",
|
|
91
|
-
"例如:npm run langfuse:setup -- --userId=... --langfuseHost=... --zipPath=D:\\\\Downloads\\\\langfuse_hook.zip",
|
|
92
|
-
"(也支持 --pyPath=本地langfuse_hook.py,如果你已解压好)"
|
|
93
|
-
].join("\n")
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
throw new Error("下载/解压 hooks zip 失败:请确认能访问 zip URL,且 PowerShell 支持 Expand-Archive。");
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function setOrReplaceUserId(pyText, userId) {
|
|
101
|
-
const patterns = [
|
|
102
|
-
/user_id\s*=\s*["'][^"']*["']/g,
|
|
103
|
-
/USER_ID\s*=\s*["'][^"']*["']/g
|
|
104
|
-
];
|
|
105
|
-
for (const re of patterns) {
|
|
106
|
-
if (re.test(pyText)) {
|
|
107
|
-
return pyText.replace(re, (m) => m.replace(/["'][^"']*["']/, `"${userId}"`));
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
return pyText;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function normalizeWinPathForClaude(p) {
|
|
114
|
-
return p.replace(/\\/g, "/");
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function quoteCommandArg(s) {
|
|
118
|
-
return `"${String(s).replace(/"/g, '\\"')}"`;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function pythonExecutableInVenv(venvDir) {
|
|
122
|
-
return process.platform === "win32"
|
|
123
|
-
? path.join(venvDir, "Scripts", "python.exe")
|
|
124
|
-
: path.join(venvDir, "bin", "python");
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function pipInstallArgs(args) {
|
|
128
|
-
const pipIndexUrl = args.pipIndexUrl || args.pipindexurl || process.env.CODE_TOOL_PIP_INDEX_URL || "";
|
|
129
|
-
const pipArgs = ["-m", "pip", "install", "-U", "langfuse"];
|
|
130
|
-
if (pipIndexUrl) pipArgs.push("-i", pipIndexUrl);
|
|
131
|
-
return pipArgs;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function createOrUpdateLangfuseVenv({ baseDir, args }) {
|
|
135
|
-
const venvDir = path.join(baseDir, "langfuse-venv");
|
|
136
|
-
const pythonCmd = process.platform === "win32" ? "python" : "python3";
|
|
137
|
-
const venvPython = pythonExecutableInVenv(venvDir);
|
|
138
|
-
|
|
139
|
-
if (!fs.existsSync(venvPython)) {
|
|
140
|
-
console.log(`Creating Python virtual environment: ${venvDir}`);
|
|
141
|
-
try {
|
|
142
|
-
execFileSync(pythonCmd, ["-m", "venv", venvDir], { stdio: "inherit" });
|
|
143
|
-
} catch (e) {
|
|
144
|
-
if (process.platform !== "win32") {
|
|
145
|
-
throw new Error("Failed to create Python venv. Please install python3-venv first, for example: sudo apt install python3-venv");
|
|
146
|
-
}
|
|
147
|
-
throw new Error("Failed to create Python venv. Please confirm Python venv support is available.");
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
console.log("Installing/updating Python package in venv: langfuse");
|
|
152
|
-
const installArgs = pipInstallArgs(args);
|
|
153
|
-
try {
|
|
154
|
-
execFileSync(venvPython, installArgs, { stdio: "inherit" });
|
|
155
|
-
} catch (e) {
|
|
156
|
-
throw new Error(`Failed to install langfuse in venv: ${venvPython} ${installArgs.join(" ")}`);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
return venvPython;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
async function main() {
|
|
163
|
-
const args = parseArgs(process.argv.slice(2));
|
|
164
|
-
|
|
165
|
-
const userId = args.userId || args.userid || process.env.CC_USER_ID;
|
|
166
|
-
if (!userId || typeof userId !== "string") {
|
|
167
|
-
throw new Error("缺少参数:--userId=你的工号(或设置环境变量 CC_USER_ID)");
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const langfuseHost =
|
|
171
|
-
args.langfuseHost ||
|
|
172
|
-
args.host ||
|
|
173
|
-
process.env.LANGFUSE_HOST ||
|
|
174
|
-
"http://120.46.221.227:3000";
|
|
175
|
-
|
|
176
|
-
const publicKey =
|
|
177
|
-
args.publicKey || process.env.LANGFUSE_PUBLIC_KEY || "pk-lf-da0c90a7-6e93-4eb7-bb86-c1047c8d187d";
|
|
178
|
-
const secretKey =
|
|
179
|
-
args.secretKey || process.env.LANGFUSE_SECRET_KEY || "sk-lf-0269b85d-bfdc-442c-bfa3-e737954e3315";
|
|
180
|
-
if (!publicKey || !secretKey) {
|
|
181
|
-
throw new Error("缺少 Langfuse Key:请提供 --publicKey=... --secretKey=...(或设置环境变量 LANGFUSE_PUBLIC_KEY/LANGFUSE_SECRET_KEY)");
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
const zipUrl =
|
|
185
|
-
args.zipUrl ||
|
|
186
|
-
process.env.CC_LANGFUSE_ZIP_URL ||
|
|
187
|
-
"https://gitcode.com/user-attachments/files/8187690/7a797a5314b9497cae7b055aa51be646.zip";
|
|
188
|
-
const zipPath = args.zipPath || args.zippath || process.env.CC_LANGFUSE_ZIP_PATH;
|
|
189
|
-
const pyPathArg = args.pyPath || args.pypath || process.env.CC_LANGFUSE_PY_PATH;
|
|
190
|
-
|
|
191
|
-
const userHome = os.homedir();
|
|
192
|
-
const claudeDir = path.join(userHome, ".claude");
|
|
193
|
-
const hooksDir = path.join(claudeDir, "hooks");
|
|
194
|
-
ensureDir(hooksDir);
|
|
195
|
-
|
|
196
|
-
console.log(`将配置写入:${claudeDir}`);
|
|
197
|
-
|
|
198
|
-
// 1) 获取 hooks 脚本:优先 pyPath,其次本地 zipPath,最后尝试在线下载 zipUrl
|
|
199
|
-
if (!pyPathArg) {
|
|
200
|
-
if (zipPath) {
|
|
201
|
-
console.log(`解压 hooks zip(本地):${zipPath}`);
|
|
202
|
-
extractZipWithPowerShell({ zipPath, destDir: hooksDir });
|
|
203
|
-
} else {
|
|
204
|
-
console.log(`下载 hooks zip:${zipUrl}`);
|
|
205
|
-
downloadAndExtractZipWithPowerShell({ zipUrl, destDir: hooksDir });
|
|
206
|
-
}
|
|
207
|
-
} else {
|
|
208
|
-
console.log(`使用已有 hook 脚本:${pyPathArg}`);
|
|
209
|
-
if (!fs.existsSync(pyPathArg)) {
|
|
210
|
-
throw new Error(`找不到 --pyPath 指定的文件:${pyPathArg}`);
|
|
211
|
-
}
|
|
212
|
-
const destPy = path.join(hooksDir, "langfuse_hook.py");
|
|
213
|
-
await fsp.copyFile(pyPathArg, destPy);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// 2) 定位 python hook 文件(尽量兼容压缩包文件名)
|
|
217
|
-
const candidates = fs
|
|
218
|
-
.readdirSync(hooksDir, { withFileTypes: true })
|
|
219
|
-
.filter((d) => d.isFile() && d.name.toLowerCase().endsWith(".py"))
|
|
220
|
-
.map((d) => path.join(hooksDir, d.name));
|
|
221
|
-
|
|
222
|
-
const preferred = candidates.find((p) => path.basename(p).toLowerCase() === "langfuse_hook.py");
|
|
223
|
-
const pyPath = preferred || candidates[0];
|
|
224
|
-
if (!pyPath) throw new Error(`未在 ${hooksDir} 找到任何 .py 文件(zip 解压后为空?)`);
|
|
225
|
-
|
|
226
|
-
// 3) 替换 user_id
|
|
227
|
-
const pyText = await fsp.readFile(pyPath, "utf8");
|
|
228
|
-
const nextPyText = setOrReplaceUserId(pyText, userId);
|
|
229
|
-
if (nextPyText !== pyText) {
|
|
230
|
-
await fsp.writeFile(pyPath, nextPyText, "utf8");
|
|
231
|
-
}
|
|
232
|
-
const hookPython = createOrUpdateLangfuseVenv({ baseDir: claudeDir, args });
|
|
233
|
-
|
|
234
|
-
// 4) 合并写入 settings.json
|
|
235
|
-
const settingsPath = path.join(claudeDir, "settings.json");
|
|
236
|
-
const existing = readJsonIfExists(settingsPath) ?? {};
|
|
237
|
-
|
|
238
|
-
const pyCmdPath = normalizeWinPathForClaude(pyPath);
|
|
239
|
-
const pyExePath = normalizeWinPathForClaude(hookPython);
|
|
240
|
-
const desired = {
|
|
241
|
-
env: {
|
|
242
|
-
TRACE_TO_LANGFUSE: "true",
|
|
243
|
-
LANGFUSE_PUBLIC_KEY: publicKey,
|
|
244
|
-
LANGFUSE_SECRET_KEY: secretKey,
|
|
245
|
-
LANGFUSE_HOST: langfuseHost,
|
|
246
|
-
CC_LANGFUSE_BASE_URL: langfuseHost,
|
|
247
|
-
LANGFUSE_BASEURL: langfuseHost,
|
|
248
|
-
CC_LANGFUSE_USER_ID: userId,
|
|
249
|
-
CLAUDE_USER_ID: userId,
|
|
250
|
-
CC_USER_ID: userId,
|
|
251
|
-
LANGFUSE_USER_ID: userId
|
|
252
|
-
},
|
|
253
|
-
hooks: {
|
|
254
|
-
Stop: [
|
|
255
|
-
{
|
|
256
|
-
hooks: [
|
|
257
|
-
{
|
|
258
|
-
type: "command",
|
|
259
|
-
command: `${quoteCommandArg(pyExePath)} ${quoteCommandArg(pyCmdPath)}`
|
|
260
|
-
}
|
|
261
|
-
]
|
|
262
|
-
}
|
|
263
|
-
]
|
|
264
|
-
}
|
|
265
|
-
};
|
|
266
|
-
|
|
267
|
-
const merged = deepMerge(existing, desired);
|
|
268
|
-
ensureDir(claudeDir);
|
|
269
|
-
writeJsonPretty(settingsPath, merged);
|
|
270
|
-
console.log(`已更新:${settingsPath}`);
|
|
271
|
-
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
main().catch((err) => {
|
|
275
|
-
console.error(err?.message || String(err));
|
|
276
|
-
process.exit(1);
|
|
277
|
-
});
|
|
278
|
-
|