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 +89 -0
- puenteo/__main__.py +3 -0
- puenteo/api.py +286 -0
- puenteo/bootstrap.py +97 -0
- puenteo/cli.py +368 -0
- puenteo/exporters.py +676 -0
- puenteo/extract.py +260 -0
- puenteo/format.py +174 -0
- puenteo/models.py +44 -0
- puenteo/providers/__init__.py +105 -0
- puenteo/providers/claude.py +231 -0
- puenteo/providers/codex.py +223 -0
- puenteo/providers/grok.py +247 -0
- puenteo/providers/pi.py +184 -0
- puenteo/py.typed +0 -0
- puenteo/rich.py +790 -0
- puenteo/search.py +174 -0
- puenteo/util.py +157 -0
- puenteo/version.py +2 -0
- puenteo-0.4.0.dist-info/METADATA +132 -0
- puenteo-0.4.0.dist-info/RECORD +25 -0
- puenteo-0.4.0.dist-info/WHEEL +5 -0
- puenteo-0.4.0.dist-info/entry_points.txt +3 -0
- puenteo-0.4.0.dist-info/licenses/LICENSE +21 -0
- puenteo-0.4.0.dist-info/top_level.txt +1 -0
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
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
|