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.
- agent_sessions_cli-0.2.0/.gitignore +4 -0
- agent_sessions_cli-0.2.0/CHANGELOG.md +23 -0
- agent_sessions_cli-0.2.0/LICENSE +21 -0
- agent_sessions_cli-0.2.0/PKG-INFO +141 -0
- agent_sessions_cli-0.2.0/README.md +117 -0
- agent_sessions_cli-0.2.0/agent_sessions/__init__.py +3 -0
- agent_sessions_cli-0.2.0/agent_sessions/__main__.py +3 -0
- agent_sessions_cli-0.2.0/agent_sessions/adapters/__init__.py +136 -0
- agent_sessions_cli-0.2.0/agent_sessions/adapters/base.py +108 -0
- agent_sessions_cli-0.2.0/agent_sessions/adapters/claude_code.py +300 -0
- agent_sessions_cli-0.2.0/agent_sessions/adapters/codex.py +360 -0
- agent_sessions_cli-0.2.0/agent_sessions/adapters/gemini.py +176 -0
- agent_sessions_cli-0.2.0/agent_sessions/adapters/opencode.py +231 -0
- agent_sessions_cli-0.2.0/agent_sessions/cli.py +858 -0
- agent_sessions_cli-0.2.0/agent_sessions/core.py +748 -0
- agent_sessions_cli-0.2.0/agent_sessions/model.py +119 -0
- agent_sessions_cli-0.2.0/agent_sessions/templates/gitattributes +2 -0
- agent_sessions_cli-0.2.0/agent_sessions/templates/sessions-README.md +31 -0
- agent_sessions_cli-0.2.0/agent_sessions/templates/summary.md +31 -0
- agent_sessions_cli-0.2.0/agents-skills/agent-sessions/scripts/agent_sessions/__init__.py +3 -0
- agent_sessions_cli-0.2.0/agents-skills/agent-sessions/scripts/agent_sessions/__main__.py +3 -0
- agent_sessions_cli-0.2.0/agents-skills/agent-sessions/scripts/agent_sessions/adapters/__init__.py +136 -0
- agent_sessions_cli-0.2.0/agents-skills/agent-sessions/scripts/agent_sessions/adapters/base.py +108 -0
- agent_sessions_cli-0.2.0/agents-skills/agent-sessions/scripts/agent_sessions/adapters/claude_code.py +300 -0
- agent_sessions_cli-0.2.0/agents-skills/agent-sessions/scripts/agent_sessions/adapters/codex.py +360 -0
- agent_sessions_cli-0.2.0/agents-skills/agent-sessions/scripts/agent_sessions/adapters/gemini.py +176 -0
- agent_sessions_cli-0.2.0/agents-skills/agent-sessions/scripts/agent_sessions/adapters/opencode.py +231 -0
- agent_sessions_cli-0.2.0/agents-skills/agent-sessions/scripts/agent_sessions/cli.py +858 -0
- agent_sessions_cli-0.2.0/agents-skills/agent-sessions/scripts/agent_sessions/core.py +748 -0
- agent_sessions_cli-0.2.0/agents-skills/agent-sessions/scripts/agent_sessions/model.py +119 -0
- agent_sessions_cli-0.2.0/agents-skills/agent-sessions/scripts/agent_sessions/templates/gitattributes +2 -0
- agent_sessions_cli-0.2.0/agents-skills/agent-sessions/scripts/agent_sessions/templates/sessions-README.md +31 -0
- agent_sessions_cli-0.2.0/agents-skills/agent-sessions/scripts/agent_sessions/templates/summary.md +31 -0
- agent_sessions_cli-0.2.0/pyproject.toml +43 -0
|
@@ -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,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)
|