oh-aicoding-tool 0.1.0

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.
Files changed (55) hide show
  1. package/CODEX_LANGFUSE_PLAN.md +62 -0
  2. package/README.md +114 -0
  3. package/bin/cli.js +166 -0
  4. package/bin/langfuse-cli.js +718 -0
  5. package/codex_langfuse_notify.py +591 -0
  6. package/langfuse_hook.py +603 -0
  7. package/opencode-ohai-report/.claude/commands/report-ai-issue.md +60 -0
  8. package/opencode-ohai-report/.opencode/commands/report-ai-issue.md +30 -0
  9. package/opencode-ohai-report/.opencode/plugins/oh-ai-report.ts +569 -0
  10. package/opencode-ohai-report/README.md +45 -0
  11. package/opencode-ohai-report/bin/cli.js +421 -0
  12. package/opencode-ohai-report/docs/opencode-ai-issue-collection-architecture.md +313 -0
  13. package/opencode-ohai-report/docs/opencode-ai-issue-collection-best-practices.md +476 -0
  14. package/opencode-ohai-report/docs/opencode-ai-issue-collection-phase1-summary.md +405 -0
  15. package/opencode-ohai-report/examples/issue_output.json +4 -0
  16. package/opencode-ohai-report/package.json +40 -0
  17. package/opencode-ohai-report/scripts/claude_report_hook.py +257 -0
  18. package/opencode-ohai-report/scripts/create_issue.py +34 -0
  19. package/opencode-ohai-report/scripts/install-claude-plugin.ps1 +254 -0
  20. package/opencode-ohai-report/scripts/install-opencode-plugin.ps1 +264 -0
  21. package/opencode-ohai-report/scripts/install-opencode-plugin.sh +218 -0
  22. package/opencode-ohai-report/scripts/merge-claude-settings.py +99 -0
  23. package/opencode-ohai-report/tools/ohai-report/README.md +151 -0
  24. package/opencode-ohai-report/tools/ohai-report/examples/issue-input.json +26 -0
  25. package/opencode-ohai-report/tools/ohai-report/ohai_report/__init__.py +5 -0
  26. package/opencode-ohai-report/tools/ohai-report/ohai_report/__main__.py +9 -0
  27. package/opencode-ohai-report/tools/ohai-report/ohai_report/cli.py +319 -0
  28. package/opencode-ohai-report/tools/ohai-report/ohai_report/git_context.py +32 -0
  29. package/opencode-ohai-report/tools/ohai-report/ohai_report/gitcode_defaults.py +14 -0
  30. package/opencode-ohai-report/tools/ohai-report/ohai_report/issue_markdown.py +313 -0
  31. package/opencode-ohai-report/tools/ohai-report/ohai_report/metadata.py +360 -0
  32. package/opencode-ohai-report/tools/ohai-report/ohai_report/observability/__init__.py +1 -0
  33. package/opencode-ohai-report/tools/ohai-report/ohai_report/observability/langfuse.py +38 -0
  34. package/opencode-ohai-report/tools/ohai-report/ohai_report/payload.py +64 -0
  35. package/opencode-ohai-report/tools/ohai-report/ohai_report/schema.py +80 -0
  36. package/opencode-ohai-report/tools/ohai-report/ohai_report/sinks/__init__.py +1 -0
  37. package/opencode-ohai-report/tools/ohai-report/ohai_report/sinks/base.py +15 -0
  38. package/opencode-ohai-report/tools/ohai-report/ohai_report/sinks/gitcode.py +405 -0
  39. package/opencode-ohai-report/tools/ohai-report/ohai_report/sinks/local.py +21 -0
  40. package/opencode-ohai-report/tools/ohai-report/ohai_report/sinks/webhook.py +354 -0
  41. package/opencode-ohai-report/tools/ohai-report/ohai_report/webhook_defaults.py +9 -0
  42. package/opencode-ohai-report/tools/ohai-report/ohai_report/workspace.py +61 -0
  43. package/opencode-ohai-report/tools/ohai-report/ohai_report.py +10 -0
  44. package/opencode-ohai-report/tools/ohai-report/schemas/report_issue.schema.json +166 -0
  45. package/package.json +59 -0
  46. package/scripts/codex-langfuse-check.mjs +101 -0
  47. package/scripts/codex-langfuse-setup.mjs +181 -0
  48. package/scripts/langfuse-check.mjs +90 -0
  49. package/scripts/langfuse-setup.mjs +278 -0
  50. package/scripts/opencode-langfuse-check.mjs +94 -0
  51. package/scripts/opencode-langfuse-run.mjs +96 -0
  52. package/scripts/opencode-langfuse-setup.mjs +478 -0
  53. package/scripts/resolve-opencode-cli.mjs +58 -0
  54. package/setup-langfuse.bat +163 -0
  55. package/setup-langfuse.sh +130 -0
@@ -0,0 +1,421 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { spawnSync } from "node:child_process";
6
+ import { createInterface } from "node:readline/promises";
7
+ import { fileURLToPath } from "node:url";
8
+
9
+ const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
10
+ const packageJson = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8"));
11
+
12
+ function parseArgs(argv) {
13
+ const args = { _: [] };
14
+ for (let i = 0; i < argv.length; i += 1) {
15
+ const raw = argv[i];
16
+ if (!raw.startsWith("--")) {
17
+ args._.push(raw);
18
+ continue;
19
+ }
20
+ const eq = raw.indexOf("=");
21
+ if (eq !== -1) {
22
+ args[raw.slice(2, eq)] = raw.slice(eq + 1);
23
+ continue;
24
+ }
25
+ const key = raw.slice(2);
26
+ const next = argv[i + 1];
27
+ if (next && !next.startsWith("--") && ["email"].includes(key)) {
28
+ args[key] = next;
29
+ i += 1;
30
+ } else {
31
+ args[key] = true;
32
+ }
33
+ }
34
+ return args;
35
+ }
36
+
37
+ function configHome() {
38
+ const xdg = (process.env.XDG_CONFIG_HOME || "").trim();
39
+ if (xdg) return xdg;
40
+ return path.join(os.homedir(), ".config");
41
+ }
42
+
43
+ function opencodeConfigDir() {
44
+ return path.join(configHome(), "opencode");
45
+ }
46
+
47
+ function runtimeRoot() {
48
+ return path.join(configHome(), "ohai-report", "runtime");
49
+ }
50
+
51
+ function runtimeCliPath() {
52
+ return path.join(runtimeRoot(), "tools", "ohai-report", "ohai_report.py");
53
+ }
54
+
55
+ function sourceCliPath() {
56
+ return path.join(rootDir, "tools", "ohai-report", "ohai_report.py");
57
+ }
58
+
59
+ function ensureDir(p) {
60
+ fs.mkdirSync(p, { recursive: true });
61
+ }
62
+
63
+ function copyDir(src, dest) {
64
+ if (!fs.existsSync(src)) return;
65
+ ensureDir(dest);
66
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
67
+ if (entry.name === "__pycache__" || entry.name === ".git" || entry.name === ".ohai-report") continue;
68
+ const s = path.join(src, entry.name);
69
+ const d = path.join(dest, entry.name);
70
+ if (entry.isDirectory()) copyDir(s, d);
71
+ else fs.copyFileSync(s, d);
72
+ }
73
+ }
74
+
75
+ function installRuntime({ dryRun }) {
76
+ const dest = runtimeRoot();
77
+ const copies = [
78
+ [".opencode", ".opencode"],
79
+ [".claude", ".claude"],
80
+ ["scripts", "scripts"],
81
+ [path.join("tools", "ohai-report"), path.join("tools", "ohai-report")],
82
+ ];
83
+ if (dryRun) {
84
+ console.log(`[dry-run] copy package runtime -> ${dest}`);
85
+ return dest;
86
+ }
87
+ for (const [srcRel, destRel] of copies) {
88
+ copyDir(path.join(rootDir, srcRel), path.join(dest, destRel));
89
+ }
90
+ return dest;
91
+ }
92
+
93
+ function readJsonIfExists(p) {
94
+ if (!fs.existsSync(p)) return null;
95
+ const raw = fs.readFileSync(p, "utf8").replace(/^\uFEFF/, "");
96
+ if (!raw.trim()) return null;
97
+ return JSON.parse(raw);
98
+ }
99
+
100
+ function writeJson(p, obj) {
101
+ ensureDir(path.dirname(p));
102
+ fs.writeFileSync(p, `${JSON.stringify(obj, null, 2)}${os.EOL}`, "utf8");
103
+ }
104
+
105
+ function emailIsValid(value) {
106
+ const t = String(value || "").trim();
107
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(t);
108
+ }
109
+
110
+ function readSavedEmail(paths) {
111
+ for (const p of paths) {
112
+ try {
113
+ const obj = readJsonIfExists(p);
114
+ const email = obj?.user_email || obj?.userEmail;
115
+ if (emailIsValid(email)) return { email: String(email).trim(), path: p };
116
+ } catch {
117
+ // Ignore malformed legacy email files.
118
+ }
119
+ }
120
+ return null;
121
+ }
122
+
123
+ async function resolveEmail({ label, defaultPaths, destPath, skipEmail, emailArg, dryRun }) {
124
+ if (skipEmail) return "";
125
+ if (emailIsValid(emailArg)) return String(emailArg).trim();
126
+ if (emailArg) throw new Error(`Invalid --email value: ${emailArg}`);
127
+ const envEmail = process.env.OHAI_INSTALL_USER_EMAIL;
128
+ if (emailIsValid(envEmail)) return envEmail.trim();
129
+
130
+ const saved = readSavedEmail(defaultPaths);
131
+ const current = saved?.email || "";
132
+ if (!process.stdin.isTTY) return "";
133
+
134
+ console.log("");
135
+ console.log(`${label} email file: ${saved?.path || destPath}`);
136
+ console.log(`Current company email: ${current || "(not set)"}`);
137
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
138
+ try {
139
+ const answer = (await rl.question("Update company email? [y/N] ")).trim().toLowerCase();
140
+ if (!["y", "yes"].includes(answer)) return "";
141
+ while (true) {
142
+ const value = (await rl.question("Company email: ")).trim();
143
+ if (emailIsValid(value)) return value;
144
+ console.log("Invalid email. Example: name@company.com");
145
+ }
146
+ } finally {
147
+ rl.close();
148
+ }
149
+ }
150
+
151
+ function saveEmail(destPath, email, { dryRun }) {
152
+ if (!email) return;
153
+ if (dryRun) {
154
+ console.log(`[dry-run] write ${destPath}`);
155
+ return;
156
+ }
157
+ writeJson(destPath, { user_email: email });
158
+ console.log(`Saved company email: ${destPath}`);
159
+ }
160
+
161
+ function psQuote(value) {
162
+ return `'${String(value).replace(/'/g, "''")}'`;
163
+ }
164
+
165
+ function setUserEnv(name, value, { dryRun }) {
166
+ if (dryRun) {
167
+ console.log(`[dry-run] set user env ${name}=${value}`);
168
+ return;
169
+ }
170
+ if (process.platform === "win32") {
171
+ const cmd = `[Environment]::SetEnvironmentVariable(${psQuote(name)}, ${psQuote(value)}, 'User')`;
172
+ const r = spawnSync("powershell.exe", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", cmd], {
173
+ stdio: "inherit",
174
+ windowsHide: true,
175
+ });
176
+ if (r.status !== 0) throw new Error(`Failed to set user env ${name}`);
177
+ console.log(`Set user env ${name}=${value}`);
178
+ return;
179
+ }
180
+
181
+ const envDir = path.join(configHome(), "environment.d");
182
+ const envFile = path.join(envDir, "99-ohai-report-opencode.conf");
183
+ ensureDir(envDir);
184
+ fs.writeFileSync(envFile, `${name}=${value}${os.EOL}`, "utf8");
185
+ console.log(`Wrote ${envFile}`);
186
+
187
+ if (process.platform === "darwin") {
188
+ const zprofile = path.join(os.homedir(), ".zprofile");
189
+ const marker = "# oh-ai-report-opencode";
190
+ const current = fs.existsSync(zprofile) ? fs.readFileSync(zprofile, "utf8") : "";
191
+ if (!current.includes(marker)) {
192
+ fs.appendFileSync(zprofile, `${os.EOL}${marker}${os.EOL}export ${name}=${shellQuote(value)}${os.EOL}`, "utf8");
193
+ console.log(`Appended ${name} to ${zprofile}`);
194
+ }
195
+ }
196
+ }
197
+
198
+ function shellQuote(value) {
199
+ return `'${String(value).replace(/'/g, "'\\''")}'`;
200
+ }
201
+
202
+ function mergePluginList(existing, entry) {
203
+ let list = [];
204
+ if (Array.isArray(existing)) list = existing.map(String).filter(Boolean);
205
+ else if (typeof existing === "string" && existing.trim()) list = [existing.trim()];
206
+ if (!list.includes(entry)) list.push(entry);
207
+ return list;
208
+ }
209
+
210
+ function updateOpenCodeConfig({ dryRun }) {
211
+ const configPath = path.join(opencodeConfigDir(), "opencode.json");
212
+ const entry = "./plugins/oh-ai-report.ts";
213
+ if (dryRun) {
214
+ console.log(`[dry-run] ensure ${configPath} includes plugin ${entry}`);
215
+ return;
216
+ }
217
+ let config = { "$schema": "https://opencode.ai/config.json" };
218
+ if (fs.existsSync(configPath)) {
219
+ try {
220
+ config = readJsonIfExists(configPath) || config;
221
+ } catch (err) {
222
+ console.warn(`Warning: could not parse ${configPath}; leaving it unchanged: ${err.message}`);
223
+ return;
224
+ }
225
+ }
226
+ config.plugin = mergePluginList(config.plugin, entry);
227
+ writeJson(configPath, config);
228
+ console.log(`Updated OpenCode config: ${configPath}`);
229
+ }
230
+
231
+ function detectPython() {
232
+ const candidates = process.platform === "win32" ? ["python", "python3", "py"] : ["python3", "python"];
233
+ for (const command of candidates) {
234
+ const r = spawnSync(command, ["--version"], { encoding: "utf8", windowsHide: true });
235
+ if (!r.error && r.status === 0) return command;
236
+ }
237
+ return "";
238
+ }
239
+
240
+ function installOpenCode(options) {
241
+ const ocRoot = opencodeConfigDir();
242
+ const pluginSrc = path.join(runtimeRoot(), ".opencode", "plugins", "oh-ai-report.ts");
243
+ const commandSrc = path.join(runtimeRoot(), ".opencode", "commands", "report-ai-issue.md");
244
+ const pluginDest = path.join(ocRoot, "plugins", "oh-ai-report.ts");
245
+ const commandDest = path.join(ocRoot, "commands", "report-ai-issue.md");
246
+
247
+ if (options.dryRun) {
248
+ console.log(`[dry-run] copy ${pluginSrc} -> ${pluginDest}`);
249
+ if (!options.pluginOnly) console.log(`[dry-run] copy ${commandSrc} -> ${commandDest}`);
250
+ } else {
251
+ ensureDir(path.dirname(pluginDest));
252
+ fs.copyFileSync(pluginSrc, pluginDest);
253
+ console.log(`Installed OpenCode plugin: ${pluginDest}`);
254
+ if (!options.pluginOnly) {
255
+ ensureDir(path.dirname(commandDest));
256
+ fs.copyFileSync(commandSrc, commandDest);
257
+ console.log(`Installed OpenCode command: ${commandDest}`);
258
+ }
259
+ }
260
+ updateOpenCodeConfig(options);
261
+ }
262
+
263
+ function installClaude(options) {
264
+ const claudeRoot = path.join(os.homedir(), ".claude");
265
+ const commandSrc = path.join(runtimeRoot(), ".claude", "commands", "report-ai-issue.md");
266
+ const hookSrc = path.join(runtimeRoot(), "scripts", "claude_report_hook.py");
267
+ const mergePy = path.join(runtimeRoot(), "scripts", "merge-claude-settings.py");
268
+ const commandDest = path.join(claudeRoot, "commands", "report-ai-issue.md");
269
+ const hookDest = path.join(claudeRoot, "hooks", "ohai-report-hook.py");
270
+ const settingsPath = path.join(claudeRoot, "settings.json");
271
+
272
+ if (options.dryRun) {
273
+ console.log(`[dry-run] copy ${commandSrc} -> ${commandDest}`);
274
+ if (!options.skipHook) {
275
+ console.log(`[dry-run] copy ${hookSrc} -> ${hookDest}`);
276
+ console.log(`[dry-run] merge Claude settings: ${settingsPath}`);
277
+ }
278
+ return;
279
+ }
280
+
281
+ ensureDir(path.dirname(commandDest));
282
+ fs.copyFileSync(commandSrc, commandDest);
283
+ console.log(`Installed Claude command: ${commandDest}`);
284
+
285
+ if (!options.skipHook) {
286
+ const python = detectPython();
287
+ if (!python) throw new Error("Python was not found. Install Python or use --skip-hook.");
288
+ ensureDir(path.dirname(hookDest));
289
+ fs.copyFileSync(hookSrc, hookDest);
290
+ console.log(`Installed Claude hook: ${hookDest}`);
291
+ const r = spawnSync(python, [mergePy, "--settings", settingsPath, "--hook", hookDest, "--python-command", python], {
292
+ stdio: "inherit",
293
+ windowsHide: true,
294
+ });
295
+ if (r.status !== 0) throw new Error("Failed to merge Claude settings.json");
296
+ }
297
+ }
298
+
299
+ async function install(target, args) {
300
+ const options = {
301
+ dryRun: !!args["dry-run"],
302
+ pluginOnly: !!args["plugin-only"],
303
+ skipEnv: !!args["skip-env"],
304
+ skipEmail: !!args["skip-email"],
305
+ skipHook: !!args["skip-hook"],
306
+ };
307
+ if (!["opencode", "claude", "both"].includes(target)) {
308
+ throw new Error("Install target must be one of: opencode, claude, both");
309
+ }
310
+
311
+ installRuntime(options);
312
+
313
+ if (target === "opencode" || target === "both") {
314
+ const emailPath = path.join(opencodeConfigDir(), "ohai-report", "email.json");
315
+ const email = await resolveEmail({
316
+ label: "OpenCode",
317
+ defaultPaths: [emailPath, path.join(opencodeConfigDir(), "ohai-report-user.json")],
318
+ destPath: emailPath,
319
+ skipEmail: options.skipEmail,
320
+ emailArg: args.email,
321
+ dryRun: options.dryRun,
322
+ });
323
+ installOpenCode(options);
324
+ saveEmail(emailPath, email, options);
325
+ }
326
+
327
+ if (target === "claude" || target === "both") {
328
+ const emailPath = path.join(os.homedir(), ".claude", "ohai-report", "email.json");
329
+ const email = await resolveEmail({
330
+ label: "Claude",
331
+ defaultPaths: [emailPath, path.join(opencodeConfigDir(), "ohai-report", "email.json")],
332
+ destPath: emailPath,
333
+ skipEmail: options.skipEmail,
334
+ emailArg: args.email,
335
+ dryRun: options.dryRun,
336
+ });
337
+ installClaude(options);
338
+ saveEmail(emailPath, email, options);
339
+ }
340
+
341
+ if (!options.skipEnv) setUserEnv("OHAI_REPORT_CLI", runtimeCliPath(), options);
342
+ else console.log("Skipped env (--skip-env).");
343
+
344
+ console.log("");
345
+ console.log("Done. Fully quit and restart your AI coding assistant.");
346
+ console.log(`OHAI_REPORT_CLI: ${runtimeCliPath()}`);
347
+ }
348
+
349
+ function doctor() {
350
+ const rows = [
351
+ ["Package root", rootDir],
352
+ ["Runtime root", runtimeRoot()],
353
+ ["Runtime CLI", fs.existsSync(runtimeCliPath()) ? runtimeCliPath() : "(not installed yet)"],
354
+ ["Source CLI", sourceCliPath()],
355
+ ["Python", detectPython() || "(not found)"],
356
+ ["OpenCode config", opencodeConfigDir()],
357
+ ["OHAI_REPORT_CLI", process.env.OHAI_REPORT_CLI || "(not set in current process)"],
358
+ ];
359
+ for (const [key, value] of rows) console.log(`${key.padEnd(18)} ${value}`);
360
+ }
361
+
362
+ function runReporter(argv) {
363
+ const python = detectPython();
364
+ if (!python) throw new Error("Python was not found. Install Python and retry.");
365
+ const cli = fs.existsSync(runtimeCliPath()) ? runtimeCliPath() : sourceCliPath();
366
+ const r = spawnSync(python, [cli, ...argv], { stdio: "inherit", windowsHide: true });
367
+ return r.status ?? 1;
368
+ }
369
+
370
+ function printHelp() {
371
+ console.log(`${packageJson.name} ${packageJson.version}`);
372
+ console.log("");
373
+ console.log("Usage:");
374
+ console.log(" opencode-ohai-report install opencode [--email user@company.com]");
375
+ console.log(" opencode-ohai-report install claude");
376
+ console.log(" opencode-ohai-report install both");
377
+ console.log(" opencode-ohai-report doctor");
378
+ console.log(" opencode-ohai-report create ...");
379
+ console.log(" opencode-ohai-report metadata update ...");
380
+ console.log("");
381
+ console.log("Options:");
382
+ console.log(" --dry-run Preview install actions");
383
+ console.log(" --plugin-only OpenCode: install plugin without command");
384
+ console.log(" --skip-env Do not write OHAI_REPORT_CLI");
385
+ console.log(" --skip-email Do not prompt or write email.json");
386
+ console.log(" --skip-hook Claude: skip hook/settings changes");
387
+ console.log(" --email <addr> Save company email non-interactively");
388
+ }
389
+
390
+ async function main() {
391
+ const args = parseArgs(process.argv.slice(2));
392
+ const [cmd, target] = args._;
393
+ if (!cmd || args.help || args.h) {
394
+ printHelp();
395
+ return 0;
396
+ }
397
+ if (args.version || args.v) {
398
+ console.log(packageJson.version);
399
+ return 0;
400
+ }
401
+ if (cmd === "install") {
402
+ await install(target || "opencode", args);
403
+ return 0;
404
+ }
405
+ if (cmd === "doctor" || cmd === "check") {
406
+ doctor();
407
+ return 0;
408
+ }
409
+ if (cmd === "create" || cmd === "metadata") {
410
+ return runReporter(args._);
411
+ }
412
+ printHelp();
413
+ return 1;
414
+ }
415
+
416
+ main()
417
+ .then((code) => process.exit(code))
418
+ .catch((err) => {
419
+ console.error(err?.message || String(err));
420
+ process.exit(1);
421
+ });