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.
Files changed (55) hide show
  1. package/README.md +79 -80
  2. package/bin/cli.js +257 -383
  3. package/package.json +28 -56
  4. package/CODEX_LANGFUSE_PLAN.md +0 -62
  5. package/bin/langfuse-cli.js +0 -718
  6. package/codex_langfuse_notify.py +0 -591
  7. package/langfuse_hook.py +0 -603
  8. package/opencode-ohai-report/.claude/commands/report-ai-issue.md +0 -60
  9. package/opencode-ohai-report/.opencode/commands/report-ai-issue.md +0 -30
  10. package/opencode-ohai-report/.opencode/plugins/oh-ai-report.ts +0 -569
  11. package/opencode-ohai-report/README.md +0 -45
  12. package/opencode-ohai-report/bin/cli.js +0 -421
  13. package/opencode-ohai-report/docs/opencode-ai-issue-collection-architecture.md +0 -313
  14. package/opencode-ohai-report/docs/opencode-ai-issue-collection-best-practices.md +0 -476
  15. package/opencode-ohai-report/docs/opencode-ai-issue-collection-phase1-summary.md +0 -405
  16. package/opencode-ohai-report/examples/issue_output.json +0 -4
  17. package/opencode-ohai-report/package.json +0 -40
  18. package/opencode-ohai-report/scripts/claude_report_hook.py +0 -257
  19. package/opencode-ohai-report/scripts/create_issue.py +0 -34
  20. package/opencode-ohai-report/scripts/install-claude-plugin.ps1 +0 -254
  21. package/opencode-ohai-report/scripts/install-opencode-plugin.ps1 +0 -264
  22. package/opencode-ohai-report/scripts/install-opencode-plugin.sh +0 -218
  23. package/opencode-ohai-report/scripts/merge-claude-settings.py +0 -99
  24. package/opencode-ohai-report/tools/ohai-report/README.md +0 -151
  25. package/opencode-ohai-report/tools/ohai-report/examples/issue-input.json +0 -26
  26. package/opencode-ohai-report/tools/ohai-report/ohai_report/__init__.py +0 -5
  27. package/opencode-ohai-report/tools/ohai-report/ohai_report/__main__.py +0 -9
  28. package/opencode-ohai-report/tools/ohai-report/ohai_report/cli.py +0 -319
  29. package/opencode-ohai-report/tools/ohai-report/ohai_report/git_context.py +0 -32
  30. package/opencode-ohai-report/tools/ohai-report/ohai_report/gitcode_defaults.py +0 -14
  31. package/opencode-ohai-report/tools/ohai-report/ohai_report/issue_markdown.py +0 -313
  32. package/opencode-ohai-report/tools/ohai-report/ohai_report/metadata.py +0 -360
  33. package/opencode-ohai-report/tools/ohai-report/ohai_report/observability/__init__.py +0 -1
  34. package/opencode-ohai-report/tools/ohai-report/ohai_report/observability/langfuse.py +0 -38
  35. package/opencode-ohai-report/tools/ohai-report/ohai_report/payload.py +0 -64
  36. package/opencode-ohai-report/tools/ohai-report/ohai_report/schema.py +0 -80
  37. package/opencode-ohai-report/tools/ohai-report/ohai_report/sinks/__init__.py +0 -1
  38. package/opencode-ohai-report/tools/ohai-report/ohai_report/sinks/base.py +0 -15
  39. package/opencode-ohai-report/tools/ohai-report/ohai_report/sinks/gitcode.py +0 -405
  40. package/opencode-ohai-report/tools/ohai-report/ohai_report/sinks/local.py +0 -21
  41. package/opencode-ohai-report/tools/ohai-report/ohai_report/sinks/webhook.py +0 -354
  42. package/opencode-ohai-report/tools/ohai-report/ohai_report/webhook_defaults.py +0 -9
  43. package/opencode-ohai-report/tools/ohai-report/ohai_report/workspace.py +0 -61
  44. package/opencode-ohai-report/tools/ohai-report/ohai_report.py +0 -10
  45. package/opencode-ohai-report/tools/ohai-report/schemas/report_issue.schema.json +0 -166
  46. package/scripts/codex-langfuse-check.mjs +0 -101
  47. package/scripts/codex-langfuse-setup.mjs +0 -181
  48. package/scripts/langfuse-check.mjs +0 -90
  49. package/scripts/langfuse-setup.mjs +0 -278
  50. package/scripts/opencode-langfuse-check.mjs +0 -94
  51. package/scripts/opencode-langfuse-run.mjs +0 -96
  52. package/scripts/opencode-langfuse-setup.mjs +0 -478
  53. package/scripts/resolve-opencode-cli.mjs +0 -58
  54. package/setup-langfuse.bat +0 -163
  55. package/setup-langfuse.sh +0 -130
@@ -1,405 +0,0 @@
1
- """GitCode Issue API sink (GitCode OpenAPI v5 create issue)."""
2
-
3
- from __future__ import annotations
4
-
5
- import json
6
- import os
7
- import urllib.error
8
- import urllib.parse
9
- import urllib.request
10
- from dataclasses import dataclass
11
- from typing import Any
12
-
13
- from ..gitcode_defaults import DEFAULT_GITCODE_OWNER, DEFAULT_GITCODE_REPO
14
- from ..issue_markdown import derive_gitcode_dimension_labels, format_issue_markdown, sanitize_gitcode_label_value
15
- from .base import SinkResult
16
- from .webhook import parse_labels_value
17
-
18
-
19
- LABEL_COLOR_BY_NAME = {
20
- "opencode": "#1d76db",
21
- "claude": "#5319e7",
22
- "codex": "#006b75",
23
- "unspecified": "#ededed",
24
- "P0": "#d73a4a",
25
- "P1": "#f66a0a",
26
- "P2": "#fbca04",
27
- "P3": "#0e8a16",
28
- "model-output-error": "#d73a4a",
29
- "model-capability": "#7057ff",
30
- "tool-call-failure": "#d93f0b",
31
- "skill-defect": "#b60205",
32
- "context-missing": "#c5def5",
33
- "environment": "#bfdadc",
34
- "build": "#0052cc",
35
- "compile": "#0052cc",
36
- "other": "#ededed",
37
- }
38
-
39
-
40
- class GitCodeSinkError(RuntimeError):
41
- def __init__(self, message: str, *, status_code: int = 0, body: str = "") -> None:
42
- super().__init__(message)
43
- self.status_code = status_code
44
- self.body = body
45
-
46
-
47
- def _truthy_env(raw: str) -> bool:
48
- return raw.strip().lower() in ("1", "true", "yes", "on")
49
-
50
-
51
- def gitcode_label_color(name: str, fallback: str = "#ededed") -> str:
52
- label = sanitize_gitcode_label_value(name)
53
- if label in LABEL_COLOR_BY_NAME:
54
- return LABEL_COLOR_BY_NAME[label]
55
- low = label.casefold()
56
- if low.startswith(("model-", "capability")):
57
- return "#7057ff"
58
- if low.startswith(("tool-", "command-", "api-")):
59
- return "#d93f0b"
60
- if low.startswith(("env-", "environment")):
61
- return "#bfdadc"
62
- return (fallback or "#ededed").strip() or "#ededed"
63
-
64
-
65
- def merge_gitcode_label_names(payload: dict[str, Any], cfg_labels: str) -> list[str]:
66
- """维度标签在前,其次 issue.labels,最后配置/环境附加标签;按大小写不敏感去重。"""
67
- issue = payload.get("issue") if isinstance(payload.get("issue"), dict) else {}
68
- merged: list[str] = []
69
- seen: set[str] = set()
70
- for group in (
71
- derive_gitcode_dimension_labels(payload),
72
- parse_labels_value(issue.get("labels")),
73
- parse_labels_value(cfg_labels),
74
- ):
75
- for name in group:
76
- t = sanitize_gitcode_label_value(name)
77
- if not t:
78
- continue
79
- k = t.casefold()
80
- if k in seen:
81
- continue
82
- seen.add(k)
83
- merged.append(t)
84
- return merged
85
-
86
-
87
- def _gitcode_request(
88
- req: urllib.request.Request,
89
- *,
90
- timeout: float,
91
- ) -> tuple[int, str]:
92
- try:
93
- with urllib.request.urlopen(req, timeout=timeout) as resp:
94
- raw = resp.read().decode("utf-8", errors="replace")
95
- code = int(getattr(resp, "status", 200) or 200)
96
- return code, raw
97
- except urllib.error.HTTPError as exc:
98
- err_body = ""
99
- try:
100
- err_body = exc.read().decode("utf-8", errors="replace")
101
- except OSError:
102
- pass
103
- raise GitCodeSinkError(
104
- f"GitCode 请求失败:HTTP {exc.code}",
105
- status_code=int(exc.code or 0),
106
- body=err_body,
107
- ) from exc
108
- except OSError as exc:
109
- raise GitCodeSinkError(f"GitCode 请求失败:{exc}") from exc
110
-
111
-
112
- def _list_repo_labels_casefold_map(cfg: GitCodeConfig) -> dict[str, str]:
113
- """casefold(label_name) -> 仓库中首次出现的规范名称。"""
114
- api = cfg.api_base.rstrip("/")
115
- owner = urllib.parse.quote(cfg.owner)
116
- repo = urllib.parse.quote(cfg.repo)
117
- out: dict[str, str] = {}
118
- page = 1
119
- while page <= 50:
120
- q = urllib.parse.urlencode(
121
- {"access_token": cfg.token, "page": str(page), "per_page": "100"},
122
- )
123
- url = f"{api}/api/v5/repos/{owner}/{repo}/labels?{q}"
124
- req = urllib.request.Request(url, method="GET")
125
- code, raw = _gitcode_request(req, timeout=45.0)
126
- if code >= 400:
127
- raise GitCodeSinkError(f"GitCode 获取标签列表失败:HTTP {code}", status_code=code, body=raw[:4096])
128
- try:
129
- data = json.loads(raw)
130
- except json.JSONDecodeError as exc:
131
- raise GitCodeSinkError("GitCode 标签列表响应不是合法 JSON") from exc
132
- if isinstance(data, list):
133
- items = data
134
- elif isinstance(data, dict):
135
- items = (
136
- data.get("list")
137
- or data.get("data")
138
- or data.get("labels")
139
- or data.get("result")
140
- or []
141
- )
142
- if not isinstance(items, list):
143
- items = []
144
- else:
145
- items = []
146
- if not items:
147
- break
148
- for it in items:
149
- if isinstance(it, dict):
150
- nm = it.get("name")
151
- if isinstance(nm, str) and nm.strip():
152
- name = nm.strip()
153
- cf = name.casefold()
154
- if cf not in out:
155
- out[cf] = name
156
- if len(items) < 100:
157
- break
158
- page += 1
159
- return out
160
-
161
-
162
- def _create_repo_label(cfg: GitCodeConfig, name: str) -> str:
163
- api = cfg.api_base.rstrip("/")
164
- owner = urllib.parse.quote(cfg.owner)
165
- repo = urllib.parse.quote(cfg.repo)
166
- q = urllib.parse.urlencode({"access_token": cfg.token})
167
- url = f"{api}/api/v5/repos/{owner}/{repo}/labels?{q}"
168
- form: dict[str, str] = {"name": name, "color": gitcode_label_color(name, cfg.new_label_color)}
169
- data = urllib.parse.urlencode(form).encode("utf-8")
170
- req = urllib.request.Request(url, data=data, method="POST")
171
- req.add_header("Content-Type", "application/x-www-form-urlencoded")
172
- req.add_header("Accept", "application/json")
173
- try:
174
- code, raw = _gitcode_request(req, timeout=30.0)
175
- except GitCodeSinkError as exc:
176
- body_l = (exc.body or "").lower()
177
- if exc.status_code in (400, 401, 403, 409, 422) and (
178
- "exist" in body_l or "已存在" in (exc.body or "") or "duplicate" in body_l
179
- ):
180
- return name
181
- raise
182
- if code >= 400:
183
- raise GitCodeSinkError(
184
- f"GitCode 新建标签失败:HTTP {code}({name})",
185
- status_code=code,
186
- body=raw[:4096],
187
- )
188
- try:
189
- obj = json.loads(raw)
190
- except json.JSONDecodeError:
191
- return name
192
- if isinstance(obj, dict):
193
- nm = obj.get("name")
194
- if isinstance(nm, str) and nm.strip():
195
- return nm.strip()
196
- return name
197
-
198
-
199
- def resolve_gitcode_issue_labels(cfg: GitCodeConfig, wanted: list[str]) -> list[str]:
200
- """按仓库已有标签解析名称;维护者模式下缺失则创建;否则缺失时报错。"""
201
- if not wanted:
202
- return []
203
- by_cf = _list_repo_labels_casefold_map(cfg)
204
- resolved: list[str] = []
205
- missing: list[str] = []
206
- for want in wanted:
207
- cf = want.casefold()
208
- if cf in by_cf:
209
- resolved.append(by_cf[cf])
210
- continue
211
- if cfg.label_maintainer:
212
- created = _create_repo_label(cfg, want)
213
- cfc = created.casefold()
214
- if cfc not in by_cf:
215
- by_cf[cfc] = created
216
- resolved.append(by_cf[cfc])
217
- else:
218
- missing.append(want)
219
- if missing:
220
- joined = ", ".join(missing)
221
- raise GitCodeSinkError(
222
- "GitCode 仓库中不存在下列标签,且当前未开启「维护者自动建标签」:"
223
- f"{joined}。"
224
- "请让仓库管理员预先创建这些标签,或在有 Maintainer 权限的机器人上设置环境变量 "
225
- "`OHAI_GITCODE_LABEL_MAINTAINER=1`(或 `--gitcode-label-maintainer`)。",
226
- )
227
- return resolved
228
-
229
-
230
- def _attach_issue_labels_json(cfg: GitCodeConfig, issue_number: int | str, label_names: list[str]) -> None:
231
- """Gitee/GitCode OpenAPI:创建 Issue 时 form 的 labels 常被忽略,须用 JSON 数组单独绑定。"""
232
- if not label_names:
233
- return
234
- num = str(issue_number).strip()
235
- if not num:
236
- raise GitCodeSinkError("GitCode 绑定标签失败:创建响应中缺少 Issue number")
237
- api = cfg.api_base.rstrip("/")
238
- owner = urllib.parse.quote(cfg.owner)
239
- repo = urllib.parse.quote(cfg.repo)
240
- q = urllib.parse.urlencode({"access_token": cfg.token})
241
- url = f"{api}/api/v5/repos/{owner}/{repo}/issues/{urllib.parse.quote(num, safe='')}/labels?{q}"
242
- payload = json.dumps(label_names, ensure_ascii=False).encode("utf-8")
243
- req = urllib.request.Request(url, data=payload, method="POST")
244
- req.add_header("Content-Type", "application/json; charset=utf-8")
245
- req.add_header("Accept", "application/json")
246
- code, raw = _gitcode_request(req, timeout=45.0)
247
- if code >= 400:
248
- raise GitCodeSinkError(
249
- f"GitCode 绑定 Issue 标签失败:HTTP {code}(Issue #{num})",
250
- status_code=code,
251
- body=raw[:4096],
252
- )
253
-
254
-
255
- @dataclass(frozen=True)
256
- class GitCodeConfig:
257
- owner: str
258
- repo: str
259
- token: str
260
- api_base: str = "https://api.gitcode.com"
261
- labels: str = ""
262
- label_maintainer: bool = False
263
- new_label_color: str = "#ededed"
264
-
265
- @classmethod
266
- def from_env(
267
- cls,
268
- *,
269
- owner: str | None = None,
270
- repo: str | None = None,
271
- token: str | None = None,
272
- api_base: str | None = None,
273
- labels: str | None = None,
274
- label_maintainer: bool | None = None,
275
- new_label_color: str | None = None,
276
- ) -> GitCodeConfig:
277
- env = os.environ
278
- o = (owner or env.get("OHAI_GITCODE_OWNER") or env.get("GITCODE_OWNER") or "").strip()
279
- if not o:
280
- o = (DEFAULT_GITCODE_OWNER or "").strip()
281
- r = (repo or env.get("OHAI_GITCODE_REPO") or env.get("GITCODE_REPO") or "").strip()
282
- if not r:
283
- r = (DEFAULT_GITCODE_REPO or "").strip()
284
- t = (token or env.get("OHAI_GITCODE_TOKEN") or env.get("GITCODE_ACCESS_TOKEN") or "").strip()
285
- base = (
286
- api_base
287
- or env.get("OHAI_GITCODE_API_BASE")
288
- or env.get("GITCODE_API_BASE")
289
- or "https://api.gitcode.com"
290
- ).strip().rstrip("/")
291
- lbl = labels if labels is not None else env.get("OHAI_GITCODE_LABELS", "") or ""
292
- if label_maintainer is not None:
293
- maint = bool(label_maintainer)
294
- else:
295
- maint = _truthy_env(env.get("OHAI_GITCODE_LABEL_MAINTAINER", "")) or _truthy_env(
296
- env.get("OHAI_GITCODE_MAINTAINER", ""),
297
- )
298
- color_raw = (
299
- (new_label_color or "").strip()
300
- if new_label_color is not None
301
- else (env.get("OHAI_GITCODE_NEW_LABEL_COLOR") or "").strip()
302
- )
303
- color = color_raw or "#ededed"
304
- if not o or not r or not t:
305
- raise GitCodeSinkError(
306
- "GitCode 缺少配置:请设置 Token 环境变量 OHAI_GITCODE_TOKEN(或 GITCODE_ACCESS_TOKEN);"
307
- "命名空间与仓库名请设置 OHAI_GITCODE_OWNER / OHAI_GITCODE_REPO,"
308
- "或在 ohai_report/gitcode_defaults.py 中填写 DEFAULT_GITCODE_OWNER / DEFAULT_GITCODE_REPO。"
309
- )
310
- return cls(
311
- owner=o,
312
- repo=r,
313
- token=t,
314
- api_base=base,
315
- labels=lbl.strip(),
316
- label_maintainer=maint,
317
- new_label_color=color,
318
- )
319
-
320
-
321
- class GitCodeIssueSink:
322
- def __init__(self, config: GitCodeConfig) -> None:
323
- self.config = config
324
-
325
- def save(self, payload: dict[str, Any]) -> SinkResult:
326
- issue = payload.get("issue") if isinstance(payload.get("issue"), dict) else {}
327
- title = str(issue.get("title") or "").strip()
328
- if not title:
329
- raise GitCodeSinkError("issue.title 不能为空")
330
-
331
- api = self.config.api_base.rstrip("/")
332
- endpoint = f"{api}/api/v5/repos/{urllib.parse.quote(self.config.owner)}/issues"
333
- q = urllib.parse.urlencode({"access_token": self.config.token})
334
-
335
- body = format_issue_markdown(payload)
336
- form: dict[str, str] = {
337
- "repo": self.config.repo,
338
- "title": title,
339
- "body": body,
340
- }
341
- merged = merge_gitcode_label_names(payload, self.config.labels)
342
- bound: list[str] = []
343
- if merged:
344
- bound = resolve_gitcode_issue_labels(self.config, merged)
345
-
346
- data = urllib.parse.urlencode(form).encode("utf-8")
347
- req = urllib.request.Request(f"{endpoint}?{q}", data=data, method="POST")
348
- req.add_header("Content-Type", "application/x-www-form-urlencoded")
349
- req.add_header("Accept", "application/json")
350
-
351
- try:
352
- with urllib.request.urlopen(req, timeout=60) as resp:
353
- raw = resp.read().decode("utf-8", errors="replace")
354
- code = getattr(resp, "status", 200)
355
- except urllib.error.HTTPError as exc:
356
- err_body = ""
357
- try:
358
- err_body = exc.read().decode("utf-8", errors="replace")
359
- except OSError:
360
- pass
361
- raise GitCodeSinkError(
362
- f"GitCode 创建 Issue 失败:HTTP {exc.code}",
363
- status_code=int(exc.code or 0),
364
- body=err_body,
365
- ) from exc
366
- except OSError as exc:
367
- raise GitCodeSinkError(f"GitCode 请求失败:{exc}") from exc
368
-
369
- try:
370
- obj = json.loads(raw)
371
- except json.JSONDecodeError as exc:
372
- raise GitCodeSinkError(f"GitCode 响应不是合法 JSON(HTTP {code})") from exc
373
-
374
- if not isinstance(obj, dict):
375
- raise GitCodeSinkError(f"GitCode 响应格式异常(HTTP {code})")
376
-
377
- html_url = str(obj.get("html_url") or "")
378
- number = obj.get("number")
379
- if bound:
380
- if number is None:
381
- raise GitCodeSinkError(
382
- "GitCode 已创建 Issue 但响应未包含 number,无法绑定标签;请检查 API 或联系平台支持。",
383
- )
384
- _attach_issue_labels_json(self.config, number, bound)
385
-
386
- return SinkResult(
387
- issue_id=str(payload.get("issue_id") or ""),
388
- path=None,
389
- url=html_url,
390
- extra={"number": number, "id": obj.get("id")},
391
- )
392
-
393
-
394
- def apply_gitcode_result(payload: dict[str, Any], result: SinkResult) -> dict[str, Any]:
395
- """Mutate payload with GitCode remote refs and matching status."""
396
- out = dict(payload)
397
- extra = getattr(result, "extra", {}) or {}
398
- num = extra.get("number")
399
- gid = extra.get("id")
400
- gc_block: dict[str, Any] = {"html_url": result.url, "number": num}
401
- if gid is not None:
402
- gc_block["id"] = gid
403
- out["gitcode"] = gc_block
404
- out["status"] = "created-gitcode"
405
- return out
@@ -1,21 +0,0 @@
1
- """Local JSON file sink."""
2
-
3
- from __future__ import annotations
4
-
5
- import pathlib
6
- from typing import Any
7
-
8
- from .base import SinkResult
9
- from ..workspace import data_dir, write_json
10
-
11
-
12
- class LocalIssueSink:
13
- def __init__(self, root: pathlib.Path) -> None:
14
- self.root = root
15
-
16
- def save(self, payload: dict[str, Any]) -> SinkResult:
17
- root_dir = data_dir(self.root)
18
- issue_path = root_dir / "issues" / f"{payload['issue_id']}.json"
19
- write_json(issue_path, payload)
20
- write_json(root_dir / "latest.json", payload)
21
- return SinkResult(issue_id=payload["issue_id"], path=issue_path)