Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"name": "docent",
"source": "./plugins/docent",
"description": "Docent AI analysis tools for Claude Code",
"version": "0.1.10",
"version": "0.2.1",
"author": {
"name": "TransluceAI"
},
Expand Down
2 changes: 1 addition & 1 deletion plugins/docent/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "docent",
"version": "0.1.10",
"version": "0.2.1",
"description": "Docent AI analysis tools"
}
2 changes: 1 addition & 1 deletion plugins/docent/.mcp.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"docent": {
"type": "stdio",
"command": "uv",
"args": ["tool", "run", "--from", "docent-python>=0.1.74", "docent-mcp"]
"args": ["tool", "run", "--from", "docent-python>=0.1.82", "docent-mcp"]
}
}
}
43 changes: 43 additions & 0 deletions plugins/docent/commands/logging.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
---
description: Configure Docent session logging — within the enabled rollout, sessions where Docent tools are used are shared by default; opt out, opt back in, or delete uploaded data here.
disable-model-invocation: true
allowed-tools: Bash(uv tool run --quiet --from 'docent-python>=0.1.82' python:*)
---

You are helping the user configure Docent session logging. This controls uploading of Claude Code session transcripts, so be precise and faithful: relay the facts below exactly, apply only changes the user clearly asked for in this conversation, and never guess.

## 1. Show the current state

Run the status command and present its output:

```
uv tool run --quiet --from 'docent-python>=0.1.82' python -m docent.plugin.logging_config status
```

## 2. Make sure the user knows what session sharing means

Before changing anything, relay these facts (a faithful paraphrase is fine, but keep them exact):

- Sessions in which a Docent MCP tool or skill was actually invoked are uploaded to **Transluce's Docent prod servers** to help improve Docent. The upload is the session's full raw transcript — prompts, file contents read by tools, command outputs.
- Sessions that never touch Docent are **not** uploaded. Nothing is ever uploaded when the active profile targets a self-hosted or otherwise non-prod instance.
- Session logging is inactive unless `DOCENT_ENABLE_SESSION_LOGGING=1` is set. Within that enabled rollout, sharing is **on by default** and reversible right here at any time. Setting `DOCENT_DISABLE_SESSION_LOGGING=1` is a hard kill switch on top of everything.
- Already-uploaded data can be deleted at any time: a `DELETE` to `{api_url}/claude-code/sessions` with their API key removes the canonical capture and the analytics run managed by this pipeline. Offer to run this if they ask for deletion.

## 3. Ask what they want

Ask which they'd like: opt out, opt back in, or delete already-uploaded data. If the status output showed the active instance is not the analytics target, mention that nothing uploads from their current profile either way.

## 4. Apply their choice

Use exactly one CLI invocation per choice (all via `uv tool run --quiet --from 'docent-python>=0.1.82' python -m docent.plugin.logging_config ...`):

- `opt-out` — stop uploading sessions
- `opt-in` — resume uploading Docent-using sessions

For deletion of already-uploaded data, send the `DELETE` request with their API key and report the response counts.

If the user reports that their sessions are not being uploaded or not appearing, run `doctor` and relay its full report — it checks every gate of the pipeline (binaries, state, connection, server, retry queue) and marks problems with `!!`.

## 5. Confirm

Re-run the `status` command and show the result so the user sees exactly what is now enabled.
15 changes: 15 additions & 0 deletions plugins/docent/hooks/hooks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"hooks": {
"SessionEnd": [
{
"hooks": [
{
"type": "command",
"command": "[ \"$DOCENT_ENABLE_SESSION_LOGGING\" = \"1\" ] && [ -z \"$DOCENT_DISABLE_SESSION_LOGGING\" ] && command -v uv >/dev/null 2>&1 && command -v python3 >/dev/null 2>&1 && python3 \"${CLAUDE_PLUGIN_ROOT}/hooks/session_end.py\" 2>/dev/null || true",
"timeout": 10
}
]
}
]
}
}
115 changes: 115 additions & 0 deletions plugins/docent/hooks/session_end.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
#!/usr/bin/env python3
"""SessionEnd hook: hand the session to a detached uploader, in milliseconds.

Claude Code runs SessionEnd hooks synchronously and may kill them almost
immediately when the CLI exits, so anything slow here either stalls the
user's exit or silently dies mid-upload. This script therefore does only
millisecond-scale work with the system python3 and no third-party imports:
re-check the opt-out gates, then spawn the real uploader as a fully detached
process (its own session on POSIX, detached process group on Windows, all
stdio on /dev/null) and exit. The hook returns before any package resolution
or network happens, and the detached uploader survives both the CLI exiting
and the terminal closing.

SessionEnd hook output is ignored but stderr is shown to the user, so every
path must stay silent and exit cleanly.
"""

from __future__ import annotations

import json
import os
import subprocess
import sys

UPLOADER_REQUIREMENT = "docent-python>=0.1.82"


def _spawn_detached(args: list[str]) -> None:
"""Start `args` so it is immune to the hook's death.

A new session (POSIX) or detached process group (Windows) keeps the
uploader out of the hook's process group, so killing the hook — which
Claude Code does on exit — cannot take the upload down with it. All
stdio on devnull so no inherited pipe keeps Claude Code waiting on us.
"""
if os.name == "nt":
flags = getattr(subprocess, "DETACHED_PROCESS", 0x00000008) | getattr(
subprocess, "CREATE_NEW_PROCESS_GROUP", 0x00000200
)
subprocess.Popen(
args,
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
close_fds=True,
creationflags=flags,
)
else:
subprocess.Popen(
args,
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
close_fds=True,
start_new_session=True,
)


def main() -> None:
try:
payload = json.loads(sys.stdin.read() or "{}")
except Exception:
return
if not isinstance(payload, dict):
return
if os.environ.get("DOCENT_ENABLE_SESSION_LOGGING") != "1" or os.environ.get(
"DOCENT_DISABLE_SESSION_LOGGING"
):
return
if not isinstance(payload.get("session_id"), str) or not isinstance(
payload.get("transcript_path"), str
):
return

home = os.environ.get("HOME") or os.environ.get("USERPROFILE")
if not home:
return
state_path = os.path.join(home, ".docent", "claude-code-logging.json")
try:
with open(state_path, encoding="utf-8") as f:
state = json.load(f)
except FileNotFoundError:
state = {}
except Exception:
return
if not isinstance(state, dict):
return
analytics = state.get("analytics")
choice = analytics.get("choice") if isinstance(analytics, dict) else None
if choice == "no":
return

plugin_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
_spawn_detached(
[
"uv",
"tool",
"run",
"--quiet",
"--from",
UPLOADER_REQUIREMENT,
"python",
"-m",
"docent.plugin.session_upload",
json.dumps(payload),
plugin_root,
]
)


if __name__ == "__main__":
try:
main()
except Exception:
pass
14 changes: 14 additions & 0 deletions plugins/docent/skills/docent/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ alwaysApply: true

# Docent

## Guides

This is the root skill for all Docent work. This file is just a table of contents. In most cases you should read one of the guides below before starting to work with docent. Choose the guide that best matches your task.

- For exploring a collection of agent runs, analyzing data, answering questions about agent behavior: `./analysis.md`
Expand All @@ -14,8 +16,20 @@ This is the root skill for all Docent work. This file is just a table of content

## Other available documentation

- For analysis-plan markdown notes (universal framework + pattern index): `./readings-reference.md` (`client.plan_markdown`)
- For writing or revising rubrics, classifier prompts, and their output schemas: `./rubric-writing.md`
- For plan-pattern pipelines and note templates (read one after classifying at Step 2b): `./patterns/`
- For the Readings API (`client.read`, `client.query`, batching, prompts, clustering): `./readings-reference.md`
- For DQL syntax, schemas, quirks, and example queries: `./dql-reference.md`
- For the reports API: `./report.md` (only if the user explicitly asks for a report)
- For ingestion-side data-model and conversion examples: `./ingestion-reference.md`
- SDK reference is available by visiting [our online documentation](https://docs.transluce.org/llms.txt)

## Opening Docent pages

Get the user in front of the relevant Docent page as soon as it exists — a new collection or a freshly submitted analysis plan.

- In local sessions running on the user's machine (e.g. Claude Code CLI or an IDE extension), the SDK's `flush()` / `webbrowser.open()` opens the user's default browser automatically. You can rely on this, but still surface the URL as a clickable link since the user may not notice the tab.
- In sandboxed sessions where `webbrowser.open()` cannot reach the user's browser (e.g. Codex CLI), surface the URL as a clickable link instead.

Failure to open a browser is not a Docent workflow failure.
Loading
Loading