agent-sessions-cli 0.2.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.
Files changed (34) hide show
  1. agent_sessions_cli-0.2.0/.gitignore +4 -0
  2. agent_sessions_cli-0.2.0/CHANGELOG.md +23 -0
  3. agent_sessions_cli-0.2.0/LICENSE +21 -0
  4. agent_sessions_cli-0.2.0/PKG-INFO +141 -0
  5. agent_sessions_cli-0.2.0/README.md +117 -0
  6. agent_sessions_cli-0.2.0/agent_sessions/__init__.py +3 -0
  7. agent_sessions_cli-0.2.0/agent_sessions/__main__.py +3 -0
  8. agent_sessions_cli-0.2.0/agent_sessions/adapters/__init__.py +136 -0
  9. agent_sessions_cli-0.2.0/agent_sessions/adapters/base.py +108 -0
  10. agent_sessions_cli-0.2.0/agent_sessions/adapters/claude_code.py +300 -0
  11. agent_sessions_cli-0.2.0/agent_sessions/adapters/codex.py +360 -0
  12. agent_sessions_cli-0.2.0/agent_sessions/adapters/gemini.py +176 -0
  13. agent_sessions_cli-0.2.0/agent_sessions/adapters/opencode.py +231 -0
  14. agent_sessions_cli-0.2.0/agent_sessions/cli.py +858 -0
  15. agent_sessions_cli-0.2.0/agent_sessions/core.py +748 -0
  16. agent_sessions_cli-0.2.0/agent_sessions/model.py +119 -0
  17. agent_sessions_cli-0.2.0/agent_sessions/templates/gitattributes +2 -0
  18. agent_sessions_cli-0.2.0/agent_sessions/templates/sessions-README.md +31 -0
  19. agent_sessions_cli-0.2.0/agent_sessions/templates/summary.md +31 -0
  20. agent_sessions_cli-0.2.0/agents-skills/agent-sessions/scripts/agent_sessions/__init__.py +3 -0
  21. agent_sessions_cli-0.2.0/agents-skills/agent-sessions/scripts/agent_sessions/__main__.py +3 -0
  22. agent_sessions_cli-0.2.0/agents-skills/agent-sessions/scripts/agent_sessions/adapters/__init__.py +136 -0
  23. agent_sessions_cli-0.2.0/agents-skills/agent-sessions/scripts/agent_sessions/adapters/base.py +108 -0
  24. agent_sessions_cli-0.2.0/agents-skills/agent-sessions/scripts/agent_sessions/adapters/claude_code.py +300 -0
  25. agent_sessions_cli-0.2.0/agents-skills/agent-sessions/scripts/agent_sessions/adapters/codex.py +360 -0
  26. agent_sessions_cli-0.2.0/agents-skills/agent-sessions/scripts/agent_sessions/adapters/gemini.py +176 -0
  27. agent_sessions_cli-0.2.0/agents-skills/agent-sessions/scripts/agent_sessions/adapters/opencode.py +231 -0
  28. agent_sessions_cli-0.2.0/agents-skills/agent-sessions/scripts/agent_sessions/cli.py +858 -0
  29. agent_sessions_cli-0.2.0/agents-skills/agent-sessions/scripts/agent_sessions/core.py +748 -0
  30. agent_sessions_cli-0.2.0/agents-skills/agent-sessions/scripts/agent_sessions/model.py +119 -0
  31. agent_sessions_cli-0.2.0/agents-skills/agent-sessions/scripts/agent_sessions/templates/gitattributes +2 -0
  32. agent_sessions_cli-0.2.0/agents-skills/agent-sessions/scripts/agent_sessions/templates/sessions-README.md +31 -0
  33. agent_sessions_cli-0.2.0/agents-skills/agent-sessions/scripts/agent_sessions/templates/summary.md +31 -0
  34. agent_sessions_cli-0.2.0/pyproject.toml +43 -0
@@ -0,0 +1,4 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .pytest_cache/
4
+ .DS_Store
@@ -0,0 +1,23 @@
1
+ # Changelog
2
+
3
+ ## 0.2.0 — 2026-09-22
4
+
5
+ Works across coding agents. The repo is now `agent-sessions`; the Claude plugin keeps its name `sessions`.
6
+
7
+ - **Adapters**: Codex CLI (rollout JSONL, session index titles, `exec` command extraction, `apply_patch` files, compaction history) and OpenCode (SQLite, read-only, locked-db fallback, compaction parts, child sessions) join Claude Code. Gemini CLI ships as an experimental adapter built from its source schema. Copilot CLI and Cursor are detected and reported as not yet supported.
8
+ - **Agent-neutral model**: adapters emit events with canonical tool kinds (shell, read, edit, write, search, web, agent, mcp, ask); the renderer, result policy, redaction and files-touched logic no longer know agent tool names.
9
+ - **Live-session identification** without env vars: the locate call recognises itself as the shell call still running in exactly one session; otherwise it lists candidates and asks instead of guessing. Codex fork stubs are skipped.
10
+ - **Re-push fix**: the transcript now stops at the last push invocation, so pushing the same session twice keeps everything after the first push.
11
+ - **Portable skill** `agents-skills/agent-sessions/` (Agent Skills standard) with a bundled copy of the package, installed by `scripts/setup.sh --skills` into `~/.agents/skills/`, which Codex, OpenCode, Gemini CLI, Cursor and Copilot all read. `init --vendor-skill` copies it into a repo for teammates.
12
+ - **Python package** `agent-sessions-cli` (`pip install agent-sessions-cli`, console script `agent-sessions`), CI on Linux, macOS and Windows, release workflow with PyPI trusted publishing.
13
+ - `doctor` subcommand; `STATUS:` printed first and last; `SCRIPT:` line; `meta.json` gains `agent`, `agent_version`, `originator`, `parent_id`, `short_id`, `tokens`; commit trailer `Agent-Session: <agent>:<id>`; temp index inside the git dir for sandboxed agents.
14
+
15
+ ## 0.1.0 — 2026-09-21
16
+
17
+ First release, Claude Code only.
18
+
19
+ - `/sessions:push` writes a handoff summary, renders and redacts the transcript, commits under `.claude/sessions/` through a temporary index, and pushes.
20
+ - `/sessions:pull` fetches, indexes sessions on the default branch and `HEAD`, and loads summaries into the chat.
21
+ - `/sessions:init` fixes `.gitignore`, drops a README and `.gitattributes`, and registers the plugin in `.claude/settings.json`.
22
+ - Renderer follows the active conversation path across rewinds and compaction, stubs MCP and file-read results, truncates other tool output, and collects files touched by subagents.
23
+ - Redaction covers cloud and VCS tokens, JWTs, private keys, URL credentials, Azure storage and Entra secrets, connection-string passwords and SAS signatures, with keyword hits flagged for review.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Prajwal P
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,141 @@
1
+ Metadata-Version: 2.5
2
+ Name: agent-sessions-cli
3
+ Version: 0.2.0
4
+ Summary: Share coding-agent sessions (Claude Code, Codex CLI, OpenCode, Gemini CLI) with your team through git.
5
+ Project-URL: Homepage, https://github.com/prajwalgajakesari/agent-sessions
6
+ Project-URL: Repository, https://github.com/prajwalgajakesari/agent-sessions
7
+ Project-URL: Changelog, https://github.com/prajwalgajakesari/agent-sessions/blob/main/CHANGELOG.md
8
+ Project-URL: Issues, https://github.com/prajwalgajakesari/agent-sessions/issues
9
+ Author: Prajwal P
10
+ License: MIT
11
+ License-File: LICENSE
12
+ Keywords: claude-code,codex,gemini-cli,git,handoff,opencode,sessions,team,transcript
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Environment :: Console
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3 :: Only
20
+ Classifier: Topic :: Software Development
21
+ Classifier: Topic :: Software Development :: Version Control :: Git
22
+ Requires-Python: >=3.9
23
+ Description-Content-Type: text/markdown
24
+
25
+ # agent-sessions
26
+
27
+ Share coding-agent sessions with your team through the git repo you already work in. One folder, `.claude/sessions/`, holds handoffs from **Claude Code, Codex CLI, OpenCode** and (experimentally) **Gemini CLI**; anyone on any agent can pull them back into their own chat.
28
+
29
+ ```
30
+ /sessions:push "Reconcile fabric tables" Claude Code, end of a session
31
+ $agent-sessions pull reconcile a teammate in Codex, next morning
32
+ ```
33
+
34
+ Coding agents keep sessions in private, undocumented, short-lived local stores. This tool turns one into three committed files and gives teammates one command to load them.
35
+
36
+ ## What a pushed session looks like
37
+
38
+ ```
39
+ .claude/sessions/2026-09-22_reconcile-fabric-tables_prajwal-p_7dcecef6/
40
+ ├── summary.md handoff the author's agent wrote at push time: goal, outcome, decisions and why,
41
+ │ files changed, commands and gotchas, open questions, a paste-ready handoff prompt (≤120 lines)
42
+ ├── transcript.md the conversation as markdown along the active path (rewinds and compaction handled),
43
+ │ tool calls collapsed, secrets redacted, file reads and query results omitted
44
+ └── meta.json title, author, agent and version, branch, dates, files touched (subagents included), tags, outcome, tokens
45
+ ```
46
+
47
+ Pull reads `summary.md` first and loads a transcript only when asked, so a teammate gets the reasoning without burning their context window.
48
+
49
+ ## Install
50
+
51
+ | You use | Do this once |
52
+ |---|---|
53
+ | Claude Code | `/plugin marketplace add prajwalgajakesari/agent-sessions` then `/plugin install sessions@agent-sessions`. Repos set up with `init` prompt you automatically. |
54
+ | Codex CLI, OpenCode, Gemini CLI, Cursor, Copilot | `scripts/setup.sh --skills` (or `scripts\setup.ps1 -Skills`) from a checkout. It copies the `agent-sessions` skill into `~/.agents/skills/`, which all of them read. |
55
+ | Any terminal | `pip install agent-sessions-cli` or `uvx agent-sessions-cli`, then `agent-sessions doctor`. |
56
+
57
+ Requirements: git and Python 3.9+ (`python3`, `python`, or the Windows `py` launcher). No Python packages.
58
+
59
+ Then, in each repo you want to share sessions in: `/sessions:init` in Claude Code, or `$agent-sessions init` in Codex, or `agent-sessions init` in a terminal. It fixes a blanket `.claude/` ignore rule so only `.claude/sessions/` and `.claude/settings.json` are tracked, drops a README and `.gitattributes`, registers the Claude plugin in `.claude/settings.json`, and commits exactly those files. Add `--vendor-skill` to also copy the portable skill into `.agents/skills/` so every teammate's agent discovers it on clone.
60
+
61
+ ## Using it
62
+
63
+ | Agent | Share this session | Read teammates' sessions |
64
+ |---|---|---|
65
+ | Claude Code | `/sessions:push [title] [--yes]` | `/sessions:pull [keyword]` |
66
+ | Codex CLI | `$agent-sessions push [title] --yes` | `$agent-sessions pull [keyword]` |
67
+ | OpenCode, Gemini CLI, Cursor, Copilot | "use the agent-sessions skill to push this session" | "use the agent-sessions skill to pull [keyword]" |
68
+ | Terminal | `agent-sessions locate` → write `summary.md` → `export` → `commit --push` | `agent-sessions list` |
69
+
70
+ Push flow, whichever agent runs it:
71
+
72
+ 1. `locate` finds this session in the agent's store, checks the repo (ignore rules, detached HEAD, merges in progress, missing upstream) and prints an outline of the conversation.
73
+ 2. The agent writes `summary.md` from the template, using the outline plus its own memory, through `write-summary` (agents' file tools tend to refuse `.claude/`).
74
+ 3. `export` renders and redacts the transcript, writes `meta.json`, renames the folder to the final title, and prints a redaction report.
75
+ 4. You see a short preview and confirm once. `--yes` skips it.
76
+ 5. `commit --push` builds the commit on a temporary index inside `.git` and pushes to your current branch. Your staged changes, working tree and any in-progress merge are untouched. `--branch` pushes to a new remote branch instead.
77
+
78
+ Pushing the same session again updates the same folder and keeps everything after the first push.
79
+
80
+ Pull flags: `--n N` how many summaries (default 3), `--full <id>` one full transcript, `--all-branches` sessions on unmerged branches, `--mine` your own.
81
+
82
+ ## How the live session is found
83
+
84
+ No agent tells a shell command which session it belongs to, so `locate` never guesses:
85
+
86
+ | Signal | Used by |
87
+ |---|---|
88
+ | `--session-id` passed by the skill | Claude Code (`${CLAUDE_SESSION_ID}` is substituted into the preflight) |
89
+ | Environment variable | Claude Code (`CLAUDE_CODE_SESSION_ID`); Codex if `CODEX_THREAD_ID` reaches child shells |
90
+ | **Self-reference**: the `locate` call itself is the newest shell call in exactly one session and has no output yet | Codex, OpenCode, Claude Code |
91
+ | A push invocation in the latest user turn | any |
92
+ | Exactly one session for this repo | any |
93
+
94
+ If more than one session still matches, `locate` prints `CANDIDATES:` and the skill asks you which one, then re-runs with `--agent` and `--session-id`. Codex fork and compaction stubs are skipped.
95
+
96
+ ## Where each agent keeps sessions
97
+
98
+ | Agent | Store | Notes |
99
+ |---|---|---|
100
+ | Claude Code | `~/.claude/projects/<encoded-cwd>/<id>.jsonl` (+ `subagents/`) | active path via `parentUuid`, compaction boundaries, subagent edits counted |
101
+ | Codex CLI | `$CODEX_HOME/sessions/YYYY/MM/DD/rollout-*.jsonl`, `session_index.jsonl` for titles | `exec` snippets parsed for `exec_command` and `apply_patch`; `compacted` history rendered once; developer messages and environment wrappers dropped |
102
+ | OpenCode | `~/.local/share/opencode/opencode.db` (SQLite, WAL) | opened read-only, copied if locked; `compaction` parts; child sessions feed files touched; tokens and cost from the session row |
103
+ | Gemini CLI | `~/.gemini/tmp/<hash>/chats/session-*.jsonl` | **experimental**: built from the source schema without a real sample; `$set`/`$rewindTo` applied |
104
+ | Copilot CLI, Cursor | detected, not exported | pull and init work; push says so. Cursor's plain-text agent transcripts are the next adapter once a sample exists |
105
+
106
+ ## What is and is not redacted
107
+
108
+ | Layer | What it does |
109
+ |---|---|
110
+ | Result policy | Results of file reads, web fetches and MCP tools are replaced by a one-line stub. Other tool output is cut to 30 lines or 2 KB. `--include-results` keeps up to 20 KB per result. |
111
+ | Sensitive files | Any tool call that reads or edits `.env*`, `local.settings.json`, `appsettings*.json`, `secrets.json`, key and certificate files, Terraform state, `.netrc`, `.pypirc`, `.databrickscfg` or anything under `.azure/` has both input and result withheld. |
112
+ | Pattern redaction | AWS keys, GitHub, Slack, OpenAI and Anthropic tokens, JWTs, private keys, credentials in URLs, Azure storage account keys, shared access keys and SAS signatures, Azure DevOps PATs, Entra client secrets, bearer tokens. Keyword hits such as `password=`, `client_secret=`, `-P` after `sqlcmd` are redacted too and listed for you to review. |
113
+
114
+ GUIDs are never redacted. Placeholders like `${DB_PASSWORD}` and `<your-key>` are left alone. If a high-confidence pattern still matches after redaction, export deletes the transcript and refuses. Treat a pushed transcript the way you treat a chat log: read the preview before you say yes.
115
+
116
+ ## Sandboxes and permissions
117
+
118
+ Codex's default sandbox blocks network access, so `commit --push` may fail; approve the escalation when Codex offers it, or run the printed `MANUAL` command in your own terminal. The temporary index lives inside `.git`, which stays writable. In OpenCode, allow the skill's script in `opencode.json` permissions to avoid a prompt per step.
119
+
120
+ ## Layout of this repo
121
+
122
+ ```
123
+ agent_sessions/ the package (stdlib only): cli.py, core.py, model.py, adapters/, templates/
124
+ scripts/sessions.py, sessions.sh entry point + shim (finds Python, always exits 0, prints STATUS)
125
+ scripts/setup.sh, setup.ps1 teammate bootstrap: --claude, --skills, --repo <path>
126
+ skills/{push,pull,init}/ the Claude Code plugin skills
127
+ agents-skills/agent-sessions/ the portable Agent-Skills skill, with a bundled copy of the package
128
+ tools/sync_bundle.py keeps that copy identical (`--check` in CI)
129
+ tests/ python3 -m unittest discover -s tests
130
+ .claude-plugin/ plugin.json (name: sessions) and marketplace.json (name: agent-sessions)
131
+ ```
132
+
133
+ Test the Claude plugin from a checkout with `claude --plugin-dir /path/to/agent-sessions`. Test the portable skill by copying `agents-skills/agent-sessions` into `~/.agents/skills/`.
134
+
135
+ ## Contributing an adapter
136
+
137
+ An adapter is one file in `agent_sessions/adapters/` that turns a native store into `Session` events with canonical tool kinds (`shell`, `read`, `edit`, `write`, `search`, `web`, `agent`, `mcp`, `ask`). The renderer, result policy, redaction and files-touched logic never see agent names. Start from `gemini.py` (small) or `codex.py` (complete). A real session sample from your agent, with secrets removed, is the most useful contribution.
138
+
139
+ ## License
140
+
141
+ MIT
@@ -0,0 +1,117 @@
1
+ # agent-sessions
2
+
3
+ Share coding-agent sessions with your team through the git repo you already work in. One folder, `.claude/sessions/`, holds handoffs from **Claude Code, Codex CLI, OpenCode** and (experimentally) **Gemini CLI**; anyone on any agent can pull them back into their own chat.
4
+
5
+ ```
6
+ /sessions:push "Reconcile fabric tables" Claude Code, end of a session
7
+ $agent-sessions pull reconcile a teammate in Codex, next morning
8
+ ```
9
+
10
+ Coding agents keep sessions in private, undocumented, short-lived local stores. This tool turns one into three committed files and gives teammates one command to load them.
11
+
12
+ ## What a pushed session looks like
13
+
14
+ ```
15
+ .claude/sessions/2026-09-22_reconcile-fabric-tables_prajwal-p_7dcecef6/
16
+ ├── summary.md handoff the author's agent wrote at push time: goal, outcome, decisions and why,
17
+ │ files changed, commands and gotchas, open questions, a paste-ready handoff prompt (≤120 lines)
18
+ ├── transcript.md the conversation as markdown along the active path (rewinds and compaction handled),
19
+ │ tool calls collapsed, secrets redacted, file reads and query results omitted
20
+ └── meta.json title, author, agent and version, branch, dates, files touched (subagents included), tags, outcome, tokens
21
+ ```
22
+
23
+ Pull reads `summary.md` first and loads a transcript only when asked, so a teammate gets the reasoning without burning their context window.
24
+
25
+ ## Install
26
+
27
+ | You use | Do this once |
28
+ |---|---|
29
+ | Claude Code | `/plugin marketplace add prajwalgajakesari/agent-sessions` then `/plugin install sessions@agent-sessions`. Repos set up with `init` prompt you automatically. |
30
+ | Codex CLI, OpenCode, Gemini CLI, Cursor, Copilot | `scripts/setup.sh --skills` (or `scripts\setup.ps1 -Skills`) from a checkout. It copies the `agent-sessions` skill into `~/.agents/skills/`, which all of them read. |
31
+ | Any terminal | `pip install agent-sessions-cli` or `uvx agent-sessions-cli`, then `agent-sessions doctor`. |
32
+
33
+ Requirements: git and Python 3.9+ (`python3`, `python`, or the Windows `py` launcher). No Python packages.
34
+
35
+ Then, in each repo you want to share sessions in: `/sessions:init` in Claude Code, or `$agent-sessions init` in Codex, or `agent-sessions init` in a terminal. It fixes a blanket `.claude/` ignore rule so only `.claude/sessions/` and `.claude/settings.json` are tracked, drops a README and `.gitattributes`, registers the Claude plugin in `.claude/settings.json`, and commits exactly those files. Add `--vendor-skill` to also copy the portable skill into `.agents/skills/` so every teammate's agent discovers it on clone.
36
+
37
+ ## Using it
38
+
39
+ | Agent | Share this session | Read teammates' sessions |
40
+ |---|---|---|
41
+ | Claude Code | `/sessions:push [title] [--yes]` | `/sessions:pull [keyword]` |
42
+ | Codex CLI | `$agent-sessions push [title] --yes` | `$agent-sessions pull [keyword]` |
43
+ | OpenCode, Gemini CLI, Cursor, Copilot | "use the agent-sessions skill to push this session" | "use the agent-sessions skill to pull [keyword]" |
44
+ | Terminal | `agent-sessions locate` → write `summary.md` → `export` → `commit --push` | `agent-sessions list` |
45
+
46
+ Push flow, whichever agent runs it:
47
+
48
+ 1. `locate` finds this session in the agent's store, checks the repo (ignore rules, detached HEAD, merges in progress, missing upstream) and prints an outline of the conversation.
49
+ 2. The agent writes `summary.md` from the template, using the outline plus its own memory, through `write-summary` (agents' file tools tend to refuse `.claude/`).
50
+ 3. `export` renders and redacts the transcript, writes `meta.json`, renames the folder to the final title, and prints a redaction report.
51
+ 4. You see a short preview and confirm once. `--yes` skips it.
52
+ 5. `commit --push` builds the commit on a temporary index inside `.git` and pushes to your current branch. Your staged changes, working tree and any in-progress merge are untouched. `--branch` pushes to a new remote branch instead.
53
+
54
+ Pushing the same session again updates the same folder and keeps everything after the first push.
55
+
56
+ Pull flags: `--n N` how many summaries (default 3), `--full <id>` one full transcript, `--all-branches` sessions on unmerged branches, `--mine` your own.
57
+
58
+ ## How the live session is found
59
+
60
+ No agent tells a shell command which session it belongs to, so `locate` never guesses:
61
+
62
+ | Signal | Used by |
63
+ |---|---|
64
+ | `--session-id` passed by the skill | Claude Code (`${CLAUDE_SESSION_ID}` is substituted into the preflight) |
65
+ | Environment variable | Claude Code (`CLAUDE_CODE_SESSION_ID`); Codex if `CODEX_THREAD_ID` reaches child shells |
66
+ | **Self-reference**: the `locate` call itself is the newest shell call in exactly one session and has no output yet | Codex, OpenCode, Claude Code |
67
+ | A push invocation in the latest user turn | any |
68
+ | Exactly one session for this repo | any |
69
+
70
+ If more than one session still matches, `locate` prints `CANDIDATES:` and the skill asks you which one, then re-runs with `--agent` and `--session-id`. Codex fork and compaction stubs are skipped.
71
+
72
+ ## Where each agent keeps sessions
73
+
74
+ | Agent | Store | Notes |
75
+ |---|---|---|
76
+ | Claude Code | `~/.claude/projects/<encoded-cwd>/<id>.jsonl` (+ `subagents/`) | active path via `parentUuid`, compaction boundaries, subagent edits counted |
77
+ | Codex CLI | `$CODEX_HOME/sessions/YYYY/MM/DD/rollout-*.jsonl`, `session_index.jsonl` for titles | `exec` snippets parsed for `exec_command` and `apply_patch`; `compacted` history rendered once; developer messages and environment wrappers dropped |
78
+ | OpenCode | `~/.local/share/opencode/opencode.db` (SQLite, WAL) | opened read-only, copied if locked; `compaction` parts; child sessions feed files touched; tokens and cost from the session row |
79
+ | Gemini CLI | `~/.gemini/tmp/<hash>/chats/session-*.jsonl` | **experimental**: built from the source schema without a real sample; `$set`/`$rewindTo` applied |
80
+ | Copilot CLI, Cursor | detected, not exported | pull and init work; push says so. Cursor's plain-text agent transcripts are the next adapter once a sample exists |
81
+
82
+ ## What is and is not redacted
83
+
84
+ | Layer | What it does |
85
+ |---|---|
86
+ | Result policy | Results of file reads, web fetches and MCP tools are replaced by a one-line stub. Other tool output is cut to 30 lines or 2 KB. `--include-results` keeps up to 20 KB per result. |
87
+ | Sensitive files | Any tool call that reads or edits `.env*`, `local.settings.json`, `appsettings*.json`, `secrets.json`, key and certificate files, Terraform state, `.netrc`, `.pypirc`, `.databrickscfg` or anything under `.azure/` has both input and result withheld. |
88
+ | Pattern redaction | AWS keys, GitHub, Slack, OpenAI and Anthropic tokens, JWTs, private keys, credentials in URLs, Azure storage account keys, shared access keys and SAS signatures, Azure DevOps PATs, Entra client secrets, bearer tokens. Keyword hits such as `password=`, `client_secret=`, `-P` after `sqlcmd` are redacted too and listed for you to review. |
89
+
90
+ GUIDs are never redacted. Placeholders like `${DB_PASSWORD}` and `<your-key>` are left alone. If a high-confidence pattern still matches after redaction, export deletes the transcript and refuses. Treat a pushed transcript the way you treat a chat log: read the preview before you say yes.
91
+
92
+ ## Sandboxes and permissions
93
+
94
+ Codex's default sandbox blocks network access, so `commit --push` may fail; approve the escalation when Codex offers it, or run the printed `MANUAL` command in your own terminal. The temporary index lives inside `.git`, which stays writable. In OpenCode, allow the skill's script in `opencode.json` permissions to avoid a prompt per step.
95
+
96
+ ## Layout of this repo
97
+
98
+ ```
99
+ agent_sessions/ the package (stdlib only): cli.py, core.py, model.py, adapters/, templates/
100
+ scripts/sessions.py, sessions.sh entry point + shim (finds Python, always exits 0, prints STATUS)
101
+ scripts/setup.sh, setup.ps1 teammate bootstrap: --claude, --skills, --repo <path>
102
+ skills/{push,pull,init}/ the Claude Code plugin skills
103
+ agents-skills/agent-sessions/ the portable Agent-Skills skill, with a bundled copy of the package
104
+ tools/sync_bundle.py keeps that copy identical (`--check` in CI)
105
+ tests/ python3 -m unittest discover -s tests
106
+ .claude-plugin/ plugin.json (name: sessions) and marketplace.json (name: agent-sessions)
107
+ ```
108
+
109
+ Test the Claude plugin from a checkout with `claude --plugin-dir /path/to/agent-sessions`. Test the portable skill by copying `agents-skills/agent-sessions` into `~/.agents/skills/`.
110
+
111
+ ## Contributing an adapter
112
+
113
+ An adapter is one file in `agent_sessions/adapters/` that turns a native store into `Session` events with canonical tool kinds (`shell`, `read`, `edit`, `write`, `search`, `web`, `agent`, `mcp`, `ask`). The renderer, result policy, redaction and files-touched logic never see agent names. Start from `gemini.py` (small) or `codex.py` (complete). A real session sample from your agent, with secrets removed, is the most useful contribution.
114
+
115
+ ## License
116
+
117
+ MIT
@@ -0,0 +1,3 @@
1
+ """agent-sessions: share coding-agent sessions with your team through git."""
2
+
3
+ __version__ = "0.2.0"
@@ -0,0 +1,3 @@
1
+ from .cli import entry
2
+
3
+ entry()
@@ -0,0 +1,136 @@
1
+ """Adapter registry and the live-session identification algorithm."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from typing import Dict, List, Optional, Tuple
7
+
8
+ from ..model import PUSH_MARKER_RE
9
+ from .base import Adapter, SessionRef, recent
10
+ from .claude_code import ClaudeCodeAdapter
11
+
12
+ REGISTRY: Dict[str, Adapter] = {}
13
+
14
+
15
+ def register(adapter: Adapter) -> None:
16
+ REGISTRY[adapter.name] = adapter
17
+
18
+
19
+ register(ClaudeCodeAdapter())
20
+
21
+ try: # optional adapters are added as they land; a broken import must not take the tool down
22
+ from .codex import CodexAdapter
23
+ register(CodexAdapter())
24
+ except ImportError: # pragma: no cover
25
+ pass
26
+ try:
27
+ from .opencode import OpenCodeAdapter
28
+ register(OpenCodeAdapter())
29
+ except ImportError: # pragma: no cover
30
+ pass
31
+ try:
32
+ from .gemini import GeminiAdapter
33
+ register(GeminiAdapter())
34
+ except ImportError: # pragma: no cover
35
+ pass
36
+
37
+ AGENT_CHOICES = ["auto"] + list(REGISTRY.keys())
38
+ RECENT_MINUTES = 15
39
+
40
+
41
+ class Resolution:
42
+ def __init__(self, ref: Optional[SessionRef], how: str, candidates: List[SessionRef], adapter: Optional[Adapter]) -> None:
43
+ self.ref, self.how, self.candidates, self.adapter = ref, how, candidates, adapter
44
+
45
+
46
+ def installed_adapters(agent: str = "auto") -> List[Adapter]:
47
+ if agent != "auto":
48
+ if agent not in REGISTRY:
49
+ raise KeyError(agent)
50
+ return [REGISTRY[agent]]
51
+ return [a for a in REGISTRY.values() if a.installed()]
52
+
53
+
54
+ def resolve(agent: str, session_id: Optional[str], repo_root: str) -> Resolution:
55
+ """Find the live session. Never picks silently between several plausible ones."""
56
+ adapters = installed_adapters(agent)
57
+ if session_id:
58
+ for a in adapters:
59
+ ref = a.locate_by_id(session_id)
60
+ if ref:
61
+ return Resolution(ref, "session id", [], a)
62
+ # known id, no data yet: fresh session. Pick the adapter the environment proves, else the explicit one.
63
+ owner = REGISTRY[agent] if agent != "auto" else _env_agent(adapters)
64
+ if owner:
65
+ return Resolution(owner.pending_ref(session_id), "not written yet", [], owner)
66
+ return Resolution(None, "not found", [], None)
67
+
68
+ for a in adapters:
69
+ env_id = a.in_env()
70
+ if env_id:
71
+ ref = a.locate_by_id(env_id)
72
+ if ref:
73
+ return Resolution(ref, f"environment ({a.name})", [], a)
74
+ return Resolution(a.pending_ref(env_id), "not written yet", [], a)
75
+
76
+ all_cands: List[Tuple[Adapter, SessionRef]] = []
77
+ for a in adapters:
78
+ try:
79
+ for ref in a.candidates(repo_root, limit=5):
80
+ all_cands.append((a, ref))
81
+ except Exception: # noqa: BLE001 - one broken store must not hide the others
82
+ continue
83
+ all_cands.sort(key=lambda ar: ar[1].updated.timestamp() if ar[1].updated else 0, reverse=True)
84
+
85
+ # self-referencing locate call still in flight
86
+ hits = []
87
+ for a, ref in all_cands:
88
+ if not recent(ref, RECENT_MINUTES):
89
+ continue
90
+ try:
91
+ sess = a.load(ref)
92
+ except Exception: # noqa: BLE001
93
+ continue
94
+ ref.extra["session"] = sess
95
+ ref.title = ref.title or sess.title
96
+ if sess.has_pending_self_call():
97
+ hits.append((a, ref))
98
+ if len(hits) == 1:
99
+ return Resolution(hits[0][1], "self-referencing locate call", [], hits[0][0])
100
+
101
+ # push marker in the latest user turn
102
+ marks = []
103
+ for a, ref in all_cands:
104
+ sess = ref.extra.get("session")
105
+ if sess is None:
106
+ try:
107
+ sess = a.load(ref)
108
+ ref.extra["session"] = sess
109
+ ref.title = ref.title or sess.title
110
+ except Exception: # noqa: BLE001
111
+ continue
112
+ if PUSH_MARKER_RE.search(sess.last_user_text() or "") or any(e.is_push_invocation for e in sess.events[-3:]):
113
+ marks.append((a, ref))
114
+ if len(marks) == 1:
115
+ return Resolution(marks[0][1], "push marker in latest user turn", [], marks[0][0])
116
+
117
+ if len(all_cands) == 1:
118
+ a, ref = all_cands[0]
119
+ how = "only recent session for this repo" if recent(ref, RECENT_MINUTES) else "only session for this repo (not recent; check it is the right one)"
120
+ return Resolution(ref, how, [], a)
121
+ if not all_cands:
122
+ return Resolution(None, "not found", [], None)
123
+ return Resolution(None, "ambiguous", [ref for _, ref in all_cands], None)
124
+
125
+
126
+ def _env_agent(adapters: List[Adapter]) -> Optional[Adapter]:
127
+ for a in adapters:
128
+ if a.in_env() is not None:
129
+ return a
130
+ return None
131
+
132
+
133
+ def adapter_for(agent: str) -> Adapter:
134
+ if agent not in REGISTRY:
135
+ raise KeyError(agent)
136
+ return REGISTRY[agent]
@@ -0,0 +1,108 @@
1
+ """Adapter contract shared by every agent."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import datetime as dt
6
+ import os
7
+ from dataclasses import dataclass, field
8
+ from typing import List, Optional, Tuple
9
+
10
+ from ..model import Session
11
+
12
+
13
+ @dataclass
14
+ class SessionRef:
15
+ agent: str
16
+ session_id: str
17
+ short_id: str
18
+ source: str # file path or "sqlite:<db>#<id>"; "" when not on disk yet
19
+ cwd: Optional[str] = None
20
+ updated: Optional[dt.datetime] = None
21
+ title: Optional[str] = None
22
+ extra: dict = field(default_factory=dict)
23
+
24
+ @property
25
+ def on_disk(self) -> bool:
26
+ return bool(self.source)
27
+
28
+
29
+ class Adapter:
30
+ name = "base"
31
+ label = "Base"
32
+ env_ids: Tuple[str, ...] = () # env vars carrying the session id
33
+ env_presence: Tuple[str, ...] = () # env vars that merely prove we run inside this agent
34
+ tested_version: Optional[Tuple[int, ...]] = None
35
+
36
+ def data_dir(self) -> str:
37
+ raise NotImplementedError
38
+
39
+ def installed(self) -> bool:
40
+ return os.path.exists(self.data_dir())
41
+
42
+ def short_id(self, session_id: str) -> str:
43
+ return session_id[:8]
44
+
45
+ def locate_by_id(self, session_id: str) -> Optional[SessionRef]:
46
+ raise NotImplementedError
47
+
48
+ def candidates(self, repo_root: str, limit: int = 5) -> List[SessionRef]:
49
+ raise NotImplementedError
50
+
51
+ def load(self, ref: SessionRef) -> Session:
52
+ raise NotImplementedError
53
+
54
+ def ref_from_source(self, source: str) -> Optional[SessionRef]:
55
+ """Build a ref from a SOURCE string this adapter printed earlier, or None if it is not ours."""
56
+ return None
57
+
58
+ def pending_ref(self, session_id: str) -> SessionRef:
59
+ """A session we know the id of but that has no data on disk yet."""
60
+ return SessionRef(self.name, session_id, self.short_id(session_id), "")
61
+
62
+ def in_env(self) -> Optional[str]:
63
+ """Session id from the environment, or "" if the environment proves the agent but not the id."""
64
+ for var in self.env_ids:
65
+ val = os.environ.get(var, "").strip()
66
+ if val and not val.startswith("${"):
67
+ return val
68
+ for var in self.env_presence:
69
+ if os.environ.get(var):
70
+ return ""
71
+ return None
72
+
73
+
74
+ def generic_label(name: str, kind: str, inp: object, paths: List[str]) -> str:
75
+ """Default one-line label. Built from full strings; the renderer redacts then truncates."""
76
+ d = inp if isinstance(inp, dict) else {}
77
+ if kind == "shell":
78
+ cmd = d.get("command") or d.get("cmd") or (inp if isinstance(inp, str) else "")
79
+ if isinstance(cmd, list):
80
+ cmd = "; ".join(str(c) for c in cmd)
81
+ desc = str(d.get("description") or "").strip()
82
+ return f"{name}: {desc or str(cmd).strip().split(chr(10))[0]}"
83
+ if kind in ("read", "edit", "write"):
84
+ target = paths[0] if paths else (d.get("filePath") or d.get("file_path") or d.get("path") or "")
85
+ return f"{name}: {target}"
86
+ if kind == "agent":
87
+ return f"{name}: {str(d.get('description') or d.get('prompt') or '')}"
88
+ if kind == "search":
89
+ return f"{name}: {str(d.get('pattern') or d.get('query') or d.get('path') or '')}"
90
+ if kind == "web":
91
+ return f"{name}: {str(d.get('url') or d.get('query') or d.get('prompt') or '')}"
92
+ if kind == "ask":
93
+ return f"{name}"
94
+ return name
95
+
96
+
97
+ def strip_wrappers(text: str, tags: Tuple[str, ...]) -> str:
98
+ """Remove <tag ...>…</tag> blocks the harness injects around user text."""
99
+ import re
100
+ for tag in tags:
101
+ text = re.sub(rf"<{tag}\b[^>]*>[\s\S]*?</{tag}>", "", text, flags=re.I)
102
+ return text.strip()
103
+
104
+
105
+ def recent(ref: SessionRef, minutes: int) -> bool:
106
+ if not ref.updated:
107
+ return True
108
+ return (dt.datetime.now(dt.timezone.utc) - ref.updated) <= dt.timedelta(minutes=minutes)