From a196954f8ea4a705e3b825beb79096be68936622 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 12 Jul 2026 14:09:41 +0000 Subject: [PATCH 1/3] Derive capture document titles from conversation content instead of date /session-capture and /response-capture previously titled every saved document with a timestamp (e.g. 'Session 2026-06-26 17:53'). Both commands now compose a short descriptive title from the captured conversation, keeping the dated title only as a fallback when the exchange is too thin to name. Plugin metadata and the README are updated to match. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01CHsM2aZn1jyXcvk8qpbkig --- .claude-plugin/marketplace.json | 4 ++-- README.md | 9 ++++---- response-capture/.claude-plugin/plugin.json | 2 +- response-capture/commands/response-capture.md | 21 ++++++++----------- session-capture/.claude-plugin/plugin.json | 2 +- session-capture/commands/session-capture.md | 21 ++++++++----------- 6 files changed, 27 insertions(+), 32 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 41d52bb..7552333 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -8,14 +8,14 @@ { "name": "session-capture", "source": "./session-capture", - "description": "Save the entire Claude Code session transcript into a chosen Hillnote workspace as a dated document.", + "description": "Save the entire Claude Code session transcript into a chosen Hillnote workspace, titled after the conversation.", "version": "0.1.0", "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.", + "description": "Save the last user question and Claude's answer into a chosen Hillnote workspace, titled after the conversation.", "version": "0.1.0", "author": { "name": "Hillnote" } }, diff --git a/README.md b/README.md index 35d3711..9dc7471 100644 --- a/README.md +++ b/README.md @@ -19,8 +19,8 @@ Then install the plugin, by replacing 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. | @@ -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, diff --git a/response-capture/.claude-plugin/plugin.json b/response-capture/.claude-plugin/plugin.json index e3f835b..f05486d 100644 --- a/response-capture/.claude-plugin/plugin.json +++ b/response-capture/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "response-capture", - "description": "Save the last user question and Claude's answer into a chosen Hillnote workspace as a dated document.", + "description": "Save the last user question and Claude's answer into a chosen Hillnote workspace, titled after the conversation.", "version": "0.1.0", "author": { "name": "Hillnote", diff --git a/response-capture/commands/response-capture.md b/response-capture/commands/response-capture.md index 9e1aa40..ab02b7c 100644 --- a/response-capture/commands/response-capture.md +++ b/response-capture/commands/response-capture.md @@ -1,18 +1,10 @@ --- -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" @@ -20,6 +12,11 @@ Capture the most recent question-and-answer exchange from this Claude Code sessi 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. @@ -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`: `"💬"` diff --git a/session-capture/.claude-plugin/plugin.json b/session-capture/.claude-plugin/plugin.json index 00e6c37..a52176a 100644 --- a/session-capture/.claude-plugin/plugin.json +++ b/session-capture/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "session-capture", - "description": "Save the entire Claude Code session transcript into a chosen Hillnote workspace as a dated document.", + "description": "Save the entire Claude Code session transcript into a chosen Hillnote workspace, titled after the conversation.", "version": "0.1.0", "author": { "name": "Hillnote", diff --git a/session-capture/commands/session-capture.md b/session-capture/commands/session-capture.md index a226f39..00908f9 100644 --- a/session-capture/commands/session-capture.md +++ b/session-capture/commands/session-capture.md @@ -1,18 +1,10 @@ --- -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" @@ -20,6 +12,11 @@ Capture this entire Claude Code session and save it into a Hillnote workspace th 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. @@ -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`: `"🎪"` From c523d30f14ae555560f84b4bb10947d06f25b663 Mon Sep 17 00:00:00 2001 From: karthik rk Date: Mon, 13 Jul 2026 12:45:55 +0530 Subject: [PATCH 2/3] updated the scripts --- kanban-board/scripts/last_response_to_md.py | 16 +++++++++++++++- response-capture/scripts/last_response_to_md.py | 16 +++++++++++++++- session-capture/scripts/transcript_to_md.py | 16 +++++++++++++++- 3 files changed, 45 insertions(+), 3 deletions(-) diff --git a/kanban-board/scripts/last_response_to_md.py b/kanban-board/scripts/last_response_to_md.py index f6b993e..303782c 100644 --- a/kanban-board/scripts/last_response_to_md.py +++ b/kanban-board/scripts/last_response_to_md.py @@ -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 "" in body or "" 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 @@ -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": @@ -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": + "response-capture:response-capture\n" + "/response-capture:response-capture"}}, + {"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: @@ -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") diff --git a/response-capture/scripts/last_response_to_md.py b/response-capture/scripts/last_response_to_md.py index f6b993e..303782c 100644 --- a/response-capture/scripts/last_response_to_md.py +++ b/response-capture/scripts/last_response_to_md.py @@ -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 "" in body or "" 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 @@ -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": @@ -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": + "response-capture:response-capture\n" + "/response-capture:response-capture"}}, + {"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: @@ -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") diff --git a/session-capture/scripts/transcript_to_md.py b/session-capture/scripts/transcript_to_md.py index 1bd3590..7bb5502 100644 --- a/session-capture/scripts/transcript_to_md.py +++ b/session-capture/scripts/transcript_to_md.py @@ -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": @@ -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 "" in body or "" in body: + continue # slash-command trigger wrapper (command with a prompt body) speaker("user", "\U0001F464", "User") out.append(body) elif isinstance(content, list): @@ -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": + "session-capture:session-capture\n" + "/session-capture:session-capture"}}, + {"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: @@ -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 From 92b1c396d8b57f89eda78712fcc7561b72038913 Mon Sep 17 00:00:00 2001 From: karthik rk Date: Mon, 13 Jul 2026 12:50:48 +0530 Subject: [PATCH 3/3] Bumping up the versions of plugin --- .claude-plugin/marketplace.json | 4 ++-- response-capture/.claude-plugin/plugin.json | 2 +- session-capture/.claude-plugin/plugin.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 7552333..5163ccd 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -9,14 +9,14 @@ "name": "session-capture", "source": "./session-capture", "description": "Save the entire Claude Code session transcript into a chosen Hillnote workspace, titled after the conversation.", - "version": "0.1.0", + "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, titled after the conversation.", - "version": "0.1.0", + "version": "0.1.1", "author": { "name": "Hillnote" } }, { diff --git a/response-capture/.claude-plugin/plugin.json b/response-capture/.claude-plugin/plugin.json index f05486d..e9f3c87 100644 --- a/response-capture/.claude-plugin/plugin.json +++ b/response-capture/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "response-capture", "description": "Save the last user question and Claude's answer into a chosen Hillnote workspace, titled after the conversation.", - "version": "0.1.0", + "version": "0.1.1", "author": { "name": "Hillnote", "email": "karthik@colloqi.com" diff --git a/session-capture/.claude-plugin/plugin.json b/session-capture/.claude-plugin/plugin.json index a52176a..de3286c 100644 --- a/session-capture/.claude-plugin/plugin.json +++ b/session-capture/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "session-capture", "description": "Save the entire Claude Code session transcript into a chosen Hillnote workspace, titled after the conversation.", - "version": "0.1.0", + "version": "0.1.1", "author": { "name": "Hillnote", "email": "karthik@colloqi.com"