puenteo 0.4.0__py3-none-any.whl

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.
puenteo/__init__.py ADDED
@@ -0,0 +1,89 @@
1
+ """
2
+ Puenteo — the bridge between coding agents.
3
+
4
+ Install::
5
+
6
+ pip install puenteo
7
+ # CLI:
8
+ puenteo list
9
+ puenteo export <session_id> -f md -o chat.md
10
+
11
+ Library::
12
+
13
+ import puenteo
14
+
15
+ print(puenteo.status())
16
+ for s in puenteo.list_sessions(limit=10):
17
+ print(s.provider, s.session_id[:8], s.title)
18
+
19
+ puenteo.export_session("019f7a24", fmt="pdf", output="chat.pdf")
20
+ hits = puenteo.search("export markdown")
21
+ msgs = puenteo.pull("019f7a24", query="dmg", mode="query")
22
+
23
+ Providers: Claude Code, Codex, Grok, Pi.
24
+ Formats: md, txt, html, pdf, json, zip, csv, xml, yaml.
25
+ """
26
+
27
+ from .api import ( # noqa: F401
28
+ APP_NAME,
29
+ SUPPORTED_FORMATS,
30
+ Attachment,
31
+ Hit,
32
+ Message,
33
+ PROVIDER_NAMES,
34
+ RichMessage,
35
+ RichTranscript,
36
+ Session,
37
+ Transcript,
38
+ clipboard_text,
39
+ export_bytes,
40
+ export_session,
41
+ find_best_agent_transcript,
42
+ get_session,
43
+ list_sessions,
44
+ list_sources_for_cwd,
45
+ load,
46
+ load_rich_transcript,
47
+ load_transcript,
48
+ pull,
49
+ render,
50
+ resolve_session,
51
+ search,
52
+ search_all,
53
+ search_transcript,
54
+ smart_pull,
55
+ status,
56
+ )
57
+ from .version import __version__
58
+
59
+ __all__ = [
60
+ "APP_NAME",
61
+ "SUPPORTED_FORMATS",
62
+ "Attachment",
63
+ "Hit",
64
+ "Message",
65
+ "PROVIDER_NAMES",
66
+ "RichMessage",
67
+ "RichTranscript",
68
+ "Session",
69
+ "Transcript",
70
+ "__version__",
71
+ "clipboard_text",
72
+ "export_bytes",
73
+ "export_session",
74
+ "find_best_agent_transcript",
75
+ "get_session",
76
+ "list_sessions",
77
+ "list_sources_for_cwd",
78
+ "load",
79
+ "load_rich_transcript",
80
+ "load_transcript",
81
+ "pull",
82
+ "render",
83
+ "resolve_session",
84
+ "search",
85
+ "search_all",
86
+ "search_transcript",
87
+ "smart_pull",
88
+ "status",
89
+ ]
puenteo/__main__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())
puenteo/api.py ADDED
@@ -0,0 +1,286 @@
1
+ """
2
+ Public library API for Puenteo.
3
+
4
+ Prefer importing from the package root::
5
+
6
+ import puenteo
7
+
8
+ sessions = puenteo.list_sessions(limit=20)
9
+ path = puenteo.export_session(sessions[0].session_id, fmt="md", output="chat.md")
10
+ hits = puenteo.search("gatekeeper")
11
+ pack = puenteo.pull(sessions[0].session_id, query="dmg", mode="query")
12
+
13
+ CLI is the same package: ``puenteo`` / ``pto`` / ``python -m puenteo``.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import os
19
+ from pathlib import Path
20
+ from typing import Any, Dict, List, Optional, Sequence, Union
21
+
22
+ from .exporters import SUPPORTED_FORMATS, clipboard_text, render
23
+ from .extract import smart_pull
24
+ from .models import Message, Session, Transcript
25
+ from .providers import PROVIDER_NAMES, list_sessions, load_transcript, resolve_session
26
+ from .rich import (
27
+ find_best_agent_transcript,
28
+ list_sources_for_cwd,
29
+ load_transcript as load_rich_transcript,
30
+ )
31
+ from .search import Hit, search_all, search_transcript
32
+ from .version import APP_NAME, __version__
33
+
34
+ # Re-export rich models for export consumers (Terminal Dashboard, etc.)
35
+ from .rich import Attachment, Message as RichMessage, Transcript as RichTranscript
36
+
37
+
38
+ def status() -> Dict[str, Any]:
39
+ """Discover which agent stores exist and how many sessions each has."""
40
+ from pathlib import Path as P
41
+
42
+ homes = {
43
+ "claude_code": P.home() / ".claude" / "projects",
44
+ "codex": P.home() / ".codex" / "sessions",
45
+ "grok": P.home() / ".grok" / "sessions",
46
+ "pi": P.home() / ".pi" / "agent" / "sessions",
47
+ }
48
+ providers: Dict[str, Any] = {}
49
+ for name, path in homes.items():
50
+ exists = path.is_dir()
51
+ count = 0
52
+ if exists:
53
+ try:
54
+ count = len(list_sessions(providers=[name], limit=500))
55
+ except Exception:
56
+ count = 0
57
+ providers[name] = {
58
+ "path": str(path),
59
+ "exists": exists,
60
+ "sessions": count,
61
+ }
62
+ return {
63
+ "name": APP_NAME,
64
+ "version": __version__,
65
+ "role": "library+cli",
66
+ "providers": providers,
67
+ "export_formats": list(SUPPORTED_FORMATS),
68
+ "provider_names": list(PROVIDER_NAMES),
69
+ }
70
+
71
+
72
+ def get_session(
73
+ ref: str,
74
+ *,
75
+ providers: Optional[Sequence[str]] = None,
76
+ cwd: Optional[str] = None,
77
+ ) -> Optional[Session]:
78
+ """Resolve a session by id prefix, path, or title substring."""
79
+ prov = list(providers) if providers else None
80
+ return resolve_session(ref, providers=prov, cwd=cwd)
81
+
82
+
83
+ def load(
84
+ ref: Union[str, Session],
85
+ *,
86
+ rich: bool = True,
87
+ include_tools: bool = False,
88
+ include_thinking: bool = False,
89
+ providers: Optional[Sequence[str]] = None,
90
+ cwd: Optional[str] = None,
91
+ ) -> Union[Transcript, RichTranscript]:
92
+ """
93
+ Load a session transcript.
94
+
95
+ - ``rich=True`` (default): full export model (tools, attachments) via ``rich`` loaders
96
+ - ``rich=False``: lightweight messages for search / pull
97
+ """
98
+ sess = ref if isinstance(ref, Session) else get_session(ref, providers=providers, cwd=cwd)
99
+ if not sess:
100
+ raise LookupError(f"Session not found: {ref!r}")
101
+ if rich:
102
+ return load_rich_transcript(
103
+ sess.provider,
104
+ sess.path,
105
+ include_tools=include_tools,
106
+ include_thinking=include_thinking,
107
+ )
108
+ return load_transcript(sess, include_tools=include_tools)
109
+
110
+
111
+ def search(
112
+ query: str,
113
+ *,
114
+ session: Optional[str] = None,
115
+ providers: Optional[Sequence[str]] = None,
116
+ cwd: Optional[str] = None,
117
+ limit: int = 20,
118
+ session_limit: int = 40,
119
+ ) -> List[Hit]:
120
+ """Search message text across sessions (or one session)."""
121
+ prov = list(providers) if providers else None
122
+ if session:
123
+ sess = get_session(session, providers=prov, cwd=cwd)
124
+ if not sess:
125
+ return []
126
+ tr = load_transcript(sess)
127
+ return search_transcript(tr, query, limit=limit)
128
+ return search_all(
129
+ query,
130
+ providers=prov,
131
+ cwd=cwd,
132
+ session_limit=session_limit,
133
+ hit_limit=limit,
134
+ )
135
+
136
+
137
+ def pull(
138
+ ref: Union[str, Session],
139
+ *,
140
+ query: Optional[str] = None,
141
+ mode: str = "auto",
142
+ last: int = 0,
143
+ max_chars: int = 12000,
144
+ max_messages: int = 30,
145
+ include_tools: bool = False,
146
+ providers: Optional[Sequence[str]] = None,
147
+ cwd: Optional[str] = None,
148
+ ) -> List[Message]:
149
+ """Build a compact message pack for another agent (same as ``puenteo pull``)."""
150
+ sess = ref if isinstance(ref, Session) else get_session(ref, providers=providers, cwd=cwd)
151
+ if not sess:
152
+ raise LookupError(f"Session not found: {ref!r}")
153
+ tr = load_transcript(sess, include_tools=include_tools)
154
+ return smart_pull(
155
+ tr,
156
+ query=query,
157
+ mode=mode,
158
+ last=last,
159
+ max_chars=max_chars,
160
+ max_messages=max_messages,
161
+ )
162
+
163
+
164
+ def export_session(
165
+ ref: Union[str, Session],
166
+ *,
167
+ fmt: str = "md",
168
+ output: Optional[Union[str, Path]] = None,
169
+ include_tools: bool = False,
170
+ include_thinking: bool = False,
171
+ providers: Optional[Sequence[str]] = None,
172
+ cwd: Optional[str] = None,
173
+ ) -> Path:
174
+ """
175
+ Export a full transcript to a file.
176
+
177
+ ``fmt``: md | txt | html | pdf | json | zip | csv | xml | yaml | all
178
+
179
+ Returns the primary written path (or directory when ``fmt=all``).
180
+ """
181
+ sess = ref if isinstance(ref, Session) else get_session(ref, providers=providers, cwd=cwd)
182
+ if not sess:
183
+ raise LookupError(f"Session not found: {ref!r}")
184
+
185
+ rich_tr = load_rich_transcript(
186
+ sess.provider,
187
+ sess.path,
188
+ include_tools=include_tools,
189
+ include_thinking=include_thinking,
190
+ )
191
+
192
+ fmt_arg = (fmt or "md").lower().strip()
193
+ formats = list(SUPPORTED_FORMATS) if fmt_arg == "all" else [fmt_arg]
194
+ out = Path(os.path.expanduser(str(output))).resolve() if output else None
195
+
196
+ written: List[Path] = []
197
+ for f in formats:
198
+ data, _media, filename = render(
199
+ rich_tr,
200
+ f,
201
+ include_tools=include_tools,
202
+ include_thinking=include_thinking,
203
+ )
204
+ if out is None:
205
+ path = Path.cwd() / filename
206
+ elif out.is_dir() or fmt_arg == "all" or (out.suffix == "" and len(formats) > 1):
207
+ out.mkdir(parents=True, exist_ok=True)
208
+ path = out / filename if out.is_dir() or out.suffix == "" else out
209
+ if out.suffix == "" and not out.is_dir():
210
+ # treat as directory path to create
211
+ out.mkdir(parents=True, exist_ok=True)
212
+ path = out / filename
213
+ else:
214
+ path = out
215
+ path.parent.mkdir(parents=True, exist_ok=True)
216
+
217
+ path.write_bytes(data)
218
+ written.append(path)
219
+
220
+ if not written:
221
+ raise RuntimeError("Nothing exported")
222
+ return written[0] if len(written) == 1 else (out if out and out.is_dir() else written[0].parent)
223
+
224
+
225
+ def export_bytes(
226
+ ref: Union[str, Session],
227
+ *,
228
+ fmt: str = "md",
229
+ include_tools: bool = False,
230
+ include_thinking: bool = False,
231
+ providers: Optional[Sequence[str]] = None,
232
+ cwd: Optional[str] = None,
233
+ ) -> tuple:
234
+ """
235
+ Export without writing a file.
236
+
237
+ Returns ``(data: bytes, media_type: str, filename: str)``.
238
+ """
239
+ sess = ref if isinstance(ref, Session) else get_session(ref, providers=providers, cwd=cwd)
240
+ if not sess:
241
+ raise LookupError(f"Session not found: {ref!r}")
242
+ rich_tr = load_rich_transcript(
243
+ sess.provider,
244
+ sess.path,
245
+ include_tools=include_tools,
246
+ include_thinking=include_thinking,
247
+ )
248
+ return render(
249
+ rich_tr,
250
+ fmt,
251
+ include_tools=include_tools,
252
+ include_thinking=include_thinking,
253
+ )
254
+
255
+
256
+ __all__ = [
257
+ "APP_NAME",
258
+ "SUPPORTED_FORMATS",
259
+ "Attachment",
260
+ "Hit",
261
+ "Message",
262
+ "PROVIDER_NAMES",
263
+ "RichMessage",
264
+ "RichTranscript",
265
+ "Session",
266
+ "Transcript",
267
+ "__version__",
268
+ "clipboard_text",
269
+ "export_bytes",
270
+ "export_session",
271
+ "find_best_agent_transcript",
272
+ "get_session",
273
+ "list_sessions",
274
+ "list_sources_for_cwd",
275
+ "load",
276
+ "load_rich_transcript",
277
+ "load_transcript",
278
+ "pull",
279
+ "render",
280
+ "resolve_session",
281
+ "search",
282
+ "search_all",
283
+ "search_transcript",
284
+ "smart_pull",
285
+ "status",
286
+ ]
puenteo/bootstrap.py ADDED
@@ -0,0 +1,97 @@
1
+ """Path helpers so other tools can import puenteo without a global install."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import shutil
7
+ import sys
8
+ from pathlib import Path
9
+ from typing import List, Optional
10
+
11
+
12
+ def package_root() -> Path:
13
+ return Path(__file__).resolve().parent.parent
14
+
15
+
16
+ def ensure_importable() -> Path:
17
+ """Ensure ``puenteo`` is importable; return package root."""
18
+ root = package_root()
19
+ root_s = str(root)
20
+ if root_s not in sys.path:
21
+ sys.path.insert(0, root_s)
22
+ return root
23
+
24
+
25
+ def discover_roots() -> List[Path]:
26
+ """Candidate install locations (sibling repos, env, ~/dev)."""
27
+ roots: List[Path] = []
28
+ env = os.environ.get("PUENTEO_PATH") or os.environ.get("AGENT_SESSION_BRIDGE_PATH")
29
+ if env:
30
+ roots.append(Path(os.path.expanduser(env)))
31
+ roots.append(package_root())
32
+ roots.append(Path.home() / "dev" / "puenteo")
33
+ roots.append(Path.home() / "dev" / "agent-session-bridge") # legacy folder name
34
+
35
+ here = Path(__file__).resolve()
36
+ for p in here.parents:
37
+ for name in ("puenteo", "agent-session-bridge"):
38
+ sibling = p / name
39
+ if sibling.is_dir():
40
+ roots.append(sibling)
41
+
42
+ seen = set()
43
+ out: List[Path] = []
44
+ for r in roots:
45
+ try:
46
+ key = r.resolve()
47
+ except Exception:
48
+ key = r
49
+ if key in seen:
50
+ continue
51
+ seen.add(key)
52
+ if (r / "puenteo").is_dir() or (r / "pyproject.toml").is_file():
53
+ out.append(r)
54
+ return out
55
+
56
+
57
+ def which_puenteo() -> Optional[str]:
58
+ """Path to ``puenteo`` CLI if available (PATH or local venv)."""
59
+ for name in ("puenteo", "pto"):
60
+ found = shutil.which(name)
61
+ if found:
62
+ return found
63
+ for root in discover_roots():
64
+ for rel in (".venv/bin/puenteo", ".venv/bin/pto", "puenteo"):
65
+ cand = root / rel
66
+ if cand.is_file() and os.access(cand, os.X_OK):
67
+ return str(cand)
68
+ local = Path.home() / ".local" / "bin" / "puenteo"
69
+ if local.is_file() and os.access(local, os.X_OK):
70
+ return str(local)
71
+ return None
72
+
73
+
74
+ # Back-compat aliases
75
+ which_asb = which_puenteo
76
+
77
+
78
+ def puenteo_cli_hint() -> dict:
79
+ """Metadata for UIs that link to the CLI project."""
80
+ roots = discover_roots()
81
+ root = str(roots[0]) if roots else str(Path.home() / "dev" / "puenteo")
82
+ return {
83
+ "package": "puenteo",
84
+ "cli": "puenteo",
85
+ "path": root,
86
+ "binary": which_puenteo(),
87
+ "docs": (
88
+ "Use `puenteo list|search|pull|export` for cross-agent context. "
89
+ "Terminal Dashboard uses the same library for chat export."
90
+ ),
91
+ "github": "https://github.com/mano7onam/puenteo",
92
+ "pypi": "https://pypi.org/project/puenteo/",
93
+ "install": "pip install puenteo # or: uv add puenteo",
94
+ }
95
+
96
+
97
+ asb_cli_hint = puenteo_cli_hint