ctxttools 0.3.0__tar.gz → 0.3.1__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 (32) hide show
  1. {ctxttools-0.3.0/ctxttools.egg-info → ctxttools-0.3.1}/PKG-INFO +73 -13
  2. {ctxttools-0.3.0 → ctxttools-0.3.1}/README.md +71 -11
  3. {ctxttools-0.3.0 → ctxttools-0.3.1}/ctools/backup.py +10 -7
  4. {ctxttools-0.3.0 → ctxttools-0.3.1}/ctools/ccat.py +2 -0
  5. {ctxttools-0.3.0 → ctxttools-0.3.1}/ctools/ccopy.py +2 -0
  6. {ctxttools-0.3.0 → ctxttools-0.3.1}/ctools/cdir.py +135 -54
  7. {ctxttools-0.3.0 → ctxttools-0.3.1}/ctools/cdu.py +45 -8
  8. {ctxttools-0.3.0 → ctxttools-0.3.1}/ctools/cextract.py +74 -5
  9. {ctxttools-0.3.0 → ctxttools-0.3.1}/ctools/cgrep.py +2 -0
  10. {ctxttools-0.3.0 → ctxttools-0.3.1}/ctools/crecover.py +3 -3
  11. {ctxttools-0.3.0 → ctxttools-0.3.1}/ctools/crm.py +2 -0
  12. ctxttools-0.3.1/ctools/ctools-web.py +30 -0
  13. {ctxttools-0.3.0 → ctxttools-0.3.1}/ctools/ctools_mcp.py +162 -0
  14. {ctxttools-0.3.0 → ctxttools-0.3.1}/ctools/filterlib.py +31 -0
  15. {ctxttools-0.3.0 → ctxttools-0.3.1}/ctools/lib.py +25 -0
  16. {ctxttools-0.3.0 → ctxttools-0.3.1}/ctools/strategy.py +19 -2
  17. {ctxttools-0.3.0 → ctxttools-0.3.1}/ctools/webui.py +1307 -211
  18. {ctxttools-0.3.0 → ctxttools-0.3.1/ctxttools.egg-info}/PKG-INFO +73 -13
  19. {ctxttools-0.3.0 → ctxttools-0.3.1}/ctxttools.egg-info/SOURCES.txt +1 -0
  20. {ctxttools-0.3.0 → ctxttools-0.3.1}/ctxttools.egg-info/requires.txt +1 -1
  21. {ctxttools-0.3.0 → ctxttools-0.3.1}/pyproject.toml +2 -2
  22. {ctxttools-0.3.0 → ctxttools-0.3.1}/LICENSE.MIT +0 -0
  23. {ctxttools-0.3.0 → ctxttools-0.3.1}/ctools/__init__.py +0 -0
  24. {ctxttools-0.3.0 → ctxttools-0.3.1}/ctools/agents.py +0 -0
  25. {ctxttools-0.3.0 → ctxttools-0.3.1}/ctools/cconnect.py +0 -0
  26. {ctxttools-0.3.0 → ctxttools-0.3.1}/ctools/cli.py +0 -0
  27. {ctxttools-0.3.0 → ctxttools-0.3.1}/ctools/log.py +0 -0
  28. {ctxttools-0.3.0 → ctxttools-0.3.1}/ctools/testing.py +0 -0
  29. {ctxttools-0.3.0 → ctxttools-0.3.1}/ctxttools.egg-info/dependency_links.txt +0 -0
  30. {ctxttools-0.3.0 → ctxttools-0.3.1}/ctxttools.egg-info/entry_points.txt +0 -0
  31. {ctxttools-0.3.0 → ctxttools-0.3.1}/ctxttools.egg-info/top_level.txt +0 -0
  32. {ctxttools-0.3.0 → ctxttools-0.3.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ctxttools
3
- Version: 0.3.0
3
+ Version: 0.3.1
4
4
  Summary: CLI tools for LLM conversations
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
@@ -9,7 +9,7 @@ Requires-Dist: rich>=13.0.0
9
9
  Requires-Dist: tiktoken>=0.5.0
10
10
  Requires-Dist: structlog>=24.0
11
11
  Provides-Extra: mcp
12
- Requires-Dist: mcp>=1.2.0; extra == "mcp"
12
+ Requires-Dist: mcp<2.0,>=1.2.0; extra == "mcp"
13
13
  Provides-Extra: dev
14
14
  Requires-Dist: pytest>=7.0.0; extra == "dev"
15
15
  Dynamic: license-file
@@ -65,6 +65,7 @@ As you can see, these are unix-friendly tools using simple paradigms that you ar
65
65
  | `cdir` | List sessions (ls for your history) | [docs/cdir.md](docs/cdir.md) |
66
66
  | `ccat` | Print one or more conversations (cat for your history) | [docs/ccat.md](docs/ccat.md) |
67
67
  | `ccopy` | Copy a whole conversation from one agent to a new session in another | [docs/ccopy.md](docs/ccopy.md) |
68
+ | `crecover` | List, verify, and restore the storage backups ccopy takes before each write | [docs/crecover.md](docs/crecover.md) |
68
69
  | `cextract` | Extract concepts from a session into files, and inject them back | [docs/cextract.md](docs/cextract.md) |
69
70
  | `cconnect` | Live concept pipelines between sessions | [docs/cconnect.md](docs/cconnect.md) |
70
71
  | `cdu` | Token usage per session (du for context windows) | [docs/cdu.md](docs/cdu.md) |
@@ -104,6 +105,32 @@ graph LR
104
105
 
105
106
  Context windows are endpoints: opencode, Claude Code, Codex, Pi. They all speak different protocols, but they all consume the same packets. See [cextract](docs/cextract.md) for the concept packet format, strategies, and the filter configs (decision model and JSON-RPC).
106
107
 
108
+ ## Write safety
109
+
110
+ The session-mover tools write into your agents' **live** storage — for the
111
+ SQLite-backed agents (opencode, goose, kilo, hermes) that's the database that
112
+ holds every session of that agent. A write that fails or is interrupted
113
+ part-way can corrupt it, and losing an agent's whole history would be
114
+ unforgivable.
115
+
116
+ So every local write is guarded:
117
+
118
+ 1. ctools **snapshots the agent's storage first** (SQLite via `VACUUM INTO` —
119
+ a consistent copy even mid-write; file agents via a tree copy), kept under
120
+ `~/.local/share/ctools/backups/<agent>/` (20 newest per agent).
121
+ 2. After the write it **verifies** the storage: the DB opens,
122
+ `PRAGMA integrity_check` passes, the session table still exists, and the
123
+ session count did not decrease. A database that is structurally valid but
124
+ emptied of data is caught too.
125
+ 3. If anything looks wrong, the snapshot is **restored automatically** — you
126
+ get a clean error, not a broken database.
127
+
128
+ And because a restore is itself a write, it snapshots what it overwrites, so
129
+ **every restore is undoable**. For the out-of-band case (you notice days later
130
+ that an agent's history vanished), `crecover list` / `verify` / `restore` are
131
+ there. Details: [ccopy — safety](docs/ccopy.md#safety-automatic-backups) and
132
+ [crecover](docs/crecover.md).
133
+
107
134
  ## Supported endpoints
108
135
 
109
136
  | Agent | Storage |
@@ -150,23 +177,47 @@ Anything the storage cannot do — pi's session trees cannot be rewritten in pla
150
177
 
151
178
  ## MCP server
152
179
 
153
- There is an MCP server for use from Claude, opencode, Cursor, or anything else that speaks MCP.
180
+ There is an MCP server for use from pi, Claude, opencode, Cursor, or anything else that speaks MCP.
154
181
 
155
182
  Tools: `list_agents`, `list_sessions`, `search_sessions`, `export_session`, `extract_concepts`, `copy_concepts`, `get_session_concepts`.
156
183
 
157
- Add to your MCP config:
184
+ ### Quick install (pi)
185
+
186
+ One command does the whole job — install into an isolated venv (if needed) and register with pi:
187
+
188
+ ```sh
189
+ pipx run 'ctxttools[mcp]' ctools-mcp install
190
+ # or, with uv:
191
+ uv tool run 'ctxttools[mcp]' ctools-mcp install
192
+ ```
193
+
194
+ It detects an existing `uv tool` or `pipx` install of `ctxttools` and uses it instead. Verify with:
195
+
196
+ ```sh
197
+ pi mcp list # expect: ctools: connected, 7 tools
198
+ ```
199
+
200
+ Then use it inside pi — run `/mcp` to inspect, or just ask the agent (the tools are also reachable through codemode / tool search).
201
+
202
+ Uninstall with `ctools-mcp uninstall` (removes the pi entry; pass `--all` to also remove Claude Code and opencode entries).
203
+
204
+ ### Other clients
205
+
206
+ Run `ctools-mcp print-config` to get the JSON snippet for any MCP host, e.g. for opencode (`opencode.json` under `"mcp"`) or Claude (`.mcp.json`):
158
207
 
159
208
  ```json
160
209
  {
161
210
  "mcpServers": {
162
211
  "ctools": {
163
212
  "command": "python",
164
- "args": ["/ABSOLUTE/PATH/TO/ctools/ctools_mcp.py"]
213
+ "args": ["-m", "ctools.ctools_mcp"]
165
214
  }
166
215
  }
167
216
  }
168
217
  ```
169
218
 
219
+ (`command` should point at the interpreter of a venv that has `ctxttools[mcp]` installed — `ctools-mcp install` prints the exact one for your machine.)
220
+
170
221
  ## Web dashboard
171
222
 
172
223
  Prefer a mouse to a man page? ctools ships a browser dashboard — a small local
@@ -174,26 +225,35 @@ web server (Python stdlib only, no web dependencies) with the same library
174
225
  underneath. Same on macOS, Windows, and Linux:
175
226
 
176
227
  ```sh
177
- $ ctools-web # or: python -m ctools.webui
228
+ $ ctools-web # installed command
229
+ # or, straight from a checkout, no install needed:
230
+ $ python3 ctools/ctools-web.py # or: python -m ctools.webui
178
231
  ctools 0.2.1 dashboard: http://127.0.0.1:8765
179
232
  ```
180
233
 
181
- Open the URL. You get four views:
234
+ Open the URL. The navbar has **Overview**, **Sessions**, and **Concepts**, plus
235
+ a search box (press <b>/</b>) and a power button to stop the server. Clicking a
236
+ session anywhere opens its **Conversation** view:
182
237
 
183
238
  - **Overview** — every supported agent, installed or not, with session counts
184
239
  and total tokens, and a jump into each agent's sessions.
185
- - **Search** — `cgrep` in the browser: one regex box, per-agent chips, live
186
- matches; click any match to open that conversation.
187
- - **Sessions** — `cdir` for one agent: newest, largest, or by token usage.
188
- - **Conversation** — `ccat` with the source session's messages, token usage,
189
- the resume command, concept extraction (`cextract`), a markdown export,
190
- and **Copy** — `ccopy` to any other installed agent, which creates a fresh
240
+ - **Search** — `cgrep` in the browser: one box, searches session ids, names,
241
+ paths <b>and</b> the content of every conversation at once; click a match to
242
+ open that conversation.
243
+ - **Sessions** — `cdir` for one agent (or all of them): newest, largest, or by
244
+ token usage, filterable.
245
+ - **Conversation** — `ccat` with the session's messages, token usage, the
246
+ resume command, concept extraction (`cextract`), a markdown export, and
247
+ **Copy** — `ccopy` to any other installed agent, which creates a fresh
191
248
  session there (source untouched) and tells you how to resume it.
192
249
 
193
250
  The server binds to `127.0.0.1` by default, so nothing is exposed to your
194
251
  network unless you pass `--host 0.0.0.0`. There is one write operation:
195
252
  Copy. Everything else is read-only against the agents' storage.
196
253
 
254
+ Stop it with **Ctrl-C**, or with the **power button** (⏻) in the top-right of
255
+ the page — it asks for confirmation, then shuts the server down cleanly.
256
+
197
257
  ```sh
198
258
  ctools-web --port 9000
199
259
  ```
@@ -49,6 +49,7 @@ As you can see, these are unix-friendly tools using simple paradigms that you ar
49
49
  | `cdir` | List sessions (ls for your history) | [docs/cdir.md](docs/cdir.md) |
50
50
  | `ccat` | Print one or more conversations (cat for your history) | [docs/ccat.md](docs/ccat.md) |
51
51
  | `ccopy` | Copy a whole conversation from one agent to a new session in another | [docs/ccopy.md](docs/ccopy.md) |
52
+ | `crecover` | List, verify, and restore the storage backups ccopy takes before each write | [docs/crecover.md](docs/crecover.md) |
52
53
  | `cextract` | Extract concepts from a session into files, and inject them back | [docs/cextract.md](docs/cextract.md) |
53
54
  | `cconnect` | Live concept pipelines between sessions | [docs/cconnect.md](docs/cconnect.md) |
54
55
  | `cdu` | Token usage per session (du for context windows) | [docs/cdu.md](docs/cdu.md) |
@@ -88,6 +89,32 @@ graph LR
88
89
 
89
90
  Context windows are endpoints: opencode, Claude Code, Codex, Pi. They all speak different protocols, but they all consume the same packets. See [cextract](docs/cextract.md) for the concept packet format, strategies, and the filter configs (decision model and JSON-RPC).
90
91
 
92
+ ## Write safety
93
+
94
+ The session-mover tools write into your agents' **live** storage — for the
95
+ SQLite-backed agents (opencode, goose, kilo, hermes) that's the database that
96
+ holds every session of that agent. A write that fails or is interrupted
97
+ part-way can corrupt it, and losing an agent's whole history would be
98
+ unforgivable.
99
+
100
+ So every local write is guarded:
101
+
102
+ 1. ctools **snapshots the agent's storage first** (SQLite via `VACUUM INTO` —
103
+ a consistent copy even mid-write; file agents via a tree copy), kept under
104
+ `~/.local/share/ctools/backups/<agent>/` (20 newest per agent).
105
+ 2. After the write it **verifies** the storage: the DB opens,
106
+ `PRAGMA integrity_check` passes, the session table still exists, and the
107
+ session count did not decrease. A database that is structurally valid but
108
+ emptied of data is caught too.
109
+ 3. If anything looks wrong, the snapshot is **restored automatically** — you
110
+ get a clean error, not a broken database.
111
+
112
+ And because a restore is itself a write, it snapshots what it overwrites, so
113
+ **every restore is undoable**. For the out-of-band case (you notice days later
114
+ that an agent's history vanished), `crecover list` / `verify` / `restore` are
115
+ there. Details: [ccopy — safety](docs/ccopy.md#safety-automatic-backups) and
116
+ [crecover](docs/crecover.md).
117
+
91
118
  ## Supported endpoints
92
119
 
93
120
  | Agent | Storage |
@@ -134,23 +161,47 @@ Anything the storage cannot do — pi's session trees cannot be rewritten in pla
134
161
 
135
162
  ## MCP server
136
163
 
137
- There is an MCP server for use from Claude, opencode, Cursor, or anything else that speaks MCP.
164
+ There is an MCP server for use from pi, Claude, opencode, Cursor, or anything else that speaks MCP.
138
165
 
139
166
  Tools: `list_agents`, `list_sessions`, `search_sessions`, `export_session`, `extract_concepts`, `copy_concepts`, `get_session_concepts`.
140
167
 
141
- Add to your MCP config:
168
+ ### Quick install (pi)
169
+
170
+ One command does the whole job — install into an isolated venv (if needed) and register with pi:
171
+
172
+ ```sh
173
+ pipx run 'ctxttools[mcp]' ctools-mcp install
174
+ # or, with uv:
175
+ uv tool run 'ctxttools[mcp]' ctools-mcp install
176
+ ```
177
+
178
+ It detects an existing `uv tool` or `pipx` install of `ctxttools` and uses it instead. Verify with:
179
+
180
+ ```sh
181
+ pi mcp list # expect: ctools: connected, 7 tools
182
+ ```
183
+
184
+ Then use it inside pi — run `/mcp` to inspect, or just ask the agent (the tools are also reachable through codemode / tool search).
185
+
186
+ Uninstall with `ctools-mcp uninstall` (removes the pi entry; pass `--all` to also remove Claude Code and opencode entries).
187
+
188
+ ### Other clients
189
+
190
+ Run `ctools-mcp print-config` to get the JSON snippet for any MCP host, e.g. for opencode (`opencode.json` under `"mcp"`) or Claude (`.mcp.json`):
142
191
 
143
192
  ```json
144
193
  {
145
194
  "mcpServers": {
146
195
  "ctools": {
147
196
  "command": "python",
148
- "args": ["/ABSOLUTE/PATH/TO/ctools/ctools_mcp.py"]
197
+ "args": ["-m", "ctools.ctools_mcp"]
149
198
  }
150
199
  }
151
200
  }
152
201
  ```
153
202
 
203
+ (`command` should point at the interpreter of a venv that has `ctxttools[mcp]` installed — `ctools-mcp install` prints the exact one for your machine.)
204
+
154
205
  ## Web dashboard
155
206
 
156
207
  Prefer a mouse to a man page? ctools ships a browser dashboard — a small local
@@ -158,26 +209,35 @@ web server (Python stdlib only, no web dependencies) with the same library
158
209
  underneath. Same on macOS, Windows, and Linux:
159
210
 
160
211
  ```sh
161
- $ ctools-web # or: python -m ctools.webui
212
+ $ ctools-web # installed command
213
+ # or, straight from a checkout, no install needed:
214
+ $ python3 ctools/ctools-web.py # or: python -m ctools.webui
162
215
  ctools 0.2.1 dashboard: http://127.0.0.1:8765
163
216
  ```
164
217
 
165
- Open the URL. You get four views:
218
+ Open the URL. The navbar has **Overview**, **Sessions**, and **Concepts**, plus
219
+ a search box (press <b>/</b>) and a power button to stop the server. Clicking a
220
+ session anywhere opens its **Conversation** view:
166
221
 
167
222
  - **Overview** — every supported agent, installed or not, with session counts
168
223
  and total tokens, and a jump into each agent's sessions.
169
- - **Search** — `cgrep` in the browser: one regex box, per-agent chips, live
170
- matches; click any match to open that conversation.
171
- - **Sessions** — `cdir` for one agent: newest, largest, or by token usage.
172
- - **Conversation** — `ccat` with the source session's messages, token usage,
173
- the resume command, concept extraction (`cextract`), a markdown export,
174
- and **Copy** — `ccopy` to any other installed agent, which creates a fresh
224
+ - **Search** — `cgrep` in the browser: one box, searches session ids, names,
225
+ paths <b>and</b> the content of every conversation at once; click a match to
226
+ open that conversation.
227
+ - **Sessions** — `cdir` for one agent (or all of them): newest, largest, or by
228
+ token usage, filterable.
229
+ - **Conversation** — `ccat` with the session's messages, token usage, the
230
+ resume command, concept extraction (`cextract`), a markdown export, and
231
+ **Copy** — `ccopy` to any other installed agent, which creates a fresh
175
232
  session there (source untouched) and tells you how to resume it.
176
233
 
177
234
  The server binds to `127.0.0.1` by default, so nothing is exposed to your
178
235
  network unless you pass `--host 0.0.0.0`. There is one write operation:
179
236
  Copy. Everything else is read-only against the agents' storage.
180
237
 
238
+ Stop it with **Ctrl-C**, or with the **power button** (⏻) in the top-right of
239
+ the page — it asks for confirmation, then shuts the server down cleanly.
240
+
181
241
  ```sh
182
242
  ctools-web --port 9000
183
243
  ```
@@ -27,12 +27,12 @@ import json
27
27
  import shutil
28
28
  import sqlite3
29
29
  import time
30
- from dataclasses import dataclass, field
30
+ from dataclasses import dataclass
31
31
  from datetime import datetime
32
32
  from pathlib import Path
33
33
  from typing import List, Optional
34
34
 
35
- from ctools.agents import Agent, AgentError
35
+ from ctools.agents import Agent
36
36
 
37
37
  __all__ = [
38
38
  "BackupError", "Backup", "Backups",
@@ -194,14 +194,15 @@ def _session_table(agent: Agent) -> Optional[str]:
194
194
  try:
195
195
  conn = sqlite3.connect(str(agent.storage_path))
196
196
  try:
197
- conn.execute(
197
+ row = conn.execute(
198
198
  "SELECT name FROM sqlite_master WHERE type='table' AND name=?",
199
199
  (name,)).fetchone()
200
- return name
201
200
  finally:
202
201
  conn.close()
203
202
  except sqlite3.Error:
204
- continue
203
+ row = None
204
+ if row is not None:
205
+ return name
205
206
  return None
206
207
 
207
208
 
@@ -342,7 +343,9 @@ class Backups:
342
343
  out.append(Backup.load(child))
343
344
  except (OSError, json.JSONDecodeError, KeyError):
344
345
  continue # skip a torn/corrupt manifest
345
- out.sort(key=lambda b: b.when, reverse=True)
346
+ # `when` has second precision; the dir name encodes milliseconds and
347
+ # a collision counter, so it breaks same-second ties in creation order.
348
+ out.sort(key=lambda b: (b.when, b.path.name), reverse=True)
346
349
  return out
347
350
 
348
351
  def all(self) -> List[Backup]:
@@ -351,7 +354,7 @@ class Backups:
351
354
  for child in self.root.iterdir():
352
355
  if child.is_dir():
353
356
  out.extend(self.for_agent(child.name))
354
- out.sort(key=lambda b: b.when, reverse=True)
357
+ out.sort(key=lambda b: (b.when, b.path.name), reverse=True)
355
358
  return out
356
359
 
357
360
  def prune(self, agent_name: str, keep: int) -> int:
@@ -37,6 +37,7 @@ from rich.console import Console
37
37
  from rich.text import Text
38
38
 
39
39
  from ctools.agents import REGISTRY as AGENTS, SessionNotFound, get_agent
40
+ from ctools.lib import install_clean_sigint
40
41
  from ctools.cli import (handle_version, parse_ref, parse_remote_ref, reporting,
41
42
  version_option)
42
43
  from ctools.ccopy import _local_agent, _pull, _remote_common_doc, _remote_has_ctools
@@ -210,6 +211,7 @@ def build_parser() -> argparse.ArgumentParser:
210
211
 
211
212
 
212
213
  def main(argv=None) -> int:
214
+ install_clean_sigint()
213
215
  args = build_parser().parse_args(argv)
214
216
  if (v := handle_version(args)) is not None:
215
217
  return v
@@ -38,6 +38,7 @@ from ctools.agents import (
38
38
  Agent, Message, REGISTRY,
39
39
  get_agent, get_resume_command, SessionNotFound, UnsupportedOperation,
40
40
  )
41
+ from ctools.lib import install_clean_sigint
41
42
  from ctools.cli import (Remote, handle_version, parse_ref, parse_remote_ref,
42
43
  version_option)
43
44
  from ctools.backup import guard_write, BackupError
@@ -475,6 +476,7 @@ def build_parser() -> argparse.ArgumentParser:
475
476
 
476
477
 
477
478
  def main(argv=None) -> int:
479
+ install_clean_sigint()
478
480
  args = build_parser().parse_args(argv)
479
481
  if (v := handle_version(args)) is not None:
480
482
  return v