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.
- lightcone/cli/__init__.py +16 -0
- lightcone/cli/claude/lightcone/agents/lc-extractor.md +114 -0
- lightcone/cli/claude/lightcone/guides/astra-reference.md +290 -0
- lightcone/cli/claude/lightcone/guides/lightcone-cli-reference.md +75 -0
- lightcone/cli/claude/lightcone/guides/ui-brand.md +86 -0
- lightcone/cli/claude/lightcone/hooks/langfuse_git_commit_hook.py +303 -0
- lightcone/cli/claude/lightcone/hooks/langfuse_hook.py +894 -0
- lightcone/cli/claude/lightcone/hooks/langfuse_prepare_commit_msg.py +142 -0
- lightcone/cli/claude/lightcone/hooks/langfuse_session_init_hook.py +83 -0
- lightcone/cli/claude/lightcone/hooks/langfuse_utils.py +457 -0
- lightcone/cli/claude/lightcone/scripts/activate-venv.sh +44 -0
- lightcone/cli/claude/lightcone/scripts/check-lc-run.sh +140 -0
- lightcone/cli/claude/lightcone/scripts/session-start.sh +140 -0
- lightcone/cli/claude/lightcone/scripts/validate-on-save.sh +77 -0
- lightcone/cli/claude/lightcone/skills/lc-build/SKILL.md +92 -0
- lightcone/cli/claude/lightcone/skills/lc-build/assets/loop-prompt.md +92 -0
- lightcone/cli/claude/lightcone/skills/lc-build/scripts/setup-lc-build.sh +240 -0
- lightcone/cli/claude/lightcone/skills/lc-feedback/SKILL.md +94 -0
- lightcone/cli/claude/lightcone/skills/lc-migrate/SKILL.md +98 -0
- lightcone/cli/claude/lightcone/skills/lc-new/SKILL.md +183 -0
- lightcone/cli/claude/lightcone/skills/lc-verify/SKILL.md +53 -0
- lightcone/cli/claude/lightcone/templates/CLAUDE.md +32 -0
- lightcone/cli/commands.py +2327 -0
- lightcone/cli/plugin.py +34 -0
- lightcone/engine/__init__.py +42 -0
- lightcone/engine/assets.py +418 -0
- lightcone/engine/container.py +370 -0
- lightcone/engine/io_manager.py +27 -0
- lightcone/engine/runner.py +1017 -0
- lightcone/engine/site_registry.py +142 -0
- lightcone/engine/status.py +135 -0
- lightcone/engine/targets.py +68 -0
- lightcone/engine/tree.py +245 -0
- lightcone/eval/__init__.py +25 -0
- lightcone/eval/build.py +148 -0
- lightcone/eval/cli.py +176 -0
- lightcone/eval/graders.py +192 -0
- lightcone/eval/harness.py +265 -0
- lightcone/eval/models.py +117 -0
- lightcone/eval/report.py +214 -0
- lightcone/eval/sandbox.py +394 -0
- lightcone_cli-0.2.0.dist-info/METADATA +16 -0
- lightcone_cli-0.2.0.dist-info/RECORD +46 -0
- lightcone_cli-0.2.0.dist-info/WHEEL +4 -0
- lightcone_cli-0.2.0.dist-info/entry_points.txt +2 -0
- 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())
|