import json
import os
import sys
import urllib.request
TURNS = 3
AUTHOR = "claude-code"
FILE_ARGS = ("file_path", "notebook_path")
SYNTHETIC_PREFIXES = (
"<task-notification>",
"<command-name>",
"<command-message>",
"<local-command-",
"<system-reminder>",
"[Request interrupted",
)
def read_entries(path):
entries = []
with open(path, encoding="utf-8") as f:
for line in f:
try:
entries.append(json.loads(line))
except ValueError:
continue
return [e for e in entries if not e.get("isSidechain")]
def prompt_text(entry):
if entry.get("type") != "user" or entry.get("isMeta"):
return None
content = (entry.get("message") or {}).get("content")
if isinstance(content, str):
text = content
elif isinstance(content, list):
if any(isinstance(b, dict) and b.get("type") == "tool_result" for b in content):
return None
text = "\n".join(
b.get("text", "")
for b in content
if isinstance(b, dict) and b.get("type") == "text"
)
else:
return None
text = text.strip()
if not text or text.startswith(SYNTHETIC_PREFIXES):
return None
return text
def queued_prompt(entry):
if entry.get("type") != "attachment":
return None
att = entry.get("attachment") or {}
if att.get("type") != "queued_command" or att.get("commandMode") != "prompt":
return None
if (att.get("origin") or {}).get("kind") != "human":
return None
text = att.get("prompt")
if not isinstance(text, str) or not text.strip():
return None
return text.strip()
def blocks(entry, kind):
if entry.get("type") != "assistant":
return []
content = (entry.get("message") or {}).get("content")
if not isinstance(content, list):
return []
return [b for b in content if isinstance(b, dict) and b.get("type") == kind]
def turn_messages(prompt, entries, transcript_session):
messages = [
{
"turn_id": prompt["uuid"],
"role": "user",
"source_session": transcript_session,
"timestamp": prompt.get("timestamp"),
"text": prompt_text(prompt),
}
]
tools, files = [], []
final = [] for entry in entries:
queued = queued_prompt(entry)
if queued is not None and entry.get("uuid"):
messages.append(
{
"turn_id": entry["uuid"],
"role": "user",
"source_session": transcript_session,
"timestamp": entry.get("timestamp"),
"text": queued,
}
)
uses = blocks(entry, "tool_use")
for use in uses:
name = use.get("name") or ""
if name and name not in tools:
tools.append(name)
args = use.get("input") or {}
for key in FILE_ARGS:
path = args.get(key)
if isinstance(path, str) and path and path not in files:
files.append(path)
if uses:
final = []
texts = [b.get("text", "") for b in blocks(entry, "text")]
if any(t.strip() for t in texts):
final.append((entry, "\n".join(texts)))
if final:
first = final[0][0]
messages.append(
{
"turn_id": first["uuid"],
"role": "assistant",
"author": AUTHOR,
"source_session": transcript_session,
"timestamp": final[-1][0].get("timestamp"),
"text": "\n\n".join(t for _, t in final).strip(),
"tools": tools,
"files": files,
}
)
return messages
def recent_messages(entries, turns=TURNS):
starts = [i for i, e in enumerate(entries) if prompt_text(e) is not None]
messages = []
for n, start in enumerate(starts[-turns:]):
later = starts[-turns:][n + 1 :]
end = later[0] if later else len(entries)
prompt = entries[start]
messages.extend(
turn_messages(prompt, entries[start + 1 : end], prompt.get("sessionId"))
)
for m in messages:
for key in [k for k, v in m.items() if v is None]:
del m[key]
return messages
def send(url, body):
request = urllib.request.Request(
url.rstrip("/") + "/api/messages",
data=json.dumps(body).encode("utf-8"),
headers={"Content-Type": "application/json"},
method="POST",
)
token = os.environ.get("PC_AUTH_TOKEN")
if token:
request.add_header("Authorization", "Bearer " + token)
urllib.request.urlopen(request, timeout=30).read()
def detach():
if not hasattr(os, "fork"):
return False
if os.fork() > 0:
return True
os.setsid()
devnull = os.open(os.devnull, os.O_RDWR)
for fd in (0, 1, 2):
os.dup2(devnull, fd)
return False
def main():
args = [a for a in sys.argv[1:] if a != "--dry-run"]
dry_run = len(args) < len(sys.argv) - 1
if len(args) < 2:
return
session_id, url = args[0], args[1]
data = json.load(sys.stdin)
transcript = data.get("transcript_path")
if not transcript or not os.path.exists(transcript):
return
messages = recent_messages(read_entries(transcript))
if not messages:
return
body = {"session_id": session_id, "messages": messages}
if dry_run:
print(json.dumps(body, ensure_ascii=False, indent=2))
return
if detach():
return
try:
send(url, body)
finally:
if hasattr(os, "fork"):
os._exit(0)
if __name__ == "__main__":
try:
main()
except Exception as exc: print("pcx-capture: %s" % exc, file=sys.stderr)