oh-aicoding-tool 0.1.1 → 0.1.4
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 -383
- package/package.json +28 -56
- 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
package/bin/langfuse-cli.js
DELETED
|
@@ -1,718 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import fs from "node:fs";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import readline from "node:readline";
|
|
5
|
-
import { createInterface } from "node:readline/promises";
|
|
6
|
-
import { fileURLToPath } from "node:url";
|
|
7
|
-
import { spawnSync } from "node:child_process";
|
|
8
|
-
|
|
9
|
-
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
10
|
-
const scriptsDir = path.join(rootDir, "scripts");
|
|
11
|
-
|
|
12
|
-
const DEFAULT_LANGFUSE_BASE_URL = "http://120.46.221.227:3000";
|
|
13
|
-
const DEFAULT_LANGFUSE_PUBLIC_KEY = "pk-lf-da0c90a7-6e93-4eb7-bb86-c1047c8d187d";
|
|
14
|
-
const DEFAULT_LANGFUSE_SECRET_KEY = "sk-lf-0269b85d-bfdc-442c-bfa3-e737954e3315";
|
|
15
|
-
|
|
16
|
-
const colorEnabled = process.stdout.isTTY && process.env.NO_COLOR !== "1";
|
|
17
|
-
const ansi = (code) => (colorEnabled ? `\x1b[${code}m` : "");
|
|
18
|
-
const rgb = (r, g, b) => (colorEnabled ? `\x1b[38;2;${r};${g};${b}m` : "");
|
|
19
|
-
const bg = (r, g, b) => (colorEnabled ? `\x1b[48;2;${r};${g};${b}m` : "");
|
|
20
|
-
const t = {
|
|
21
|
-
reset: ansi(0),
|
|
22
|
-
bold: ansi(1),
|
|
23
|
-
dim: ansi(2),
|
|
24
|
-
cyan: rgb(93, 214, 255),
|
|
25
|
-
teal: rgb(83, 232, 188),
|
|
26
|
-
blue: rgb(132, 167, 255),
|
|
27
|
-
violet: rgb(190, 145, 255),
|
|
28
|
-
gold: rgb(245, 202, 116),
|
|
29
|
-
red: rgb(255, 117, 117),
|
|
30
|
-
muted: rgb(132, 142, 158),
|
|
31
|
-
panel: rgb(73, 83, 101),
|
|
32
|
-
selectedBg: bg(31, 53, 68),
|
|
33
|
-
selectedFg: rgb(205, 247, 255)
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
function paint(s, ...styles) {
|
|
37
|
-
if (!colorEnabled) return s;
|
|
38
|
-
return `${styles.join("")}${s}${t.reset}`;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function parseArgs(argv) {
|
|
42
|
-
const args = { _: [] };
|
|
43
|
-
for (const raw of argv) {
|
|
44
|
-
if (!raw.startsWith("--")) {
|
|
45
|
-
args._.push(raw);
|
|
46
|
-
continue;
|
|
47
|
-
}
|
|
48
|
-
const eq = raw.indexOf("=");
|
|
49
|
-
if (eq === -1) args[raw.slice(2)] = true;
|
|
50
|
-
else args[raw.slice(2, eq)] = raw.slice(eq + 1);
|
|
51
|
-
}
|
|
52
|
-
return args;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function stripAnsi(s) {
|
|
56
|
-
return String(s).replace(/\x1b\[[0-9;]*m/g, "");
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function terminalWidth() {
|
|
60
|
-
return Math.max(72, Math.min(process.stdout.columns || 88, 104));
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function visibleLength(s) {
|
|
64
|
-
return stripAnsi(s).length;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function clearScreen() {
|
|
68
|
-
if (process.stdout.isTTY) process.stdout.write("\x1b[2J\x1b[H");
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function rule(width = terminalWidth()) {
|
|
72
|
-
return paint("─".repeat(width), t.panel);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function hasValue(v) {
|
|
76
|
-
return typeof v === "string" && v.trim().length > 0;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function scriptPath(name) {
|
|
80
|
-
return path.join(scriptsDir, name);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function commandExists(command, args = ["--version"]) {
|
|
84
|
-
const isWindowsShellScript = process.platform === "win32" && /\.(cmd|bat)$/i.test(command);
|
|
85
|
-
const r = isWindowsShellScript
|
|
86
|
-
? spawnSync([command, ...args].join(" "), { encoding: "utf8", shell: true })
|
|
87
|
-
: spawnSync(command, args, { encoding: "utf8" });
|
|
88
|
-
return {
|
|
89
|
-
ok: !r.error && r.status === 0,
|
|
90
|
-
stdout: (r.stdout || "").trim(),
|
|
91
|
-
stderr: (r.stderr || "").trim()
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function detectPython() {
|
|
96
|
-
const candidates = process.platform === "win32" ? ["python", "py"] : ["python3", "python"];
|
|
97
|
-
for (const cmd of candidates) {
|
|
98
|
-
const versionArgs = cmd === "py" ? ["-3", "--version"] : ["--version"];
|
|
99
|
-
const version = commandExists(cmd, versionArgs);
|
|
100
|
-
if (!version.ok) continue;
|
|
101
|
-
const pipArgs = cmd === "py" ? ["-3", "-m", "pip", "--version"] : ["-m", "pip", "--version"];
|
|
102
|
-
const pip = commandExists(cmd, pipArgs);
|
|
103
|
-
return { ok: true, command: cmd, version: version.stdout || version.stderr, pipOk: pip.ok, pipVersion: pip.stdout || pip.stderr };
|
|
104
|
-
}
|
|
105
|
-
return { ok: false, command: null, version: "", pipOk: false, pipVersion: "" };
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function detectOpencode() {
|
|
109
|
-
const candidates = process.platform === "win32" ? ["opencode.cmd", "opencode"] : ["opencode"];
|
|
110
|
-
for (const cmd of candidates) {
|
|
111
|
-
const r = commandExists(cmd, ["--version"]);
|
|
112
|
-
if (r.ok) return { ok: true, command: cmd, version: r.stdout || r.stderr };
|
|
113
|
-
}
|
|
114
|
-
return { ok: false, command: null, version: "" };
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function detectEnvironment() {
|
|
118
|
-
const node = { ok: true, version: process.version };
|
|
119
|
-
const npm = commandExists(process.platform === "win32" ? "npm.cmd" : "npm", ["--version"]);
|
|
120
|
-
const python = detectPython();
|
|
121
|
-
const opencode = detectOpencode();
|
|
122
|
-
return { node, npm, python, opencode };
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function statusIcon(ok) {
|
|
126
|
-
return ok ? paint("OK", t.teal) : paint("MISSING", t.red);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function installHint(name) {
|
|
130
|
-
if (name === "npm") {
|
|
131
|
-
return process.platform === "win32"
|
|
132
|
-
? "Install Node.js LTS from https://nodejs.org, then reopen the terminal."
|
|
133
|
-
: "Install Node.js/npm with your package manager, for example: sudo apt install nodejs npm";
|
|
134
|
-
}
|
|
135
|
-
if (name === "python") {
|
|
136
|
-
return process.platform === "win32"
|
|
137
|
-
? "Install Python from https://www.python.org/downloads/ and enable Add python.exe to PATH."
|
|
138
|
-
: "Install Python and pip, for example: sudo apt install python3 python3-pip";
|
|
139
|
-
}
|
|
140
|
-
if (name === "pip") {
|
|
141
|
-
return process.platform === "win32"
|
|
142
|
-
? "Install/repair Python with pip enabled, or run: python -m ensurepip --upgrade"
|
|
143
|
-
: "Install pip, for example: sudo apt install python3-pip";
|
|
144
|
-
}
|
|
145
|
-
if (name === "opencode") {
|
|
146
|
-
return "Install OpenCode CLI first, for example: npm install -g opencode-ai@latest";
|
|
147
|
-
}
|
|
148
|
-
return "";
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function renderEnvironment(env, { target = "all" } = {}) {
|
|
152
|
-
const rows = [
|
|
153
|
-
`${statusIcon(env.node.ok)} ${labelValue("Node.js", env.node.version, t.blue)}`,
|
|
154
|
-
`${statusIcon(env.npm.ok)} ${labelValue("npm", env.npm.stdout || env.npm.stderr || "<not found>", env.npm.ok ? t.blue : t.red)}`
|
|
155
|
-
];
|
|
156
|
-
if (target === "all" || target === "claude") {
|
|
157
|
-
rows.push(`${statusIcon(env.python.ok)} ${labelValue("Python", env.python.version || "<not found>", env.python.ok ? t.blue : t.red)}`);
|
|
158
|
-
rows.push(`${statusIcon(env.python.pipOk)} ${labelValue("pip", env.python.pipVersion || "<not found>", env.python.pipOk ? t.blue : t.red)}`);
|
|
159
|
-
}
|
|
160
|
-
if (target === "codex") {
|
|
161
|
-
rows.push(`${statusIcon(env.python.ok)} ${labelValue("Python", env.python.version || "<not found>", env.python.ok ? t.blue : t.red)}`);
|
|
162
|
-
rows.push(`${statusIcon(env.python.pipOk)} ${labelValue("pip", env.python.pipVersion || "<not found>", env.python.pipOk ? t.blue : t.red)}`);
|
|
163
|
-
}
|
|
164
|
-
if (target === "all" || target === "opencode") {
|
|
165
|
-
rows.push(`${statusIcon(env.opencode.ok)} ${labelValue("OpenCode CLI", env.opencode.version || "<not found>", env.opencode.ok ? t.blue : t.red)}`);
|
|
166
|
-
}
|
|
167
|
-
renderSection("Environment Check", rows);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function missingForTarget(env, target) {
|
|
171
|
-
const missing = [];
|
|
172
|
-
if (!env.npm.ok) missing.push(["npm", installHint("npm")]);
|
|
173
|
-
if (target === "claude" || target === "codex") {
|
|
174
|
-
if (!env.python.ok) missing.push(["Python", installHint("python")]);
|
|
175
|
-
else if (!env.python.pipOk) missing.push(["pip", installHint("pip")]);
|
|
176
|
-
}
|
|
177
|
-
if (target === "opencode" && !env.opencode.ok) missing.push(["OpenCode CLI", installHint("opencode")]);
|
|
178
|
-
return missing;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
async function ensureEnvironment(rl, target, options) {
|
|
182
|
-
clearScreen();
|
|
183
|
-
renderBrand(options);
|
|
184
|
-
const env = detectEnvironment();
|
|
185
|
-
renderEnvironment(env, { target });
|
|
186
|
-
const missing = missingForTarget(env, target);
|
|
187
|
-
if (missing.length === 0) return true;
|
|
188
|
-
|
|
189
|
-
renderSection(
|
|
190
|
-
"Action Needed",
|
|
191
|
-
missing.map(([name, hint]) => `${paint(name, t.red)} ${paint(hint, t.muted)}`)
|
|
192
|
-
);
|
|
193
|
-
console.log("");
|
|
194
|
-
await rl.question(`${paint("Press Enter after installing the missing dependency, or Ctrl+C to exit.", t.gold)} `);
|
|
195
|
-
return false;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function envDefault(name, fallback = "") {
|
|
199
|
-
return process.env[name] || fallback;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
function renderBrand({ dryRun = false } = {}) {
|
|
203
|
-
const width = terminalWidth();
|
|
204
|
-
console.log("");
|
|
205
|
-
console.log(
|
|
206
|
-
`${paint("Langfuse", t.bold, t.teal)} ${paint("Setup Console", t.bold, t.cyan)} ` +
|
|
207
|
-
paint("Claude Code / OpenCode / Codex", t.muted)
|
|
208
|
-
);
|
|
209
|
-
console.log(rule(width));
|
|
210
|
-
console.log(
|
|
211
|
-
`${paint("Mode", t.muted)} ${dryRun ? paint("dry-run", t.gold) : paint("write", t.teal)} ` +
|
|
212
|
-
`${paint("Config", t.muted)} ${DEFAULT_LANGFUSE_BASE_URL} ` +
|
|
213
|
-
`${paint("Secret", t.muted)} ${paint("configured", t.teal)}`
|
|
214
|
-
);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function renderSection(title, rows = []) {
|
|
218
|
-
const width = terminalWidth();
|
|
219
|
-
console.log("");
|
|
220
|
-
console.log(`${paint(title, t.bold, t.blue)}`);
|
|
221
|
-
console.log(paint("─".repeat(Math.min(width, 64)), t.panel));
|
|
222
|
-
for (const row of rows) console.log(row);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
function labelValue(label, value, valueStyle = t.reset) {
|
|
226
|
-
return `${paint(label.padEnd(22), t.muted)} ${paint(value, valueStyle)}`;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
function runNodeScript(name, args = [], { dryRun = false } = {}) {
|
|
230
|
-
const target = scriptPath(name);
|
|
231
|
-
const cmd = `${process.execPath} ${target} ${args.join(" ")}`.trim();
|
|
232
|
-
if (dryRun) {
|
|
233
|
-
renderSection("Preview", [paint(cmd, t.gold)]);
|
|
234
|
-
return 0;
|
|
235
|
-
}
|
|
236
|
-
console.log("");
|
|
237
|
-
console.log(paint("Running installer...", t.bold, t.teal));
|
|
238
|
-
console.log(paint("─".repeat(Math.min(terminalWidth(), 64)), t.panel));
|
|
239
|
-
const r = spawnSync(process.execPath, [target, ...args], { stdio: "inherit" });
|
|
240
|
-
return r.status ?? (r.error ? 1 : 0);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
async function askText(rl, label, { defaultValue = "", required = false } = {}) {
|
|
244
|
-
while (true) {
|
|
245
|
-
const suffix = defaultValue ? paint(` ${defaultValue}`, t.muted) : "";
|
|
246
|
-
const answer = (await rl.question(`${paint(label, t.cyan)}${suffix}\n${paint(">", t.teal)} `)).trim();
|
|
247
|
-
const value = answer || defaultValue;
|
|
248
|
-
if (!required || hasValue(value)) return value;
|
|
249
|
-
console.log(paint("This value is required.", t.red));
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
async function askYesNo(rl, label, { defaultValue = true } = {}) {
|
|
254
|
-
const hint = defaultValue ? "Y/n" : "y/N";
|
|
255
|
-
while (true) {
|
|
256
|
-
const answer = (await rl.question(`${paint(label, t.cyan)} ${paint(`(${hint})`, t.muted)} `)).trim().toLowerCase();
|
|
257
|
-
if (!answer) return defaultValue;
|
|
258
|
-
if (["y", "yes"].includes(answer)) return true;
|
|
259
|
-
if (["n", "no"].includes(answer)) return false;
|
|
260
|
-
console.log(paint("Please answer y or n.", t.red));
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
function renderChoiceScreen(label, choices, index, options = {}) {
|
|
265
|
-
clearScreen();
|
|
266
|
-
renderBrand(options);
|
|
267
|
-
console.log("");
|
|
268
|
-
console.log(paint(label, t.bold, t.cyan));
|
|
269
|
-
console.log(paint("Use arrow keys, number keys, or Enter. Press q to exit.", t.muted));
|
|
270
|
-
console.log("");
|
|
271
|
-
|
|
272
|
-
choices.forEach((choice, idx) => {
|
|
273
|
-
const selected = idx === index;
|
|
274
|
-
const hotkey = paint(`[${idx + 1}]`, selected ? t.selectedFg : t.muted);
|
|
275
|
-
const name = selected ? paint(` ${choice.label} `, t.bold, t.selectedFg, t.selectedBg) : paint(choice.label, t.bold);
|
|
276
|
-
const cursor = selected ? paint(">", t.teal) : " ";
|
|
277
|
-
console.log(` ${cursor} ${hotkey} ${name}`);
|
|
278
|
-
if (choice.description) {
|
|
279
|
-
console.log(` ${paint(choice.description, selected ? t.blue : t.muted)}`);
|
|
280
|
-
}
|
|
281
|
-
if (idx < choices.length - 1) console.log("");
|
|
282
|
-
});
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
async function askChoice(rl, label, choices, options = {}) {
|
|
286
|
-
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
287
|
-
rl.pause();
|
|
288
|
-
return await new Promise((resolve) => {
|
|
289
|
-
let index = 0;
|
|
290
|
-
const stdin = process.stdin;
|
|
291
|
-
|
|
292
|
-
function cleanup(value) {
|
|
293
|
-
stdin.off("keypress", onKeypress);
|
|
294
|
-
if (stdin.isTTY) stdin.setRawMode(false);
|
|
295
|
-
stdin.pause();
|
|
296
|
-
rl.resume();
|
|
297
|
-
clearScreen();
|
|
298
|
-
resolve(value);
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
function onKeypress(_, key = {}) {
|
|
302
|
-
if (key.ctrl && key.name === "c") return cleanup("exit");
|
|
303
|
-
if (key.name === "q" || key.name === "escape") return cleanup("exit");
|
|
304
|
-
if (key.name === "up") {
|
|
305
|
-
index = (index - 1 + choices.length) % choices.length;
|
|
306
|
-
renderChoiceScreen(label, choices, index, options);
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
309
|
-
if (key.name === "down") {
|
|
310
|
-
index = (index + 1) % choices.length;
|
|
311
|
-
renderChoiceScreen(label, choices, index, options);
|
|
312
|
-
return;
|
|
313
|
-
}
|
|
314
|
-
if (key.name === "return" || key.name === "enter") return cleanup(choices[index].value);
|
|
315
|
-
const num = Number.parseInt(key.sequence, 10);
|
|
316
|
-
if (Number.isInteger(num) && choices[num - 1]) return cleanup(choices[num - 1].value);
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
readline.emitKeypressEvents(stdin);
|
|
320
|
-
if (stdin.isTTY) stdin.setRawMode(true);
|
|
321
|
-
stdin.on("keypress", onKeypress);
|
|
322
|
-
stdin.resume();
|
|
323
|
-
renderChoiceScreen(label, choices, index, options);
|
|
324
|
-
});
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
console.log("");
|
|
328
|
-
console.log(label);
|
|
329
|
-
choices.forEach((choice, idx) => console.log(` ${idx + 1}. ${choice.label}`));
|
|
330
|
-
while (true) {
|
|
331
|
-
const answer = (await rl.question("> ")).trim();
|
|
332
|
-
const index = Number.parseInt(answer, 10) - 1;
|
|
333
|
-
if (Number.isInteger(index) && choices[index]) return choices[index].value;
|
|
334
|
-
const byValue = choices.find((choice) => choice.value === answer || choice.label.toLowerCase() === answer.toLowerCase());
|
|
335
|
-
if (byValue) return byValue.value;
|
|
336
|
-
console.log("Please choose one of the listed options.");
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
function renderMultiChoiceScreen(label, choices, index, selected, options = {}) {
|
|
341
|
-
clearScreen();
|
|
342
|
-
renderBrand(options);
|
|
343
|
-
console.log("");
|
|
344
|
-
console.log(paint(label, t.bold, t.cyan));
|
|
345
|
-
console.log(paint("Use arrow keys to move, Space to toggle, Enter to continue. Press q to exit.", t.muted));
|
|
346
|
-
console.log("");
|
|
347
|
-
|
|
348
|
-
choices.forEach((choice, idx) => {
|
|
349
|
-
const focused = idx === index;
|
|
350
|
-
const checked = selected.has(choice.value);
|
|
351
|
-
const box = checked ? paint("[✓]", t.teal) : paint("[ ]", t.muted);
|
|
352
|
-
const name = focused ? paint(` ${choice.label} `, t.bold, t.selectedFg, t.selectedBg) : paint(choice.label, t.bold);
|
|
353
|
-
const cursor = focused ? paint(">", t.teal) : " ";
|
|
354
|
-
console.log(` ${cursor} ${box} ${name}`);
|
|
355
|
-
if (choice.description) {
|
|
356
|
-
console.log(` ${paint(choice.description, focused ? t.blue : t.muted)}`);
|
|
357
|
-
}
|
|
358
|
-
if (idx < choices.length - 1) console.log("");
|
|
359
|
-
});
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
async function askMultiChoice(rl, label, choices, options = {}) {
|
|
363
|
-
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
364
|
-
rl.pause();
|
|
365
|
-
return await new Promise((resolve) => {
|
|
366
|
-
let index = 0;
|
|
367
|
-
const selected = new Set(choices.filter((choice) => choice.selected).map((choice) => choice.value));
|
|
368
|
-
const stdin = process.stdin;
|
|
369
|
-
|
|
370
|
-
function cleanup(value) {
|
|
371
|
-
stdin.off("keypress", onKeypress);
|
|
372
|
-
if (stdin.isTTY) stdin.setRawMode(false);
|
|
373
|
-
stdin.pause();
|
|
374
|
-
rl.resume();
|
|
375
|
-
clearScreen();
|
|
376
|
-
resolve(value);
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
function toggle() {
|
|
380
|
-
const value = choices[index].value;
|
|
381
|
-
if (selected.has(value)) selected.delete(value);
|
|
382
|
-
else selected.add(value);
|
|
383
|
-
renderMultiChoiceScreen(label, choices, index, selected, options);
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
function onKeypress(_, key = {}) {
|
|
387
|
-
if (key.ctrl && key.name === "c") return cleanup([]);
|
|
388
|
-
if (key.name === "q" || key.name === "escape") return cleanup([]);
|
|
389
|
-
if (key.name === "up") {
|
|
390
|
-
index = (index - 1 + choices.length) % choices.length;
|
|
391
|
-
renderMultiChoiceScreen(label, choices, index, selected, options);
|
|
392
|
-
return;
|
|
393
|
-
}
|
|
394
|
-
if (key.name === "down") {
|
|
395
|
-
index = (index + 1) % choices.length;
|
|
396
|
-
renderMultiChoiceScreen(label, choices, index, selected, options);
|
|
397
|
-
return;
|
|
398
|
-
}
|
|
399
|
-
if (key.name === "space") return toggle();
|
|
400
|
-
if (key.name === "return" || key.name === "enter") return cleanup([...selected]);
|
|
401
|
-
const num = Number.parseInt(key.sequence, 10);
|
|
402
|
-
if (Number.isInteger(num) && choices[num - 1]) {
|
|
403
|
-
index = num - 1;
|
|
404
|
-
toggle();
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
readline.emitKeypressEvents(stdin);
|
|
409
|
-
if (stdin.isTTY) stdin.setRawMode(true);
|
|
410
|
-
stdin.on("keypress", onKeypress);
|
|
411
|
-
stdin.resume();
|
|
412
|
-
renderMultiChoiceScreen(label, choices, index, selected, options);
|
|
413
|
-
});
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
console.log("");
|
|
417
|
-
console.log(label);
|
|
418
|
-
choices.forEach((choice, idx) => console.log(` ${idx + 1}. ${choice.label}`));
|
|
419
|
-
const answer = await rl.question("Select one or more numbers separated by comma > ");
|
|
420
|
-
return answer
|
|
421
|
-
.split(/[, ]+/)
|
|
422
|
-
.map((x) => Number.parseInt(x, 10) - 1)
|
|
423
|
-
.filter((idx) => Number.isInteger(idx) && choices[idx])
|
|
424
|
-
.map((idx) => choices[idx].value);
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
function langfuseConfig() {
|
|
428
|
-
return {
|
|
429
|
-
baseUrl: envDefault("LANGFUSE_BASEURL", envDefault("LANGFUSE_HOST", DEFAULT_LANGFUSE_BASE_URL)),
|
|
430
|
-
publicKey: envDefault("LANGFUSE_PUBLIC_KEY", DEFAULT_LANGFUSE_PUBLIC_KEY),
|
|
431
|
-
secretKey: envDefault("LANGFUSE_SECRET_KEY", DEFAULT_LANGFUSE_SECRET_KEY),
|
|
432
|
-
userId: envDefault("LANGFUSE_USER_ID", envDefault("CC_USER_ID", ""))
|
|
433
|
-
};
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
async function collectLangfuseConfig(rl, { requireUserId = false } = {}) {
|
|
437
|
-
const config = langfuseConfig();
|
|
438
|
-
renderSection("Langfuse Target", [
|
|
439
|
-
labelValue("Base URL", config.baseUrl, t.teal),
|
|
440
|
-
labelValue("Public Key", config.publicKey, t.blue),
|
|
441
|
-
labelValue("Secret Key", "configured", t.teal)
|
|
442
|
-
]);
|
|
443
|
-
config.userId = await askText(rl, "User ID / employee number, for example h00613222", {
|
|
444
|
-
defaultValue: config.userId,
|
|
445
|
-
required: requireUserId
|
|
446
|
-
});
|
|
447
|
-
return config;
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
async function collectSharedConfig(rl, options) {
|
|
451
|
-
clearScreen();
|
|
452
|
-
renderBrand(options);
|
|
453
|
-
return await collectLangfuseConfig(rl, { requireUserId: true });
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
function commonLangfuseArgs(config) {
|
|
457
|
-
return [
|
|
458
|
-
`--langfuseBaseUrl=${config.baseUrl}`,
|
|
459
|
-
`--publicKey=${config.publicKey}`,
|
|
460
|
-
`--secretKey=${config.secretKey}`,
|
|
461
|
-
...(hasValue(config.userId) ? [`--userId=${config.userId}`] : [])
|
|
462
|
-
];
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
async function confirmAction(rl, title, rows, options) {
|
|
466
|
-
clearScreen();
|
|
467
|
-
renderBrand(options);
|
|
468
|
-
renderSection(title, rows);
|
|
469
|
-
if (options.dryRun) return true;
|
|
470
|
-
console.log("");
|
|
471
|
-
return await askYesNo(rl, "Continue with these changes", { defaultValue: true });
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
async function setupClaude(rl, options) {
|
|
475
|
-
while (!(await ensureEnvironment(rl, "claude", options))) {}
|
|
476
|
-
clearScreen();
|
|
477
|
-
renderBrand(options);
|
|
478
|
-
const config = options.config || (await collectLangfuseConfig(rl, { requireUserId: true }));
|
|
479
|
-
const defaultHook = path.join(rootDir, "langfuse_hook.py");
|
|
480
|
-
const pyPath = fs.existsSync(defaultHook) ? defaultHook : path.join(rootDir, "langfuse_hook.py");
|
|
481
|
-
|
|
482
|
-
const args = [...commonLangfuseArgs(config), `--pyPath=${pyPath}`];
|
|
483
|
-
const ok = await confirmAction(
|
|
484
|
-
rl,
|
|
485
|
-
"Claude Code Langfuse Setup",
|
|
486
|
-
[
|
|
487
|
-
labelValue("User ID", config.userId, t.teal),
|
|
488
|
-
labelValue("Hook script", pyPath, t.blue),
|
|
489
|
-
labelValue("Settings file", "~/.claude/settings.json", t.violet),
|
|
490
|
-
labelValue("Python package", "langfuse", t.gold)
|
|
491
|
-
],
|
|
492
|
-
options
|
|
493
|
-
);
|
|
494
|
-
if (!ok) return 0;
|
|
495
|
-
return runNodeScript("langfuse-setup.mjs", args, options);
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
async function setupOpenCode(rl, options) {
|
|
499
|
-
while (!(await ensureEnvironment(rl, "opencode", options))) {}
|
|
500
|
-
clearScreen();
|
|
501
|
-
renderBrand(options);
|
|
502
|
-
const config = options.config || (await collectLangfuseConfig(rl, { requireUserId: true }));
|
|
503
|
-
const setEnv = true;
|
|
504
|
-
const installPlugin = true;
|
|
505
|
-
const cliPath = "";
|
|
506
|
-
|
|
507
|
-
const args = [
|
|
508
|
-
...commonLangfuseArgs(config),
|
|
509
|
-
...(!setEnv ? ["--no-set-env"] : []),
|
|
510
|
-
...(!installPlugin ? ["--skip-plugin-install"] : []),
|
|
511
|
-
...(hasValue(cliPath) ? [`--cmd=${cliPath}`] : [])
|
|
512
|
-
];
|
|
513
|
-
const ok = await confirmAction(
|
|
514
|
-
rl,
|
|
515
|
-
"OpenCode Langfuse Setup",
|
|
516
|
-
[
|
|
517
|
-
labelValue("User ID", config.userId || "<none>", config.userId ? t.teal : t.muted),
|
|
518
|
-
labelValue("User env vars", setEnv ? "write" : "skip", setEnv ? t.teal : t.gold),
|
|
519
|
-
labelValue("Plugin install", installPlugin ? "install/update" : "skip", installPlugin ? t.teal : t.gold),
|
|
520
|
-
labelValue("CLI path", cliPath || "auto-detect", t.blue),
|
|
521
|
-
labelValue("Config file", "~/.config/opencode/opencode.json", t.violet)
|
|
522
|
-
],
|
|
523
|
-
options
|
|
524
|
-
);
|
|
525
|
-
if (!ok) return 0;
|
|
526
|
-
return runNodeScript("opencode-langfuse-setup.mjs", args, options);
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
async function setupCodex(rl, options) {
|
|
530
|
-
while (!(await ensureEnvironment(rl, "codex", options))) {}
|
|
531
|
-
clearScreen();
|
|
532
|
-
renderBrand(options);
|
|
533
|
-
const config = options.config || (await collectLangfuseConfig(rl, { requireUserId: true }));
|
|
534
|
-
const defaultHook = path.join(rootDir, "codex_langfuse_notify.py");
|
|
535
|
-
const pyPath = fs.existsSync(defaultHook) ? defaultHook : path.join(rootDir, "codex_langfuse_notify.py");
|
|
536
|
-
|
|
537
|
-
const args = [...commonLangfuseArgs(config), `--pyPath=${pyPath}`];
|
|
538
|
-
const ok = await confirmAction(
|
|
539
|
-
rl,
|
|
540
|
-
"Codex Langfuse Setup",
|
|
541
|
-
[
|
|
542
|
-
labelValue("User ID", config.userId, t.teal),
|
|
543
|
-
labelValue("Notify hook", pyPath, t.blue),
|
|
544
|
-
labelValue("Config file", "~/.codex/config.toml", t.violet),
|
|
545
|
-
labelValue("Python package", "langfuse", t.gold)
|
|
546
|
-
],
|
|
547
|
-
options
|
|
548
|
-
);
|
|
549
|
-
if (!ok) return 0;
|
|
550
|
-
return runNodeScript("codex-langfuse-setup.mjs", args, options);
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
function checkClaude(options, { clear = true } = {}) {
|
|
554
|
-
if (clear) clearScreen();
|
|
555
|
-
renderBrand(options);
|
|
556
|
-
return runNodeScript("langfuse-check.mjs", [], options);
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
function checkOpenCode(options, { clear = true } = {}) {
|
|
560
|
-
if (clear) clearScreen();
|
|
561
|
-
renderBrand(options);
|
|
562
|
-
return runNodeScript("opencode-langfuse-check.mjs", [], options);
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
function checkCodex(options, { clear = true } = {}) {
|
|
566
|
-
if (clear) clearScreen();
|
|
567
|
-
renderBrand(options);
|
|
568
|
-
return runNodeScript("codex-langfuse-check.mjs", [], options);
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
async function checkMenu(rl, options) {
|
|
572
|
-
const target = await askChoice(
|
|
573
|
-
rl,
|
|
574
|
-
"Choose a configuration check",
|
|
575
|
-
[
|
|
576
|
-
{ label: "Environment", value: "environment", description: "Check Node.js, npm, Python, pip, and OpenCode CLI availability." },
|
|
577
|
-
{ label: "Claude Code", value: "claude", description: "Validate Claude hook settings and Langfuse environment fields." },
|
|
578
|
-
{ label: "OpenCode", value: "opencode", description: "Validate OpenCode plugin, OpenTelemetry, and launcher files." },
|
|
579
|
-
{ label: "Codex", value: "codex", description: "Validate Codex notify hook and session JSONL access." },
|
|
580
|
-
{ label: "All", value: "all", description: "Run all checks in sequence." }
|
|
581
|
-
],
|
|
582
|
-
options
|
|
583
|
-
);
|
|
584
|
-
if (target === "exit") return 0;
|
|
585
|
-
if (target === "environment") {
|
|
586
|
-
clearScreen();
|
|
587
|
-
renderBrand(options);
|
|
588
|
-
renderEnvironment(detectEnvironment(), { target: "all" });
|
|
589
|
-
return 0;
|
|
590
|
-
}
|
|
591
|
-
if (target === "claude") return checkClaude(options);
|
|
592
|
-
if (target === "opencode") return checkOpenCode(options);
|
|
593
|
-
if (target === "codex") return checkCodex(options);
|
|
594
|
-
clearScreen();
|
|
595
|
-
const claude = checkClaude(options, { clear: false });
|
|
596
|
-
console.log("");
|
|
597
|
-
const opencode = checkOpenCode(options, { clear: false });
|
|
598
|
-
console.log("");
|
|
599
|
-
const codex = checkCodex(options, { clear: false });
|
|
600
|
-
return claude || opencode || codex;
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
async function interactiveMain(options) {
|
|
604
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
605
|
-
try {
|
|
606
|
-
const action = await askChoice(
|
|
607
|
-
rl,
|
|
608
|
-
"What would you like to configure?",
|
|
609
|
-
[
|
|
610
|
-
{ label: "Setup Langfuse", value: "setup-langfuse", description: "Select one or more targets: Claude Code, OpenCode, Codex." },
|
|
611
|
-
{ label: "Check Environment", value: "check-environment", description: "Verify required local tools before setup." },
|
|
612
|
-
{ label: "Check Configuration", value: "check", description: "Inspect current setup without changing local files." },
|
|
613
|
-
{ label: "Exit", value: "exit", description: "Close the setup console." }
|
|
614
|
-
],
|
|
615
|
-
options
|
|
616
|
-
);
|
|
617
|
-
|
|
618
|
-
if (action === "setup-langfuse") return await setupLangfuseMenu(rl, options);
|
|
619
|
-
if (action === "setup-claude") return await setupClaude(rl, options);
|
|
620
|
-
if (action === "setup-opencode") return await setupOpenCode(rl, options);
|
|
621
|
-
if (action === "setup-codex") return await setupCodex(rl, options);
|
|
622
|
-
if (action === "check-environment") {
|
|
623
|
-
clearScreen();
|
|
624
|
-
renderBrand(options);
|
|
625
|
-
renderEnvironment(detectEnvironment(), { target: "all" });
|
|
626
|
-
return 0;
|
|
627
|
-
}
|
|
628
|
-
if (action === "check") return await checkMenu(rl, options);
|
|
629
|
-
return 0;
|
|
630
|
-
} finally {
|
|
631
|
-
rl.close();
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
async function setupLangfuseMenu(rl, options) {
|
|
636
|
-
const targets = await askMultiChoice(
|
|
637
|
-
rl,
|
|
638
|
-
"Select Langfuse setup targets",
|
|
639
|
-
[
|
|
640
|
-
{ label: "Claude Code Langfuse", value: "claude", selected: true, description: "Install the Langfuse Stop hook and connect Claude transcripts." },
|
|
641
|
-
{ label: "OpenCode Langfuse", value: "opencode", selected: false, description: "Install the Langfuse plugin and enable OpenTelemetry." },
|
|
642
|
-
{ label: "Codex Langfuse", value: "codex", selected: false, description: "Install the Codex notify hook and connect session JSONL events." }
|
|
643
|
-
],
|
|
644
|
-
options
|
|
645
|
-
);
|
|
646
|
-
|
|
647
|
-
if (!targets.length) return 0;
|
|
648
|
-
const config = await collectSharedConfig(rl, options);
|
|
649
|
-
let code = 0;
|
|
650
|
-
if (targets.includes("claude")) code ||= await setupClaude(rl, { ...options, config });
|
|
651
|
-
if (targets.includes("opencode")) code ||= await setupOpenCode(rl, { ...options, config });
|
|
652
|
-
if (targets.includes("codex")) code ||= await setupCodex(rl, { ...options, config });
|
|
653
|
-
return code;
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
function printHelp() {
|
|
657
|
-
renderBrand({ dryRun: false });
|
|
658
|
-
console.log("");
|
|
659
|
-
renderSection("Usage", [
|
|
660
|
-
"code-tool-langfuse",
|
|
661
|
-
"code-tool-langfuse setup",
|
|
662
|
-
"code-tool-langfuse setup claude",
|
|
663
|
-
"code-tool-langfuse setup opencode",
|
|
664
|
-
"code-tool-langfuse setup codex",
|
|
665
|
-
"code-tool-langfuse check",
|
|
666
|
-
"code-tool-langfuse check environment",
|
|
667
|
-
"code-tool-langfuse check claude",
|
|
668
|
-
"code-tool-langfuse check opencode",
|
|
669
|
-
"code-tool-langfuse check codex"
|
|
670
|
-
]);
|
|
671
|
-
renderSection("Options", [
|
|
672
|
-
`${paint("--dry-run", t.gold)} Preview actions without writing files or installing packages.`,
|
|
673
|
-
`${paint("--help", t.gold)} Show this help.`
|
|
674
|
-
]);
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
async function main() {
|
|
678
|
-
const args = parseArgs(process.argv.slice(2));
|
|
679
|
-
const options = { dryRun: !!args["dry-run"] };
|
|
680
|
-
const [cmd, target] = args._;
|
|
681
|
-
|
|
682
|
-
if (args.help || args.h) {
|
|
683
|
-
printHelp();
|
|
684
|
-
return 0;
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
if (!cmd) return await interactiveMain(options);
|
|
688
|
-
|
|
689
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
690
|
-
try {
|
|
691
|
-
if (cmd === "setup" && target === "claude") return await setupClaude(rl, options);
|
|
692
|
-
if (cmd === "setup" && target === "opencode") return await setupOpenCode(rl, options);
|
|
693
|
-
if (cmd === "setup" && target === "codex") return await setupCodex(rl, options);
|
|
694
|
-
if (cmd === "setup") return await setupLangfuseMenu(rl, options);
|
|
695
|
-
if (cmd === "check" && target === "claude") return checkClaude(options);
|
|
696
|
-
if (cmd === "check" && target === "opencode") return checkOpenCode(options);
|
|
697
|
-
if (cmd === "check" && target === "codex") return checkCodex(options);
|
|
698
|
-
if (cmd === "check" && target === "environment") {
|
|
699
|
-
clearScreen();
|
|
700
|
-
renderBrand(options);
|
|
701
|
-
renderEnvironment(detectEnvironment(), { target: "all" });
|
|
702
|
-
return 0;
|
|
703
|
-
}
|
|
704
|
-
if (cmd === "check") return await checkMenu(rl, options);
|
|
705
|
-
} finally {
|
|
706
|
-
rl.close();
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
printHelp();
|
|
710
|
-
return 1;
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
main()
|
|
714
|
-
.then((code) => process.exit(code))
|
|
715
|
-
.catch((err) => {
|
|
716
|
-
console.error(paint(err?.message || String(err), t.red));
|
|
717
|
-
process.exit(1);
|
|
718
|
-
});
|