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
8 changes: 4 additions & 4 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@
{
"name": "session-capture",
"source": "./session-capture",
"description": "Save the entire Claude Code session transcript into a chosen Hillnote workspace as a dated document.",
"version": "0.1.0",
"description": "Save the entire Claude Code session transcript into a chosen Hillnote workspace, titled after the conversation.",
"version": "0.1.1",
"author": { "name": "Hillnote" }
},
{
"name": "response-capture",
"source": "./response-capture",
"description": "Save the last user question and Claude's answer into a chosen Hillnote workspace as a dated document.",
"version": "0.1.0",
"description": "Save the last user question and Claude's answer into a chosen Hillnote workspace, titled after the conversation.",
"version": "0.1.1",
"author": { "name": "Hillnote" }
},
{
Expand Down
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ Then install the plugin, by replacing <plugin-name> with appropriate one from th

| Plugin Name | Description |
| --- | --- |
| `session-capture` | Save the entire Claude Code session transcript into a chosen Hillnote workspace as a dated document. |
| `response-capture` | Save the last user question and Claude's answer into a chosen Hillnote workspace as a dated document. |
| `session-capture` | Save the entire Claude Code session transcript into a chosen Hillnote workspace, titled after the conversation. |
| `response-capture` | Save the last user question and Claude's answer into a chosen Hillnote workspace, titled after the conversation. |
| `kanban-board` | Parse the last Claude answer for TODO items and turn them into a kanban board in a chosen Hillnote workspace. |


Expand All @@ -35,8 +35,9 @@ server in your browser.

**session-capture** — run `/session-capture` in any Claude Code session. It
renders the full transcript to Markdown, lists your Hillnote workspaces so you
can pick one, and saves the session as a dated document under
`documents/claude-sessions/`.
can pick one, and saves the session under `documents/claude-sessions/` with a
title derived from what the conversation was about (falling back to a dated
title only when the session is too thin to name).

**response-capture** — run `/response-capture` to save just the last question you asked and
Claude's answer (skipping tool calls and thinking). Same workspace picker,
Expand Down
16 changes: 15 additions & 1 deletion kanban-board/scripts/last_response_to_md.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ def _user_text(content):
body = content.strip()
if not body or re.fullmatch(r"/[\w:.-]+", body):
return None # bare /command invocation, e.g. the trigger itself
if "<command-name>" in body or "<command-message>" in body:
return None # slash-command trigger wrapper (command with a prompt body)
return body
if isinstance(content, list):
parts = [b.get("text", "").strip() for b in content
Expand All @@ -65,6 +67,8 @@ def extract_last_qa(transcript_path):
t = entry.get("type")
if t not in ("user", "assistant"):
continue
if entry.get("isMeta"):
continue # system-injected turn, e.g. an expanded slash-command body
content = (entry.get("message") or {}).get("content")

if t == "user":
Expand Down Expand Up @@ -111,7 +115,14 @@ def _selfcheck():
{"type": "assistant", "message": {"role": "assistant", "content": [
{"type": "text", "text": "Part two."},
]}},
{"type": "user", "message": {"role": "user", "content": "/response-capture:response-capture"}},
# A slash-command-with-body is recorded as a wrapper string followed by
# an isMeta expansion of the command's prompt. Neither is a real turn.
{"type": "user", "message": {"role": "user", "content":
"<command-message>response-capture:response-capture</command-message>\n"
"<command-name>/response-capture:response-capture</command-name>"}},
{"type": "user", "isMeta": True, "message": {"role": "user", "content": [
{"type": "text", "text": "Capture the most recent question-and-answer exchange ..."},
]}},
]
with tempfile.NamedTemporaryFile("w", suffix=".jsonl", delete=False) as f:
for row in sample:
Expand All @@ -128,6 +139,9 @@ def _selfcheck():
# Tools / thinking omitted; the bare trigger is not treated as the question.
assert "Bash" not in md and "ponder" not in md, "noise leaked"
assert "/response-capture" not in md, "trigger treated as question"
# The slash-command wrapper and its isMeta body must not become the question.
assert "command-name" not in md, "command wrapper treated as question"
assert "Capture the most recent" not in md, "expanded command body leaked"
print("selfcheck OK")


Expand Down
4 changes: 2 additions & 2 deletions response-capture/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "response-capture",
"description": "Save the last user question and Claude's answer into a chosen Hillnote workspace as a dated document.",
"version": "0.1.0",
"description": "Save the last user question and Claude's answer into a chosen Hillnote workspace, titled after the conversation.",
"version": "0.1.1",
"author": {
"name": "Hillnote",
"email": "karthik@colloqi.com"
Expand Down
21 changes: 9 additions & 12 deletions response-capture/commands/response-capture.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,22 @@
---
description: Save the last user question and Claude's answer to a chosen Hillnote workspace as a dated document.
description: Save the last user question and Claude's answer to a chosen Hillnote workspace, titled after what was asked.
---

Capture the most recent question-and-answer exchange from this Claude Code session and save it into a Hillnote workspace the user picks. Follow these steps exactly.

1. **Build the document title.** Run:

```bash
date "+Q&A %Y-%m-%d %H:%M"
```

Use the output verbatim as the document title (e.g. `Q&A 2026-06-26 17:53`).

2. **Render the last Q&A to Markdown.** Run:
1. **Render the last Q&A to Markdown.** Run:

```bash
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/last_response_to_md.py"
```

The script reads `$CLAUDE_CODE_SESSION_ID`, finds this session's `.jsonl`, and prints the **last** user question and Claude's answer as Markdown to stdout. Earlier turns, tool calls, tool results, and thinking are omitted. Capture that stdout — it is the document body. If the script exits non‑zero, show its error and stop.

2. **Build the document title from the Q&A.** Read the rendered question and answer and compose a short, descriptive Title Case name — 3 to 8 words capturing what was asked or resolved (e.g. `Why Docker Builds Fail on ARM`, `Renaming a Git Branch Safely`). Rules:
- Derive it from the captured question/answer; do not invent detail that isn't there.
- Plain text only — no dates, emoji, quotes, slashes, or other punctuation that could upset a filename.
- Only if the exchange is too thin to name, fall back to a dated title by running `date "+Q&A %Y-%m-%d %H:%M"` and using its output verbatim.

3. **List Hillnote workspaces and let the user choose — interactively.** Call the Hillnote `list_workspaces` tool. Then present the choice with the `AskUserQuestion` tool (a clickable menu) instead of asking them to type — this keeps the turn alive so the command doesn't quit.

- Header: `Workspace`. Question: which workspace to save the Q&A into.
Expand All @@ -29,8 +26,8 @@ Capture the most recent question-and-answer exchange from this Claude Code sessi

4. **Save the document.** Call the Hillnote `add_document` tool with:
- `workspace`: the id of the workspace the user chose
- `title`: the title from step 1
- `content`: the Markdown body from step 2
- `title`: the title from step 2
- `content`: the Markdown body from step 1
- `folder`: `"claude-qa"` (saves under `documents/claude-qa/`)
- `tags`: `["claude-code-qa"]`
- `emoji`: `"💬"`
Expand Down
16 changes: 15 additions & 1 deletion response-capture/scripts/last_response_to_md.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ def _user_text(content):
body = content.strip()
if not body or re.fullmatch(r"/[\w:.-]+", body):
return None # bare /command invocation, e.g. the trigger itself
if "<command-name>" in body or "<command-message>" in body:
return None # slash-command trigger wrapper (command with a prompt body)
return body
if isinstance(content, list):
parts = [b.get("text", "").strip() for b in content
Expand All @@ -65,6 +67,8 @@ def extract_last_qa(transcript_path):
t = entry.get("type")
if t not in ("user", "assistant"):
continue
if entry.get("isMeta"):
continue # system-injected turn, e.g. an expanded slash-command body
content = (entry.get("message") or {}).get("content")

if t == "user":
Expand Down Expand Up @@ -111,7 +115,14 @@ def _selfcheck():
{"type": "assistant", "message": {"role": "assistant", "content": [
{"type": "text", "text": "Part two."},
]}},
{"type": "user", "message": {"role": "user", "content": "/response-capture:response-capture"}},
# A slash-command-with-body is recorded as a wrapper string followed by
# an isMeta expansion of the command's prompt. Neither is a real turn.
{"type": "user", "message": {"role": "user", "content":
"<command-message>response-capture:response-capture</command-message>\n"
"<command-name>/response-capture:response-capture</command-name>"}},
{"type": "user", "isMeta": True, "message": {"role": "user", "content": [
{"type": "text", "text": "Capture the most recent question-and-answer exchange ..."},
]}},
]
with tempfile.NamedTemporaryFile("w", suffix=".jsonl", delete=False) as f:
for row in sample:
Expand All @@ -128,6 +139,9 @@ def _selfcheck():
# Tools / thinking omitted; the bare trigger is not treated as the question.
assert "Bash" not in md and "ponder" not in md, "noise leaked"
assert "/response-capture" not in md, "trigger treated as question"
# The slash-command wrapper and its isMeta body must not become the question.
assert "command-name" not in md, "command wrapper treated as question"
assert "Capture the most recent" not in md, "expanded command body leaked"
print("selfcheck OK")


Expand Down
4 changes: 2 additions & 2 deletions session-capture/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "session-capture",
"description": "Save the entire Claude Code session transcript into a chosen Hillnote workspace as a dated document.",
"version": "0.1.0",
"description": "Save the entire Claude Code session transcript into a chosen Hillnote workspace, titled after the conversation.",
"version": "0.1.1",
"author": {
"name": "Hillnote",
"email": "karthik@colloqi.com"
Expand Down
21 changes: 9 additions & 12 deletions session-capture/commands/session-capture.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,22 @@
---
description: Save the entire current Claude Code session to a chosen Hillnote workspace as a dated document.
description: Save the entire current Claude Code session to a chosen Hillnote workspace, titled after what the session was about.
---

Capture this entire Claude Code session and save it into a Hillnote workspace the user picks. Follow these steps exactly.

1. **Build the document title.** Run:

```bash
date "+Session %Y-%m-%d %H:%M"
```

Use the output verbatim as the document title (e.g. `Session 2026-06-26 17:53`).

2. **Render the session transcript to Markdown.** Run:
1. **Render the session transcript to Markdown.** Run:

```bash
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/transcript_to_md.py"
```

The script reads `$CLAUDE_CODE_SESSION_ID`, finds this session's `.jsonl`, and prints the conversation — user messages and Claude's text replies — as Markdown to stdout. Tool calls, tool results, and thinking are omitted by default for a clean read; prefix `TRANSCRIPT_FULL=1` to the command to include them. Capture that stdout — it is the document body. If the script exits non‑zero, show its error and stop.

2. **Build the document title from the conversation.** Read the rendered transcript and compose a short, descriptive Title Case name — 3 to 8 words capturing the session's main topic or outcome (e.g. `Debugging Auth Token Refresh`, `Planning the Q3 Data Migration`). Rules:
- Derive it from what was actually discussed; do not invent detail that isn't in the transcript.
- Plain text only — no dates, emoji, quotes, slashes, or other punctuation that could upset a filename.
- Only if the conversation is too thin to name (e.g. a single trivial exchange), fall back to a dated title by running `date "+Session %Y-%m-%d %H:%M"` and using its output verbatim.

3. **List Hillnote workspaces and let the user choose — interactively.** Call the Hillnote `list_workspaces` tool. Then present the choice with the `AskUserQuestion` tool (a clickable menu) instead of asking them to type — this keeps the turn alive so the command doesn't quit.

- Header: `Workspace`. Question: which workspace to save the transcript into.
Expand All @@ -29,8 +26,8 @@ Capture this entire Claude Code session and save it into a Hillnote workspace th

4. **Save the document.** Call the Hillnote `add_document` tool with:
- `workspace`: the id of the workspace the user chose
- `title`: the title from step 1
- `content`: the Markdown body from step 2
- `title`: the title from step 2
- `content`: the Markdown body from step 1
- `folder`: `"claude-sessions"` (saves under `documents/claude-sessions/`)
- `tags`: `["claude-code-session"]`
- `emoji`: `"🎪"`
Expand Down
16 changes: 15 additions & 1 deletion session-capture/scripts/transcript_to_md.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ def speaker(name, emoji, label):
t = entry.get("type")
if t not in ("user", "assistant"):
continue # metadata lines (mode, attachment, ai-title, ...)
if entry.get("isMeta"):
continue # system-injected turn, e.g. an expanded slash-command body
content = (entry.get("message") or {}).get("content")

if t == "user":
Expand All @@ -91,6 +93,8 @@ def speaker(name, emoji, label):
# /session-capture trigger itself); keep prose that merely mentions one.
if re.fullmatch(r"/[\w:.-]+", body):
continue
if "<command-name>" in body or "<command-message>" in body:
continue # slash-command trigger wrapper (command with a prompt body)
speaker("user", "\U0001F464", "User")
out.append(body)
elif isinstance(content, list):
Expand Down Expand Up @@ -148,7 +152,14 @@ def _selfcheck():
{"type": "assistant", "message": {"role": "assistant", "content": [
{"type": "text", "text": "Done."},
]}},
{"type": "user", "message": {"role": "user", "content": "/session-capture:session-capture"}},
# A slash-command-with-body is recorded as a wrapper string followed by
# an isMeta expansion of the command's prompt. Neither is a real turn.
{"type": "user", "message": {"role": "user", "content":
"<command-message>session-capture:session-capture</command-message>\n"
"<command-name>/session-capture:session-capture</command-name>"}},
{"type": "user", "isMeta": True, "message": {"role": "user", "content": [
{"type": "text", "text": "Save the entire current Claude Code session ..."},
]}},
]
with tempfile.NamedTemporaryFile("w", suffix=".jsonl", delete=False) as f:
for row in sample:
Expand All @@ -169,6 +180,9 @@ def _selfcheck():
assert md.count("## \U0001F916 Assistant") == 1, "assistant header not collapsed"
# bare slash-command invocation is stripped from the saved doc
assert "/session-capture" not in md, "bare invocation not stripped"
# the slash-command wrapper and its isMeta expansion are stripped too
assert "command-name" not in md, "command wrapper leaked into saved doc"
assert "Save the entire current" not in md, "expanded command body leaked"

# Full mode: everything present.
assert "#### \U0001F527 Bash" in full and '"command": "ls"' in full
Expand Down
Loading