panagent 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
panagent-0.3.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 panagent contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,167 @@
1
+ Metadata-Version: 2.4
2
+ Name: panagent
3
+ Version: 0.3.0
4
+ Summary: Move AI conversations between Claude Code, Codex, ChatGPT, Claude and tavya, and resume them natively
5
+ Author: Abhimanyu Pallavi Sudhir
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/abhimanyupallavisudhir/panagent
8
+ Project-URL: Source, https://github.com/abhimanyupallavisudhir/panagent
9
+ Project-URL: Issues, https://github.com/abhimanyupallavisudhir/panagent/issues
10
+ Project-URL: Changelog, https://github.com/abhimanyupallavisudhir/panagent/blob/master/CHANGELOG.md
11
+ Keywords: agents,claude-code,codex,chatgpt,claude,tavya,conversation,transcript,export
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
23
+ Classifier: Topic :: Communications :: Chat
24
+ Classifier: Topic :: Software Development
25
+ Classifier: Topic :: Utilities
26
+ Classifier: Typing :: Typed
27
+ Requires-Python: >=3.10
28
+ Description-Content-Type: text/markdown
29
+ License-File: LICENSE
30
+ Provides-Extra: browser
31
+ Requires-Dist: playwright>=1.50; extra == "browser"
32
+ Dynamic: license-file
33
+
34
+ # panagent
35
+
36
+ **Take an AI conversation anywhere.** Turn a ChatGPT, Claude or tavya share link, a ChatGPT or Claude data export, or a Claude Code or Codex session into a session that Claude Code or Codex can resume. One command does it:
37
+
38
+ ```console
39
+ $ panagent convert https://chatgpt.com/share/6a781aea-… --to claude --install
40
+ cd /home/me/project && claude --resume 0b6d1c1e-5f0e-4d43-9a8e-2f8f5e0c7a11
41
+ ```
42
+
43
+ panagent converts through a small provider-neutral format. It keeps source provenance and tool-call IDs where the source has them. When something cannot be carried across, panagent reports it instead of pretending the conversion is exact. It has no dependencies beyond the Python standard library.
44
+
45
+ | From ↓ / to → | Claude Code | Codex | Neutral JSON | Markdown |
46
+ | --- | :---: | :---: | :---: | :---: |
47
+ | Claude Code session (`~/.claude/projects/…/*.jsonl`) | ✓ | ✓ | ✓ | ✓ |
48
+ | Codex session (`~/.codex/sessions/…/rollout-*.jsonl`) | ✓ | ✓ | ✓ | ✓ |
49
+ | ChatGPT share link or data export (`conversations.json`) | ✓ | ✓ | ✓ | ✓ |
50
+ | Claude share link or data export (`conversations.json`) | ✓ | ✓ | ✓ | ✓ |
51
+ | tavya share link (`tavya.io/share/conversations/…`, or any karmax server) | ✓ | ✓ | ✓ | ✓ |
52
+
53
+ ## Install
54
+
55
+ ```bash
56
+ pipx install panagent # or: uv tool install panagent, or: pip install panagent
57
+ ```
58
+
59
+ Python 3.10 or later is required. ChatGPT and tavya links need nothing else. Claude share pages sometimes need a real browser (see [Claude share links](#claude-share-links)):
60
+
61
+ ```bash
62
+ pipx install 'panagent[browser]' && playwright install chromium
63
+ ```
64
+
65
+ ## Use
66
+
67
+ ### Continue a conversation in Claude Code or Codex
68
+
69
+ `--install` writes the converted session into the CLI's own history and prints the command that resumes it:
70
+
71
+ ```bash
72
+ panagent convert 'https://claude.ai/share/…' --to claude --install # resume in Claude Code
73
+ panagent convert 'https://tavya.io/share/conversations/…' --to codex --install
74
+ panagent convert ~/.claude/projects/-home-me-app/1f0c….jsonl --to codex --install
75
+ ```
76
+
77
+ Claude Code keeps sessions per project directory. `--install` uses the current directory; pass `--cwd DIR` to choose another one. Each install creates a new session ID, and an existing session file is never overwritten. `$CLAUDE_CONFIG_DIR` and `$CODEX_HOME` are honored.
78
+
79
+ ### Convert to a file
80
+
81
+ The source format is detected from the URL or the file's contents. Output goes to stdout unless you pass `-o`.
82
+
83
+ ```bash
84
+ panagent convert codex-session.jsonl --to claude-code -o claude-session.jsonl
85
+ panagent convert 'https://chatgpt.com/share/…' --to markdown -o chat.md
86
+ panagent convert session.jsonl --to ir -o conversation.agent.json # neutral JSON
87
+ ```
88
+
89
+ ### Data exports
90
+
91
+ ChatGPT (*Settings → Data controls → Export*) and Claude (*Settings → Privacy → Export data*) both email you a `conversations.json` with every conversation. List the conversations, then pick one by ID or exact title:
92
+
93
+ ```bash
94
+ panagent list conversations.json
95
+ panagent convert conversations.json --conversation 'Plot the data' --to claude --install
96
+ ```
97
+
98
+ Claude exports keep text extracted from uploaded files, so attachments are carried across as text. ChatGPT exports refer to images that the export does not contain; these become named attachment placeholders.
99
+
100
+ ### From Python
101
+
102
+ ```python
103
+ import panagent
104
+
105
+ conversation = panagent.load("https://tavya.io/share/conversations/…") # or a path
106
+ print(panagent.render(conversation, "markdown").text)
107
+ session = panagent.install(conversation, "codex")
108
+ print(session.command) # codex resume …
109
+ ```
110
+
111
+ `panagent.parse(text)` reads a string you already have. Each function raises a `panagent.PanagentError` with an actionable message. Library calls never start a browser unless you pass `browser="auto"`, `"headed"` or `"headless"`.
112
+
113
+ ## How web chats arrive
114
+
115
+ A web chat was never an agent session. It has no tool state, sandbox or hidden instructions to resume. By default (`--mode auto`), Claude Code and Codex therefore receive a share or an export as **one guarded context message**. That message says the imported text is prior discussion and does not override the agent's current instructions. Agent sessions keep their turn-by-turn structure, including tool calls and results.
116
+
117
+ Pass `--mode transcript` to rebuild a web chat turn by turn, or `--mode context` to flatten an agent session. Markdown and neutral JSON always keep every message.
118
+
119
+ ## Provenance and warnings
120
+
121
+ The neutral format ([reference](https://github.com/abhimanyupallavisudhir/panagent/blob/master/docs/ir.md)) records:
122
+
123
+ - where the conversation came from: format, provider, source kind, URL or file name, conversation ID and acquisition time;
124
+ - the source ID and record index of every message;
125
+ - ordered, typed blocks: text, code, visible reasoning, tool calls and results, images and attachments;
126
+ - what the source could represent and what it could not;
127
+ - warnings with stable codes.
128
+
129
+ Generated Claude Code and Codex files embed the same provenance under a `panagent` field, so a session converted twice still remembers its original source. Warnings go to stderr. `--report FILE` writes them as JSON, and `--fail-on-warning` exits with status 3 for strict automation. Use `panagent validate FILE` to check that a file parses without converting it.
130
+
131
+ ## Claude share links
132
+
133
+ Claude share pages may show a Cloudflare challenge, or an app shell that only renders in a browser. panagent never treats either one as an empty conversation. Plain HTTPS is tried first. With the `browser` extra, `--browser auto` falls back to a Chrome window where you complete the challenge yourself. Two alternatives:
134
+
135
+ - `--browser headed` opens a browser window straight away;
136
+ - `--cdp-url http://127.0.0.1:9222` reuses a Chrome you already have open.
137
+
138
+ Non-interactive runs fail with the command to run instead of hanging, and `--browser never` forbids the fallback. panagent never asks for cookies or tokens and never automates a challenge. If browser extraction stops recognizing Claude's page, the [manual export recipe](https://github.com/abhimanyupallavisudhir/panagent/blob/master/docs/browser-export.md) still works.
139
+
140
+ ## Limits
141
+
142
+ The native Claude Code and Codex formats are undocumented and change over time. Opt-in compatibility tests check the output against the real CLIs (see [Development](#development)): Codex must read and resume a session written by `--install`, and Claude Code must resume one and send its history to a local stand-in for the API, so the test spends nothing. Provider-only state cannot be recreated: sandboxes, approvals, file snapshots, encrypted reasoning, token accounting and compaction state.
143
+
144
+ panagent converts conversations only. It does not migrate credentials, MCP servers, hooks, plugins, repositories or running processes, and it does not bypass access controls. Anything it cannot carry over is reported as a warning.
145
+
146
+ ## Development
147
+
148
+ ```bash
149
+ PYTHONPATH=src python -m unittest discover -s tests -v
150
+ ```
151
+
152
+ The fixtures are redacted, synthetic copies of each source format. The tavya fixture is the output of tavya's own share renderer. Optional suites:
153
+
154
+ ```bash
155
+ PANAGENT_LIVE_TESTS=1 PYTHONPATH=src python -m unittest tests.test_live_samples -v # public sample links
156
+ PANAGENT_NATIVE_TESTS=1 PYTHONPATH=src python -m unittest tests.test_native_cli -v # installed codex
157
+ PANAGENT_CLAUDE_TESTS=1 PANAGENT_CLAUDE_COMMAND=claude \
158
+ PYTHONPATH=src python -m unittest tests.test_native_cli.ClaudeNativeCompatibilityTests -v
159
+ ```
160
+
161
+ When a sibling `../karmax` checkout has its dependencies installed, the default suite also runs the Codex release that checkout pins. Point `PANAGENT_PINNED_CODEX` at another binary to use that one instead.
162
+
163
+ To make a release, bump `__version__` in `src/panagent/__init__.py` and add a `CHANGELOG.md` entry. Merging that to master is the release. GitHub Actions tests and builds it, publishes it to PyPI through trusted publishing (no token is stored), and tags it with a GitHub release.
164
+
165
+ ## License
166
+
167
+ MIT
@@ -0,0 +1,134 @@
1
+ # panagent
2
+
3
+ **Take an AI conversation anywhere.** Turn a ChatGPT, Claude or tavya share link, a ChatGPT or Claude data export, or a Claude Code or Codex session into a session that Claude Code or Codex can resume. One command does it:
4
+
5
+ ```console
6
+ $ panagent convert https://chatgpt.com/share/6a781aea-… --to claude --install
7
+ cd /home/me/project && claude --resume 0b6d1c1e-5f0e-4d43-9a8e-2f8f5e0c7a11
8
+ ```
9
+
10
+ panagent converts through a small provider-neutral format. It keeps source provenance and tool-call IDs where the source has them. When something cannot be carried across, panagent reports it instead of pretending the conversion is exact. It has no dependencies beyond the Python standard library.
11
+
12
+ | From ↓ / to → | Claude Code | Codex | Neutral JSON | Markdown |
13
+ | --- | :---: | :---: | :---: | :---: |
14
+ | Claude Code session (`~/.claude/projects/…/*.jsonl`) | ✓ | ✓ | ✓ | ✓ |
15
+ | Codex session (`~/.codex/sessions/…/rollout-*.jsonl`) | ✓ | ✓ | ✓ | ✓ |
16
+ | ChatGPT share link or data export (`conversations.json`) | ✓ | ✓ | ✓ | ✓ |
17
+ | Claude share link or data export (`conversations.json`) | ✓ | ✓ | ✓ | ✓ |
18
+ | tavya share link (`tavya.io/share/conversations/…`, or any karmax server) | ✓ | ✓ | ✓ | ✓ |
19
+
20
+ ## Install
21
+
22
+ ```bash
23
+ pipx install panagent # or: uv tool install panagent, or: pip install panagent
24
+ ```
25
+
26
+ Python 3.10 or later is required. ChatGPT and tavya links need nothing else. Claude share pages sometimes need a real browser (see [Claude share links](#claude-share-links)):
27
+
28
+ ```bash
29
+ pipx install 'panagent[browser]' && playwright install chromium
30
+ ```
31
+
32
+ ## Use
33
+
34
+ ### Continue a conversation in Claude Code or Codex
35
+
36
+ `--install` writes the converted session into the CLI's own history and prints the command that resumes it:
37
+
38
+ ```bash
39
+ panagent convert 'https://claude.ai/share/…' --to claude --install # resume in Claude Code
40
+ panagent convert 'https://tavya.io/share/conversations/…' --to codex --install
41
+ panagent convert ~/.claude/projects/-home-me-app/1f0c….jsonl --to codex --install
42
+ ```
43
+
44
+ Claude Code keeps sessions per project directory. `--install` uses the current directory; pass `--cwd DIR` to choose another one. Each install creates a new session ID, and an existing session file is never overwritten. `$CLAUDE_CONFIG_DIR` and `$CODEX_HOME` are honored.
45
+
46
+ ### Convert to a file
47
+
48
+ The source format is detected from the URL or the file's contents. Output goes to stdout unless you pass `-o`.
49
+
50
+ ```bash
51
+ panagent convert codex-session.jsonl --to claude-code -o claude-session.jsonl
52
+ panagent convert 'https://chatgpt.com/share/…' --to markdown -o chat.md
53
+ panagent convert session.jsonl --to ir -o conversation.agent.json # neutral JSON
54
+ ```
55
+
56
+ ### Data exports
57
+
58
+ ChatGPT (*Settings → Data controls → Export*) and Claude (*Settings → Privacy → Export data*) both email you a `conversations.json` with every conversation. List the conversations, then pick one by ID or exact title:
59
+
60
+ ```bash
61
+ panagent list conversations.json
62
+ panagent convert conversations.json --conversation 'Plot the data' --to claude --install
63
+ ```
64
+
65
+ Claude exports keep text extracted from uploaded files, so attachments are carried across as text. ChatGPT exports refer to images that the export does not contain; these become named attachment placeholders.
66
+
67
+ ### From Python
68
+
69
+ ```python
70
+ import panagent
71
+
72
+ conversation = panagent.load("https://tavya.io/share/conversations/…") # or a path
73
+ print(panagent.render(conversation, "markdown").text)
74
+ session = panagent.install(conversation, "codex")
75
+ print(session.command) # codex resume …
76
+ ```
77
+
78
+ `panagent.parse(text)` reads a string you already have. Each function raises a `panagent.PanagentError` with an actionable message. Library calls never start a browser unless you pass `browser="auto"`, `"headed"` or `"headless"`.
79
+
80
+ ## How web chats arrive
81
+
82
+ A web chat was never an agent session. It has no tool state, sandbox or hidden instructions to resume. By default (`--mode auto`), Claude Code and Codex therefore receive a share or an export as **one guarded context message**. That message says the imported text is prior discussion and does not override the agent's current instructions. Agent sessions keep their turn-by-turn structure, including tool calls and results.
83
+
84
+ Pass `--mode transcript` to rebuild a web chat turn by turn, or `--mode context` to flatten an agent session. Markdown and neutral JSON always keep every message.
85
+
86
+ ## Provenance and warnings
87
+
88
+ The neutral format ([reference](https://github.com/abhimanyupallavisudhir/panagent/blob/master/docs/ir.md)) records:
89
+
90
+ - where the conversation came from: format, provider, source kind, URL or file name, conversation ID and acquisition time;
91
+ - the source ID and record index of every message;
92
+ - ordered, typed blocks: text, code, visible reasoning, tool calls and results, images and attachments;
93
+ - what the source could represent and what it could not;
94
+ - warnings with stable codes.
95
+
96
+ Generated Claude Code and Codex files embed the same provenance under a `panagent` field, so a session converted twice still remembers its original source. Warnings go to stderr. `--report FILE` writes them as JSON, and `--fail-on-warning` exits with status 3 for strict automation. Use `panagent validate FILE` to check that a file parses without converting it.
97
+
98
+ ## Claude share links
99
+
100
+ Claude share pages may show a Cloudflare challenge, or an app shell that only renders in a browser. panagent never treats either one as an empty conversation. Plain HTTPS is tried first. With the `browser` extra, `--browser auto` falls back to a Chrome window where you complete the challenge yourself. Two alternatives:
101
+
102
+ - `--browser headed` opens a browser window straight away;
103
+ - `--cdp-url http://127.0.0.1:9222` reuses a Chrome you already have open.
104
+
105
+ Non-interactive runs fail with the command to run instead of hanging, and `--browser never` forbids the fallback. panagent never asks for cookies or tokens and never automates a challenge. If browser extraction stops recognizing Claude's page, the [manual export recipe](https://github.com/abhimanyupallavisudhir/panagent/blob/master/docs/browser-export.md) still works.
106
+
107
+ ## Limits
108
+
109
+ The native Claude Code and Codex formats are undocumented and change over time. Opt-in compatibility tests check the output against the real CLIs (see [Development](#development)): Codex must read and resume a session written by `--install`, and Claude Code must resume one and send its history to a local stand-in for the API, so the test spends nothing. Provider-only state cannot be recreated: sandboxes, approvals, file snapshots, encrypted reasoning, token accounting and compaction state.
110
+
111
+ panagent converts conversations only. It does not migrate credentials, MCP servers, hooks, plugins, repositories or running processes, and it does not bypass access controls. Anything it cannot carry over is reported as a warning.
112
+
113
+ ## Development
114
+
115
+ ```bash
116
+ PYTHONPATH=src python -m unittest discover -s tests -v
117
+ ```
118
+
119
+ The fixtures are redacted, synthetic copies of each source format. The tavya fixture is the output of tavya's own share renderer. Optional suites:
120
+
121
+ ```bash
122
+ PANAGENT_LIVE_TESTS=1 PYTHONPATH=src python -m unittest tests.test_live_samples -v # public sample links
123
+ PANAGENT_NATIVE_TESTS=1 PYTHONPATH=src python -m unittest tests.test_native_cli -v # installed codex
124
+ PANAGENT_CLAUDE_TESTS=1 PANAGENT_CLAUDE_COMMAND=claude \
125
+ PYTHONPATH=src python -m unittest tests.test_native_cli.ClaudeNativeCompatibilityTests -v
126
+ ```
127
+
128
+ When a sibling `../karmax` checkout has its dependencies installed, the default suite also runs the Codex release that checkout pins. Point `PANAGENT_PINNED_CODEX` at another binary to use that one instead.
129
+
130
+ To make a release, bump `__version__` in `src/panagent/__init__.py` and add a `CHANGELOG.md` entry. Merging that to master is the release. GitHub Actions tests and builds it, publishes it to PyPI through trusted publishing (no token is stored), and tags it with a GitHub release.
131
+
132
+ ## License
133
+
134
+ MIT
@@ -0,0 +1,53 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "panagent"
7
+ dynamic = ["version"]
8
+ description = "Move AI conversations between Claude Code, Codex, ChatGPT, Claude and tavya, and resume them natively"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "Abhimanyu Pallavi Sudhir" }]
14
+ keywords = ["agents", "claude-code", "codex", "chatgpt", "claude", "tavya", "conversation", "transcript", "export"]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Environment :: Console",
18
+ "Intended Audience :: Developers",
19
+ "Operating System :: OS Independent",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3 :: Only",
22
+ "Programming Language :: Python :: 3.10",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Programming Language :: Python :: 3.13",
26
+ "Programming Language :: Python :: 3.14",
27
+ "Topic :: Communications :: Chat",
28
+ "Topic :: Software Development",
29
+ "Topic :: Utilities",
30
+ "Typing :: Typed",
31
+ ]
32
+ dependencies = []
33
+
34
+ [project.optional-dependencies]
35
+ browser = ["playwright>=1.50"]
36
+
37
+ [project.urls]
38
+ Homepage = "https://github.com/abhimanyupallavisudhir/panagent"
39
+ Source = "https://github.com/abhimanyupallavisudhir/panagent"
40
+ Issues = "https://github.com/abhimanyupallavisudhir/panagent/issues"
41
+ Changelog = "https://github.com/abhimanyupallavisudhir/panagent/blob/master/CHANGELOG.md"
42
+
43
+ [project.scripts]
44
+ panagent = "panagent.cli:main"
45
+
46
+ [tool.setuptools.packages.find]
47
+ where = ["src"]
48
+
49
+ [tool.setuptools.package-data]
50
+ panagent = ["py.typed"]
51
+
52
+ [tool.setuptools.dynamic]
53
+ version = {attr = "panagent.__version__"}
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,30 @@
1
+ """Move agent conversations between Claude Code, Codex, ChatGPT, Claude and tavya.
2
+
3
+ import panagent
4
+ conversation = panagent.load("https://chatgpt.com/share/...")
5
+ print(panagent.render(conversation, "markdown").text)
6
+ panagent.install(conversation, "claude-code").command # 'cd ... && claude --resume ...'
7
+ """
8
+
9
+ __version__ = "0.3.0"
10
+
11
+ from .api import Installed, install, load, parse, render
12
+ from .errors import AcquisitionError, BrowserRequired, FormatError, PanagentError
13
+ from .model import SCHEMA, new_conversation, validate_conversation
14
+ from .writers import Rendered
15
+
16
+ __all__ = [
17
+ "SCHEMA",
18
+ "AcquisitionError",
19
+ "BrowserRequired",
20
+ "FormatError",
21
+ "Installed",
22
+ "PanagentError",
23
+ "Rendered",
24
+ "install",
25
+ "load",
26
+ "new_conversation",
27
+ "parse",
28
+ "render",
29
+ "validate_conversation",
30
+ ]
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())
@@ -0,0 +1,171 @@
1
+ """The library interface: load any supported source, render or install it as any target."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import re
8
+ from dataclasses import dataclass
9
+ from datetime import datetime, timezone
10
+ from pathlib import Path
11
+ from shlex import quote
12
+ from typing import Any
13
+ from uuid import uuid4
14
+
15
+ from .browser import fetch_share_browser
16
+ from .detect import canonical_format, detect_text, url_format
17
+ from .errors import AcquisitionError, BrowserRequired, PanagentError
18
+ from .model import default_mode, validate_conversation
19
+ from .readers import READERS, read_file
20
+ from .web import WEB_READERS, fetch_share
21
+ from .writers import WRITERS, Rendered
22
+
23
+ # Shares whose pages may need a real browser (a challenge or a client-rendered app).
24
+ BROWSER_FORMATS = {"chatgpt-share", "claude-share"}
25
+ NATIVE_TARGETS = {"claude-code", "codex"}
26
+
27
+
28
+ def parse(
29
+ text: str,
30
+ source_format: str | None = None,
31
+ *,
32
+ source_uri: str | None = None,
33
+ conversation: str | None = None,
34
+ ) -> dict[str, Any]:
35
+ """Read a conversation from text in any supported input format (detected when omitted)."""
36
+ fmt = canonical_format(source_format) if source_format else detect_text(text)
37
+ reader = READERS.get(fmt) or WEB_READERS.get(fmt)
38
+ if reader is None:
39
+ raise PanagentError(f"{fmt} is output-only")
40
+ return validate_conversation(reader(text, source_uri=source_uri, conversation=conversation))
41
+
42
+
43
+ def load(
44
+ source: str | os.PathLike[str],
45
+ source_format: str | None = None,
46
+ *,
47
+ conversation: str | None = None,
48
+ timeout: float = 30.0,
49
+ browser: str = "never",
50
+ browser_timeout: float = 120.0,
51
+ cdp_url: str | None = None,
52
+ browser_profile: str | None = None,
53
+ ) -> dict[str, Any]:
54
+ """Read a conversation from a file or a public share URL.
55
+
56
+ browser is "never" (the library default), "auto", "headless" or "headed";
57
+ it only applies to ChatGPT and Claude shares that plain HTTPS cannot read.
58
+ """
59
+ source = os.fspath(source)
60
+ share = url_format(source) if "://" in source else None
61
+ if not share:
62
+ return parse(read_file(Path(source)), source_format, source_uri=source, conversation=conversation)
63
+ fmt = canonical_format(source_format) if source_format else share
64
+ if fmt != share:
65
+ raise PanagentError(f"URL does not match source format {fmt}")
66
+ reader = WEB_READERS[fmt]
67
+
68
+ def through_browser() -> dict[str, Any]:
69
+ text = fetch_share_browser(source, timeout=browser_timeout, mode=browser, cdp_url=cdp_url, profile=browser_profile)
70
+ return validate_conversation(reader(text, source_uri=source))
71
+
72
+ if fmt in BROWSER_FORMATS and (browser in {"headless", "headed"} or cdp_url):
73
+ return through_browser()
74
+ try:
75
+ return validate_conversation(reader(fetch_share(source, timeout=timeout), source_uri=source))
76
+ except AcquisitionError as exc:
77
+ retryable = isinstance(exc, BrowserRequired) or exc.status in {403, 429}
78
+ if fmt not in BROWSER_FORMATS or browser == "never" or not retryable:
79
+ raise
80
+ return through_browser()
81
+
82
+
83
+ def render(
84
+ conv: dict[str, Any],
85
+ target: str,
86
+ *,
87
+ mode: str = "auto",
88
+ cwd: str | None = None,
89
+ session_id: str | None = None,
90
+ ) -> Rendered:
91
+ """Write a conversation as target ("ir", "markdown", "claude-code" or "codex").
92
+
93
+ mode "auto" gives native targets a guarded context message for web snapshots
94
+ and a turn-by-turn transcript for agent sessions; IR and Markdown ignore it.
95
+ """
96
+ fmt = canonical_format(target)
97
+ writer = WRITERS.get(fmt)
98
+ if writer is None:
99
+ raise PanagentError(f"{fmt} is input-only")
100
+ validate_conversation(conv)
101
+ if mode == "auto":
102
+ mode = default_mode(conv) if fmt in NATIVE_TARGETS else "transcript"
103
+ if mode not in {"context", "transcript"}:
104
+ raise PanagentError(f"unknown mode: {mode}")
105
+ rendered = writer(conv, mode=mode, cwd=cwd, session_id=session_id)
106
+ rendered.format, rendered.mode = fmt, mode
107
+ return rendered
108
+
109
+
110
+ @dataclass
111
+ class Installed:
112
+ path: Path
113
+ session_id: str
114
+ command: str
115
+ rendered: Rendered
116
+
117
+
118
+ def install(
119
+ conv: dict[str, Any],
120
+ target: str,
121
+ *,
122
+ cwd: str | os.PathLike[str] | None = None,
123
+ session_id: str | None = None,
124
+ mode: str = "auto",
125
+ home: str | os.PathLike[str] | None = None,
126
+ ) -> Installed:
127
+ """Add a conversation to Claude Code's or Codex's own history as a new session.
128
+
129
+ Claude Code finds sessions per project, so cwd (default: the current
130
+ directory) is where `claude --resume` must run. home overrides the CLI's
131
+ data directory ($CLAUDE_CONFIG_DIR or ~/.claude; $CODEX_HOME or ~/.codex).
132
+ An existing session file is never overwritten.
133
+ """
134
+ fmt = canonical_format(target)
135
+ if fmt not in NATIVE_TARGETS:
136
+ raise PanagentError("only claude-code and codex sessions can be installed")
137
+ directory = str(Path(cwd or os.getcwd()).expanduser().resolve())
138
+ # The installed copy is a new session of the target CLI, never the source's identity.
139
+ session_id = session_id or str(uuid4())
140
+ rendered = render(conv, fmt, mode=mode, cwd=directory, session_id=session_id)
141
+ if fmt == "claude-code":
142
+ root = Path(home or os.environ.get("CLAUDE_CONFIG_DIR") or Path.home() / ".claude").expanduser()
143
+ path = root / "projects" / re.sub(r"[^A-Za-z0-9]", "-", directory) / f"{session_id}.jsonl"
144
+ command = f"cd {quote(directory)} && claude --resume {session_id}"
145
+ else:
146
+ root = Path(home or os.environ.get("CODEX_HOME") or Path.home() / ".codex").expanduser()
147
+ started = _utc(rendered.text.split("\n", 1)[0])
148
+ path = (root / "sessions" / started.strftime("%Y/%m/%d")
149
+ / f"rollout-{started.strftime('%Y-%m-%dT%H-%M-%S')}-{session_id}.jsonl")
150
+ command = f"codex resume {session_id}"
151
+ path.parent.mkdir(parents=True, exist_ok=True)
152
+ try:
153
+ descriptor = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
154
+ except FileExistsError as exc:
155
+ raise PanagentError(f"session {session_id} already exists at {path}") from exc
156
+ try:
157
+ with os.fdopen(descriptor, "w", encoding="utf-8", newline="") as handle:
158
+ handle.write(rendered.text)
159
+ except BaseException:
160
+ path.unlink(missing_ok=True)
161
+ raise
162
+ return Installed(path, session_id, command, rendered)
163
+
164
+
165
+ def _utc(first_record: str) -> datetime:
166
+ """The session start of a Codex rollout (from its session_meta record), else now."""
167
+ try:
168
+ value = json.loads(first_record)["timestamp"]
169
+ return datetime.fromisoformat(value.replace("Z", "+00:00")).astimezone(timezone.utc)
170
+ except (ValueError, KeyError, TypeError, AttributeError):
171
+ return datetime.now(timezone.utc)