Skip to content

Commit 40f12ca

Browse files
houfuclaude
andcommitted
Add --process-name flag to cowork for non-interactive in-session use
Adds find_cowork_session_by_process_name() helper and a --process-name option to the cowork command that skips the interactive picker entirely. From within a Cowork wrap-up step: claude-code-transcripts cowork --process-name "$(basename $PWD)" -a -o ~/Documents/Transcripts The processName is extracted from the virtual $PWD (/sessions/{processName}) that Cowork sets for running agents. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent ce34611 commit 40f12ca

2 files changed

Lines changed: 196 additions & 28 deletions

File tree

src/claude_code_transcripts/__init__.py

Lines changed: 104 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,63 @@ def find_cowork_sessions(base_dir=None, limit=10):
246246
return results[:limit]
247247

248248

249+
def find_cowork_session_by_process_name(process_name, base_dir=None):
250+
"""Find a single Cowork session by its processName.
251+
252+
Returns a session dict (title, jsonl_path, folders, mtime) or None if not found.
253+
Useful for non-interactive in-session use: pass basename of $PWD as process_name.
254+
"""
255+
if base_dir is None:
256+
base_dir = (
257+
Path.home()
258+
/ "Library"
259+
/ "Application Support"
260+
/ "Claude"
261+
/ "local-agent-mode-sessions"
262+
)
263+
base_dir = Path(base_dir)
264+
if not base_dir.exists():
265+
return None
266+
267+
for metadata_file in base_dir.glob("**/local_*.json"):
268+
if not metadata_file.is_file():
269+
continue
270+
try:
271+
metadata = json.loads(metadata_file.read_text(encoding="utf-8"))
272+
except (json.JSONDecodeError, OSError):
273+
continue
274+
275+
if metadata.get("processName") != process_name:
276+
continue
277+
278+
cli_session_id = metadata.get("cliSessionId", "")
279+
title = metadata.get("title") or metadata.get("initialMessage", "(untitled)")
280+
folders = metadata.get("userSelectedFolders", [])
281+
last_activity_at = metadata.get("lastActivityAt", 0)
282+
283+
stem = metadata_file.stem
284+
jsonl_path = (
285+
metadata_file.parent
286+
/ stem
287+
/ ".claude"
288+
/ "projects"
289+
/ f"-sessions-{process_name}"
290+
/ f"{cli_session_id}.jsonl"
291+
)
292+
293+
if not jsonl_path.exists():
294+
return None
295+
296+
return {
297+
"title": title,
298+
"jsonl_path": jsonl_path,
299+
"folders": folders,
300+
"mtime": last_activity_at / 1000,
301+
}
302+
303+
return None
304+
305+
249306
def get_project_display_name(folder_name):
250307
"""Convert encoded folder name to readable project name.
251308
@@ -1686,38 +1743,58 @@ def local_cmd(output, output_auto, repo, gist, include_json, open_browser, limit
16861743
default=10,
16871744
help="Maximum number of sessions to show (default: 10)",
16881745
)
1689-
def cowork_cmd(output, output_auto, gist, open_browser, limit):
1746+
@click.option(
1747+
"--process-name",
1748+
"process_name",
1749+
default=None,
1750+
help=(
1751+
"Process name to match (e.g. 'quirky-eager-fermat'). "
1752+
"Skips the interactive picker. "
1753+
'Use --process-name "$(basename $PWD)" from within a Cowork session.'
1754+
),
1755+
)
1756+
def cowork_cmd(output, output_auto, gist, open_browser, limit, process_name):
16901757
"""Select and convert a local Claude Cowork session to HTML."""
1691-
click.echo("Loading Cowork sessions...")
1692-
sessions = find_cowork_sessions(limit=limit)
1758+
if process_name:
1759+
# Non-interactive mode: find session by processName (for in-session use)
1760+
selected = find_cowork_session_by_process_name(process_name)
1761+
if selected is None:
1762+
click.echo(f"No Cowork session found with process name: {process_name}")
1763+
click.echo(
1764+
"Ensure ~/Library/Application Support/Claude/ is in your userSelectedFolders."
1765+
)
1766+
return
1767+
else:
1768+
click.echo("Loading Cowork sessions...")
1769+
sessions = find_cowork_sessions(limit=limit)
16931770

1694-
if not sessions:
1695-
click.echo("No Cowork sessions found.")
1696-
click.echo(
1697-
"Expected sessions in: ~/Library/Application Support/Claude/local-agent-mode-sessions/"
1698-
)
1699-
return
1771+
if not sessions:
1772+
click.echo("No Cowork sessions found.")
1773+
click.echo(
1774+
"Expected sessions in: ~/Library/Application Support/Claude/local-agent-mode-sessions/"
1775+
)
1776+
return
17001777

1701-
# Build choices for questionary
1702-
choices = []
1703-
for session in sessions:
1704-
mod_time = datetime.fromtimestamp(session["mtime"])
1705-
date_str = mod_time.strftime("%Y-%m-%d %H:%M")
1706-
title = session["title"]
1707-
if len(title) > 50:
1708-
title = title[:47] + "..."
1709-
folder = session["folders"][0] if session["folders"] else "(no folder)"
1710-
display = f"{title:50} {date_str} {folder}"
1711-
choices.append(questionary.Choice(title=display, value=session))
1778+
# Build choices for questionary
1779+
choices = []
1780+
for session in sessions:
1781+
mod_time = datetime.fromtimestamp(session["mtime"])
1782+
date_str = mod_time.strftime("%Y-%m-%d %H:%M")
1783+
title = session["title"]
1784+
if len(title) > 50:
1785+
title = title[:47] + "..."
1786+
folder = session["folders"][0] if session["folders"] else "(no folder)"
1787+
display = f"{title:50} {date_str} {folder}"
1788+
choices.append(questionary.Choice(title=display, value=session))
17121789

1713-
selected = questionary.select(
1714-
"Select a session to convert:",
1715-
choices=choices,
1716-
).ask()
1790+
selected = questionary.select(
1791+
"Select a session to convert:",
1792+
choices=choices,
1793+
).ask()
17171794

1718-
if selected is None:
1719-
click.echo("No session selected.")
1720-
return
1795+
if selected is None:
1796+
click.echo("No session selected.")
1797+
return
17211798

17221799
session_file = selected["jsonl_path"]
17231800

tests/test_cowork.py

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,16 @@
55

66
import pytest
77

8-
from claude_code_transcripts import find_cowork_sessions, parse_session_file
8+
from unittest.mock import patch
9+
10+
from click.testing import CliRunner
11+
12+
from claude_code_transcripts import (
13+
cli,
14+
find_cowork_session_by_process_name,
15+
find_cowork_sessions,
16+
parse_session_file,
17+
)
918

1019

1120
def make_cowork_session(
@@ -171,3 +180,85 @@ def test_cowork_jsonl_parses_with_queue_operation(tmp_path):
171180
assert loglines[1]["type"] == "assistant"
172181
# Content should be correct
173182
assert loglines[0]["message"]["content"] == "Hello cowork"
183+
184+
185+
def test_find_cowork_session_by_process_name_found(tmp_path):
186+
"""Finds the session matching a given processName."""
187+
make_cowork_session(
188+
tmp_path,
189+
process_name="quirky-eager-fermat",
190+
title="Target Session",
191+
)
192+
make_cowork_session(
193+
tmp_path,
194+
session_uuid="other-session",
195+
process_name="other-process",
196+
cli_session_id="other-cli",
197+
title="Other Session",
198+
)
199+
200+
result = find_cowork_session_by_process_name(
201+
"quirky-eager-fermat", base_dir=tmp_path
202+
)
203+
204+
assert result is not None
205+
assert result["title"] == "Target Session"
206+
assert result["jsonl_path"].exists()
207+
208+
209+
def test_find_cowork_session_by_process_name_not_found(tmp_path):
210+
"""Returns None when no session matches the processName."""
211+
make_cowork_session(tmp_path, process_name="some-other-process")
212+
213+
result = find_cowork_session_by_process_name("no-such-process", base_dir=tmp_path)
214+
215+
assert result is None
216+
217+
218+
def test_cowork_process_name_flag_converts_without_picker(tmp_path):
219+
"""--process-name skips the picker and converts the matching session."""
220+
_, jsonl_file = make_cowork_session(
221+
tmp_path,
222+
process_name="quirky-eager-fermat",
223+
cli_session_id="cli-abc",
224+
title="My Cowork Session",
225+
)
226+
output_dir = tmp_path / "output"
227+
228+
runner = CliRunner()
229+
with (
230+
patch(
231+
"claude_code_transcripts.find_cowork_session_by_process_name"
232+
) as mock_find,
233+
patch("claude_code_transcripts.generate_html") as mock_gen,
234+
):
235+
mock_find.return_value = {
236+
"title": "My Cowork Session",
237+
"jsonl_path": jsonl_file,
238+
"folders": [],
239+
"mtime": 1700000000.0,
240+
}
241+
result = runner.invoke(
242+
cli,
243+
["cowork", "--process-name", "quirky-eager-fermat", "-o", str(output_dir)],
244+
)
245+
246+
assert result.exit_code == 0
247+
mock_find.assert_called_once_with("quirky-eager-fermat")
248+
mock_gen.assert_called_once()
249+
250+
251+
def test_cowork_process_name_not_found_exits_cleanly(tmp_path):
252+
"""--process-name with no match exits with a clear message."""
253+
runner = CliRunner()
254+
with patch(
255+
"claude_code_transcripts.find_cowork_session_by_process_name"
256+
) as mock_find:
257+
mock_find.return_value = None
258+
result = runner.invoke(
259+
cli,
260+
["cowork", "--process-name", "no-such-process"],
261+
)
262+
263+
assert result.exit_code == 0
264+
assert "no-such-process" in result.output

0 commit comments

Comments
 (0)