lightcone-cli 0.2.0__py3-none-any.whl

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 (46) hide show
  1. lightcone/cli/__init__.py +16 -0
  2. lightcone/cli/claude/lightcone/agents/lc-extractor.md +114 -0
  3. lightcone/cli/claude/lightcone/guides/astra-reference.md +290 -0
  4. lightcone/cli/claude/lightcone/guides/lightcone-cli-reference.md +75 -0
  5. lightcone/cli/claude/lightcone/guides/ui-brand.md +86 -0
  6. lightcone/cli/claude/lightcone/hooks/langfuse_git_commit_hook.py +303 -0
  7. lightcone/cli/claude/lightcone/hooks/langfuse_hook.py +894 -0
  8. lightcone/cli/claude/lightcone/hooks/langfuse_prepare_commit_msg.py +142 -0
  9. lightcone/cli/claude/lightcone/hooks/langfuse_session_init_hook.py +83 -0
  10. lightcone/cli/claude/lightcone/hooks/langfuse_utils.py +457 -0
  11. lightcone/cli/claude/lightcone/scripts/activate-venv.sh +44 -0
  12. lightcone/cli/claude/lightcone/scripts/check-lc-run.sh +140 -0
  13. lightcone/cli/claude/lightcone/scripts/session-start.sh +140 -0
  14. lightcone/cli/claude/lightcone/scripts/validate-on-save.sh +77 -0
  15. lightcone/cli/claude/lightcone/skills/lc-build/SKILL.md +92 -0
  16. lightcone/cli/claude/lightcone/skills/lc-build/assets/loop-prompt.md +92 -0
  17. lightcone/cli/claude/lightcone/skills/lc-build/scripts/setup-lc-build.sh +240 -0
  18. lightcone/cli/claude/lightcone/skills/lc-feedback/SKILL.md +94 -0
  19. lightcone/cli/claude/lightcone/skills/lc-migrate/SKILL.md +98 -0
  20. lightcone/cli/claude/lightcone/skills/lc-new/SKILL.md +183 -0
  21. lightcone/cli/claude/lightcone/skills/lc-verify/SKILL.md +53 -0
  22. lightcone/cli/claude/lightcone/templates/CLAUDE.md +32 -0
  23. lightcone/cli/commands.py +2327 -0
  24. lightcone/cli/plugin.py +34 -0
  25. lightcone/engine/__init__.py +42 -0
  26. lightcone/engine/assets.py +418 -0
  27. lightcone/engine/container.py +370 -0
  28. lightcone/engine/io_manager.py +27 -0
  29. lightcone/engine/runner.py +1017 -0
  30. lightcone/engine/site_registry.py +142 -0
  31. lightcone/engine/status.py +135 -0
  32. lightcone/engine/targets.py +68 -0
  33. lightcone/engine/tree.py +245 -0
  34. lightcone/eval/__init__.py +25 -0
  35. lightcone/eval/build.py +148 -0
  36. lightcone/eval/cli.py +176 -0
  37. lightcone/eval/graders.py +192 -0
  38. lightcone/eval/harness.py +265 -0
  39. lightcone/eval/models.py +117 -0
  40. lightcone/eval/report.py +214 -0
  41. lightcone/eval/sandbox.py +394 -0
  42. lightcone_cli-0.2.0.dist-info/METADATA +16 -0
  43. lightcone_cli-0.2.0.dist-info/RECORD +46 -0
  44. lightcone_cli-0.2.0.dist-info/WHEEL +4 -0
  45. lightcone_cli-0.2.0.dist-info/entry_points.txt +2 -0
  46. lightcone_cli-0.2.0.dist-info/licenses/LICENSE +29 -0
@@ -0,0 +1,303 @@
1
+ #!/usr/bin/env python3
2
+ # Copied from langfuse-cli (https://github.com/langfuse/langfuse-cli)
3
+ # Copyright (c) 2023-2026 Langfuse GmbH — MIT License
4
+ # See NOTICE file in the project root for full license text.
5
+ """
6
+ Claude Code PostToolUse hook for git commit detection.
7
+
8
+ Fires after Bash tool use, checks if a git commit occurred, and records
9
+ metadata in a trace manifest. Installed by langfuse-cli.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ import os
16
+ import re
17
+ import sys
18
+ import uuid
19
+ from datetime import datetime, timezone
20
+ from pathlib import Path
21
+ from typing import Any
22
+
23
+ try:
24
+ from langfuse_utils import (
25
+ atomic_write_json,
26
+ build_github_commit_url,
27
+ debug,
28
+ extract_session_id,
29
+ get_remote_url,
30
+ read_hook_payload,
31
+ read_last_trace,
32
+ resolve_repo_root,
33
+ run_git,
34
+ tracing_enabled,
35
+ write_trace_manifest,
36
+ )
37
+ except ImportError:
38
+ sys.exit(0)
39
+
40
+ # Detect git commit in simple and chained shell commands:
41
+ # - git commit -m "..."
42
+ # - cd repo && git commit
43
+ # - VAR=1 git -C repo commit
44
+ GIT_COMMIT_RE = re.compile(
45
+ r"(?:^|&&|\|\||;)\s*(?:[A-Za-z_][A-Za-z0-9_]*=\S+\s+)*git(?:\s+-C\s+\S+)?\s+commit(?:\s|$)"
46
+ )
47
+
48
+
49
+ def _to_int(value: Any) -> int | None:
50
+ if isinstance(value, bool):
51
+ return int(value)
52
+ if isinstance(value, int):
53
+ return value
54
+ if isinstance(value, str):
55
+ stripped = value.strip()
56
+ if stripped and stripped.lstrip("-").isdigit():
57
+ return int(stripped)
58
+ return None
59
+
60
+
61
+ def _command_succeeded(payload: dict) -> bool | None:
62
+ for key in (
63
+ "exit_code",
64
+ "exitCode",
65
+ "status",
66
+ "status_code",
67
+ "tool_exit_code",
68
+ "toolExitCode",
69
+ ):
70
+ if key not in payload:
71
+ continue
72
+ code = _to_int(payload.get(key))
73
+ if code is not None:
74
+ return code == 0
75
+
76
+ for key in ("success", "ok"):
77
+ if key in payload and isinstance(payload[key], bool):
78
+ return payload[key]
79
+
80
+ result = payload.get("tool_result")
81
+ if isinstance(result, dict):
82
+ return _command_succeeded(result)
83
+
84
+ return None
85
+
86
+
87
+ def _extract_tool_name(payload: dict) -> str:
88
+ value = payload.get("tool_name") or payload.get("toolName")
89
+ return value if isinstance(value, str) else ""
90
+
91
+
92
+ def _extract_command(payload: dict) -> str:
93
+ tool_input = payload.get("tool_input")
94
+ if not isinstance(tool_input, dict):
95
+ tool_input = payload.get("toolInput")
96
+
97
+ if isinstance(tool_input, dict):
98
+ command = tool_input.get("command")
99
+ if isinstance(command, str):
100
+ return command
101
+
102
+ command = payload.get("command")
103
+ if isinstance(command, str):
104
+ return command
105
+
106
+ return ""
107
+
108
+
109
+ def _looks_like_git_commit_command(command: str) -> bool:
110
+ return bool(GIT_COMMIT_RE.search(command))
111
+
112
+
113
+ def _find_repo_root(payload: dict) -> Path:
114
+ cwd = payload.get("cwd")
115
+ if not isinstance(cwd, str) or not cwd.strip():
116
+ cwd = os.getcwd()
117
+
118
+ root = resolve_repo_root(Path(cwd))
119
+ if root:
120
+ return root
121
+ return Path(cwd).expanduser().resolve()
122
+
123
+
124
+ def _head_changed_from_orig_head(repo_root: Path, head_sha: str) -> bool:
125
+ orig_head = run_git(repo_root, ["rev-parse", "ORIG_HEAD"])
126
+ if orig_head and orig_head == head_sha:
127
+ return False
128
+ return True
129
+
130
+
131
+ def _extract_host(trace_url: str | None) -> str | None:
132
+ if not trace_url or "://" not in trace_url:
133
+ return None
134
+ before_trace = trace_url.split("/trace/")[0]
135
+ return before_trace.rstrip("/") if before_trace else None
136
+
137
+
138
+ def _write_agent_trace_record(
139
+ repo_root: Path,
140
+ commit_sha: str,
141
+ trace_url: str | None,
142
+ session_id: str,
143
+ ) -> None:
144
+ try:
145
+ changed_files = run_git(repo_root, ["diff-tree", "--no-commit-id", "--name-only", "-r", commit_sha])
146
+ if not changed_files:
147
+ return
148
+
149
+ files = []
150
+ conversation_entry: dict[str, Any] = {
151
+ "contributor": {"type": "ai"},
152
+ "ranges": [{"start_line": 1, "end_line": 1}],
153
+ }
154
+ if trace_url:
155
+ conversation_entry["url"] = trace_url
156
+ related: list[dict[str, str]] = []
157
+ if trace_url:
158
+ related.append({"type": "trace", "url": trace_url})
159
+ if related:
160
+ conversation_entry["related"] = related
161
+
162
+ for fname in changed_files.strip().splitlines():
163
+ fname = fname.strip()
164
+ if not fname:
165
+ continue
166
+ fpath = repo_root / fname
167
+ line_count = 1
168
+ if fpath.is_file():
169
+ try:
170
+ with open(fpath, "rb") as fh:
171
+ line_count = max(1, sum(1 for _ in fh))
172
+ except Exception:
173
+ pass
174
+ conv = dict(conversation_entry)
175
+ conv["ranges"] = [{"start_line": 1, "end_line": line_count}]
176
+ files.append({"path": fname, "conversations": [conv]})
177
+
178
+ if not files:
179
+ return
180
+
181
+ record: dict[str, Any] = {
182
+ "version": "0.1.0",
183
+ "id": str(uuid.uuid4()),
184
+ "timestamp": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
185
+ "vcs": {"type": "git", "revision": commit_sha},
186
+ "tool": {"name": "claude-code"},
187
+ "files": files,
188
+ "metadata": {"sessionId": session_id},
189
+ }
190
+
191
+ traces_dir = repo_root / ".langfuse" / "traces"
192
+ record_path = traces_dir / f"agent-trace-{commit_sha[:12]}.json"
193
+ atomic_write_json(record_path, record)
194
+ debug(f"Wrote Agent Trace record to {record_path}")
195
+ except Exception as exc:
196
+ debug(f"_write_agent_trace_record failed: {exc}")
197
+
198
+
199
+ def main() -> int:
200
+ try:
201
+ if not tracing_enabled():
202
+ return 0
203
+
204
+ payload = read_hook_payload()
205
+ if _extract_tool_name(payload) != "Bash":
206
+ return 0
207
+
208
+ command = _extract_command(payload).strip()
209
+ if not command or not _looks_like_git_commit_command(command):
210
+ return 0
211
+
212
+ command_success = _command_succeeded(payload)
213
+ if command_success is False:
214
+ return 0
215
+
216
+ creds_ok = (
217
+ os.getenv("LANGFUSE_PUBLIC_KEY")
218
+ and os.getenv("LANGFUSE_SECRET_KEY")
219
+ )
220
+ if not creds_ok:
221
+ return 0
222
+
223
+ host = (
224
+ os.getenv("LANGFUSE_BASE_URL")
225
+ or os.getenv("LANGFUSE_HOST")
226
+ or "https://cloud.langfuse.com"
227
+ ).rstrip("/")
228
+
229
+ repo_root = _find_repo_root(payload)
230
+
231
+ session_id = extract_session_id(payload)
232
+
233
+ # Read last-trace with session validation to avoid cross-session confusion
234
+ session_data = read_last_trace(expected_session_id=session_id)
235
+
236
+ # Fallback: try per-repo session file (legacy)
237
+ if not session_data:
238
+ legacy_session_path = repo_root / ".langfuse" / "current-session.json"
239
+ if legacy_session_path.exists():
240
+ try:
241
+ data = json.loads(legacy_session_path.read_text(encoding="utf-8"))
242
+ if isinstance(data, dict) and data.get("trace_id"):
243
+ if not session_id or data.get("session_id") == session_id:
244
+ session_data = data
245
+ except Exception:
246
+ pass
247
+
248
+ if not session_data:
249
+ return 0
250
+
251
+ session_id = session_data.get("session_id", session_id or "")
252
+ trace_id = session_data.get("trace_id")
253
+ trace_url = session_data.get("trace_url")
254
+
255
+ if not isinstance(session_id, str) or not session_id:
256
+ return 0
257
+ if not isinstance(trace_id, str) or not trace_id:
258
+ return 0
259
+ if not isinstance(trace_url, str) or not trace_url:
260
+ trace_url = f"{host}/trace/{trace_id}"
261
+
262
+ commit_sha = run_git(repo_root, ["rev-parse", "HEAD"])
263
+ if not commit_sha:
264
+ return 0
265
+
266
+ if command_success is None and not _head_changed_from_orig_head(repo_root, commit_sha):
267
+ return 0
268
+
269
+ branch = run_git(repo_root, ["rev-parse", "--abbrev-ref", "HEAD"]) or "unknown"
270
+ commit_message = run_git(repo_root, ["log", "-1", "--pretty=%s"]) or ""
271
+
272
+ remote_url = get_remote_url(repo_root)
273
+ commit_url = build_github_commit_url(remote_url, commit_sha)
274
+
275
+ metadata = {
276
+ "commit_sha": commit_sha,
277
+ "commit_url": commit_url,
278
+ "commit_message": commit_message,
279
+ "branch": branch,
280
+ "remote_url": remote_url,
281
+ "session_id": session_id,
282
+ "source": "claude-code",
283
+ }
284
+
285
+ git_metadata = {
286
+ "git_commit_sha": commit_sha,
287
+ "git_commit_url": commit_url,
288
+ "git_remote_url": remote_url,
289
+ }
290
+ write_trace_manifest(repo_root, session_id, trace_id, _extract_host(trace_url) or host, git_metadata)
291
+
292
+ _write_agent_trace_record(
293
+ repo_root, commit_sha, trace_url, session_id,
294
+ )
295
+
296
+ return 0
297
+ except Exception as exc:
298
+ debug(str(exc))
299
+ return 0
300
+
301
+
302
+ if __name__ == "__main__":
303
+ raise SystemExit(main())