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 +21 -0
- panagent-0.3.0/PKG-INFO +167 -0
- panagent-0.3.0/README.md +134 -0
- panagent-0.3.0/pyproject.toml +53 -0
- panagent-0.3.0/setup.cfg +4 -0
- panagent-0.3.0/src/panagent/__init__.py +30 -0
- panagent-0.3.0/src/panagent/__main__.py +3 -0
- panagent-0.3.0/src/panagent/api.py +171 -0
- panagent-0.3.0/src/panagent/browser.py +210 -0
- panagent-0.3.0/src/panagent/cli.py +228 -0
- panagent-0.3.0/src/panagent/detect.py +154 -0
- panagent-0.3.0/src/panagent/errors.py +18 -0
- panagent-0.3.0/src/panagent/model.py +154 -0
- panagent-0.3.0/src/panagent/py.typed +0 -0
- panagent-0.3.0/src/panagent/readers.py +484 -0
- panagent-0.3.0/src/panagent/web.py +774 -0
- panagent-0.3.0/src/panagent/writers.py +533 -0
- panagent-0.3.0/src/panagent.egg-info/PKG-INFO +167 -0
- panagent-0.3.0/src/panagent.egg-info/SOURCES.txt +28 -0
- panagent-0.3.0/src/panagent.egg-info/dependency_links.txt +1 -0
- panagent-0.3.0/src/panagent.egg-info/entry_points.txt +2 -0
- panagent-0.3.0/src/panagent.egg-info/requires.txt +3 -0
- panagent-0.3.0/src/panagent.egg-info/top_level.txt +1 -0
- panagent-0.3.0/tests/test_browser.py +75 -0
- panagent-0.3.0/tests/test_cli.py +126 -0
- panagent-0.3.0/tests/test_conversion.py +318 -0
- panagent-0.3.0/tests/test_install.py +82 -0
- panagent-0.3.0/tests/test_live_samples.py +35 -0
- panagent-0.3.0/tests/test_native_cli.py +215 -0
- panagent-0.3.0/tests/test_sources.py +171 -0
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.
|
panagent-0.3.0/PKG-INFO
ADDED
|
@@ -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
|
panagent-0.3.0/README.md
ADDED
|
@@ -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__"}
|
panagent-0.3.0/setup.cfg
ADDED
|
@@ -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,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)
|