converseek 0.2.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.
- converseek/__init__.py +2 -0
- converseek/__main__.py +473 -0
- converseek/adapters/__init__.py +0 -0
- converseek/adapters/antigravity.py +303 -0
- converseek/adapters/claude_code.py +282 -0
- converseek/adapters/cursor.py +426 -0
- converseek/adapters/hermes.py +169 -0
- converseek/adapters/opencode.py +232 -0
- converseek/adapters/paseo.py +164 -0
- converseek/adapters/zcode.py +147 -0
- converseek/base.py +83 -0
- converseek-0.2.0.dist-info/METADATA +5 -0
- converseek-0.2.0.dist-info/RECORD +15 -0
- converseek-0.2.0.dist-info/WHEEL +4 -0
- converseek-0.2.0.dist-info/entry_points.txt +2 -0
converseek/__init__.py
ADDED
converseek/__main__.py
ADDED
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""converseek — cross-tool conversation search, browse, and export.
|
|
3
|
+
|
|
4
|
+
Search and read sessions from 7 AI coding tools:
|
|
5
|
+
claude, hermes, opencode, paseo, zcode, cursor, antigravity
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
converseek list [--tool TOOL] [--limit N] [--since DATE] [--project PATH]
|
|
9
|
+
converseek search QUERY [--tool TOOL] [--limit N] [--project PATH]
|
|
10
|
+
converseek show TOOL:SESSION_ID [--window N]
|
|
11
|
+
converseek export TOOL:SESSION_ID [-o FILE]
|
|
12
|
+
converseek projects
|
|
13
|
+
converseek tools
|
|
14
|
+
|
|
15
|
+
Examples:
|
|
16
|
+
converseek list --limit 10
|
|
17
|
+
converseek search "docker networking"
|
|
18
|
+
converseek search "auth refactor" --tool claude,hermes
|
|
19
|
+
converseek show hermes:20260620_201309_a8e8cb95
|
|
20
|
+
converseek export hermes:20260620_201309_a8e8cb95
|
|
21
|
+
converseek tools
|
|
22
|
+
"""
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import argparse
|
|
26
|
+
import sys
|
|
27
|
+
from collections import Counter
|
|
28
|
+
from datetime import datetime
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
|
|
31
|
+
from .adapters.claude_code import ClaudeCodeAdapter
|
|
32
|
+
from .adapters.hermes import HermesAdapter
|
|
33
|
+
from .adapters.opencode import OpenCodeAdapter
|
|
34
|
+
from .adapters.paseo import PaseoAdapter
|
|
35
|
+
from .adapters.zcode import ZCodeAdapter
|
|
36
|
+
from .adapters.cursor import CursorAdapter
|
|
37
|
+
from .adapters.antigravity import AntigravityAdapter
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
ADAPTERS = {
|
|
41
|
+
"claude": ClaudeCodeAdapter,
|
|
42
|
+
"hermes": HermesAdapter,
|
|
43
|
+
"opencode": OpenCodeAdapter,
|
|
44
|
+
"paseo": PaseoAdapter,
|
|
45
|
+
"zcode": ZCodeAdapter,
|
|
46
|
+
"cursor": CursorAdapter,
|
|
47
|
+
"antigravity": AntigravityAdapter,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def get_adapters(tools: str | None = None) -> list:
|
|
52
|
+
"""Get adapter instances for specified tools (or all available)."""
|
|
53
|
+
if tools:
|
|
54
|
+
names = [t.strip() for t in tools.split(",")]
|
|
55
|
+
else:
|
|
56
|
+
names = list(ADAPTERS.keys())
|
|
57
|
+
instances = []
|
|
58
|
+
for name in names:
|
|
59
|
+
cls = ADAPTERS.get(name)
|
|
60
|
+
if not cls:
|
|
61
|
+
print(f"Warning: unknown tool '{name}'", file=sys.stderr)
|
|
62
|
+
continue
|
|
63
|
+
instance = cls()
|
|
64
|
+
if instance.is_available():
|
|
65
|
+
instances.append((name, instance))
|
|
66
|
+
else:
|
|
67
|
+
print(f"Info: '{name}' data not found, skipping", file=sys.stderr)
|
|
68
|
+
return instances
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _fmt_time(ts: float) -> str:
|
|
72
|
+
if not ts:
|
|
73
|
+
return "?"
|
|
74
|
+
return datetime.fromtimestamp(ts).strftime("%Y-%m-%d %H:%M")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _with_timeout(fn, adapter_name: str, *args, **kwargs):
|
|
78
|
+
"""Run fn with a 15s SIGALRM timeout. Returns (result, timed_out)."""
|
|
79
|
+
import signal
|
|
80
|
+
|
|
81
|
+
def _timeout_handler(signum, frame):
|
|
82
|
+
raise TimeoutError()
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
old_handler = signal.signal(signal.SIGALRM, _timeout_handler)
|
|
86
|
+
signal.alarm(15)
|
|
87
|
+
try:
|
|
88
|
+
return fn(*args, **kwargs), False
|
|
89
|
+
finally:
|
|
90
|
+
signal.alarm(0)
|
|
91
|
+
signal.signal(signal.SIGALRM, old_handler)
|
|
92
|
+
except TimeoutError:
|
|
93
|
+
print(f"Warning: '{adapter_name}' timed out, skipping", file=sys.stderr)
|
|
94
|
+
return [], True
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _project_name(cwd: str | None) -> str:
|
|
98
|
+
"""Extract a short project name from a cwd path."""
|
|
99
|
+
if not cwd:
|
|
100
|
+
return "(unknown)"
|
|
101
|
+
p = Path(cwd)
|
|
102
|
+
# Use the last meaningful segment
|
|
103
|
+
if p.name:
|
|
104
|
+
return str(p)
|
|
105
|
+
return cwd
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _matches_project(cwd: str | None, project: str) -> bool:
|
|
109
|
+
"""Check if a session's cwd matches the project filter.
|
|
110
|
+
|
|
111
|
+
Matches if the project string appears anywhere in the cwd path
|
|
112
|
+
(case-insensitive), or if cwd ends with the project string.
|
|
113
|
+
"""
|
|
114
|
+
if not cwd:
|
|
115
|
+
return False
|
|
116
|
+
return project.lower() in cwd.lower()
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def cmd_projects(args):
|
|
120
|
+
"""List all unique project directories with session counts."""
|
|
121
|
+
adapters = get_adapters(args.tool)
|
|
122
|
+
project_counter: Counter = Counter() # {cwd: total_count}
|
|
123
|
+
|
|
124
|
+
for name, adapter in adapters:
|
|
125
|
+
sessions, _ = _with_timeout(
|
|
126
|
+
adapter.list_sessions, name, limit=9999
|
|
127
|
+
)
|
|
128
|
+
for s in sessions:
|
|
129
|
+
if s.cwd:
|
|
130
|
+
project_counter[s.cwd] += 1
|
|
131
|
+
|
|
132
|
+
if not project_counter:
|
|
133
|
+
print("No project data found.")
|
|
134
|
+
return 0
|
|
135
|
+
|
|
136
|
+
# Sort by session count descending
|
|
137
|
+
sorted_projects = project_counter.most_common()
|
|
138
|
+
|
|
139
|
+
# Apply --limit
|
|
140
|
+
if args.limit:
|
|
141
|
+
sorted_projects = sorted_projects[: args.limit]
|
|
142
|
+
|
|
143
|
+
print(f"Found {len(project_counter)} unique projects:\n")
|
|
144
|
+
print(f"{'SESSIONS':>8} PROJECT PATH")
|
|
145
|
+
print("-" * 100)
|
|
146
|
+
for cwd, count in sorted_projects:
|
|
147
|
+
print(f"{count:>8} {cwd}")
|
|
148
|
+
|
|
149
|
+
print(f"\nUse --project <path> with list/search to filter by project.")
|
|
150
|
+
return 0
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def cmd_list(args):
|
|
154
|
+
since = None
|
|
155
|
+
if args.since:
|
|
156
|
+
try:
|
|
157
|
+
since = datetime.fromisoformat(args.since).timestamp()
|
|
158
|
+
except ValueError:
|
|
159
|
+
print(f"Error: invalid date format '{args.since}', use YYYY-MM-DD", file=sys.stderr)
|
|
160
|
+
return 1
|
|
161
|
+
|
|
162
|
+
adapters = get_adapters(args.tool)
|
|
163
|
+
all_sessions = []
|
|
164
|
+
|
|
165
|
+
# --project is a CLI-side post-filter, so the per-adapter limit must NOT
|
|
166
|
+
# truncate before filtering (otherwise we'd only ever filter the globally
|
|
167
|
+
# most-recent `limit` sessions). Fetch a large pool when filtering.
|
|
168
|
+
fetch_limit = 100000 if args.project else args.limit
|
|
169
|
+
|
|
170
|
+
for name, adapter in adapters:
|
|
171
|
+
sessions, _ = _with_timeout(
|
|
172
|
+
adapter.list_sessions, name,
|
|
173
|
+
limit=fetch_limit, since=since, cwd=args.cwd
|
|
174
|
+
)
|
|
175
|
+
all_sessions.extend(sessions)
|
|
176
|
+
|
|
177
|
+
# Filter by project
|
|
178
|
+
if args.project:
|
|
179
|
+
all_sessions = [s for s in all_sessions if _matches_project(s.cwd, args.project)]
|
|
180
|
+
|
|
181
|
+
all_sessions.sort(key=lambda m: m.updated_at, reverse=True)
|
|
182
|
+
all_sessions = all_sessions[: args.limit]
|
|
183
|
+
|
|
184
|
+
if not all_sessions:
|
|
185
|
+
print("No sessions found.")
|
|
186
|
+
return 0
|
|
187
|
+
|
|
188
|
+
print(f"Found {len(all_sessions)} sessions:\n")
|
|
189
|
+
if args.project:
|
|
190
|
+
print(f" (filtered by project: '{args.project}')\n")
|
|
191
|
+
print(f"{'TOOL':<14} {'ID':<42} {'UPDATED':<17} {'MSG':>4} {'PROJECT':<30} TITLE")
|
|
192
|
+
print("-" * 140)
|
|
193
|
+
for s in all_sessions:
|
|
194
|
+
title = s.title[:40] + "..." if len(s.title) > 40 else s.title
|
|
195
|
+
sid = s.session_id
|
|
196
|
+
proj = _project_name(s.cwd)[:28] if s.cwd else "-"
|
|
197
|
+
print(f"{s.tool:<14} {sid:<42} {_fmt_time(s.updated_at):<17} {s.message_count:>4} {proj:<30} {title}")
|
|
198
|
+
|
|
199
|
+
print(f"\nReference format: @session:<tool>:<session_id>")
|
|
200
|
+
return 0
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def cmd_search(args):
|
|
204
|
+
adapters = get_adapters(args.tool)
|
|
205
|
+
all_results: list[tuple] = []
|
|
206
|
+
|
|
207
|
+
# See cmd_list: --project filters after the fact, so fetch a larger pool.
|
|
208
|
+
fetch_limit = 100000 if args.project else args.limit
|
|
209
|
+
|
|
210
|
+
for name, adapter in adapters:
|
|
211
|
+
results, _ = _with_timeout(
|
|
212
|
+
adapter.search_sessions, name, args.query, limit=fetch_limit
|
|
213
|
+
)
|
|
214
|
+
all_results.extend(results)
|
|
215
|
+
|
|
216
|
+
# Filter by project
|
|
217
|
+
if args.project:
|
|
218
|
+
all_results = [
|
|
219
|
+
(meta, snippet) for meta, snippet in all_results
|
|
220
|
+
if _matches_project(meta.cwd, args.project)
|
|
221
|
+
]
|
|
222
|
+
|
|
223
|
+
all_results.sort(key=lambda r: r[0].updated_at, reverse=True)
|
|
224
|
+
all_results = all_results[: args.limit]
|
|
225
|
+
|
|
226
|
+
if not all_results:
|
|
227
|
+
print(f'No sessions found for "{args.query}".')
|
|
228
|
+
return 0
|
|
229
|
+
|
|
230
|
+
print(f'Found {len(all_results)} sessions matching "{args.query}":\n')
|
|
231
|
+
if args.project:
|
|
232
|
+
print(f" (filtered by project: '{args.project}')\n")
|
|
233
|
+
for i, (meta, snippet) in enumerate(all_results, 1):
|
|
234
|
+
snippet = snippet.replace("\n", " ").strip()
|
|
235
|
+
if len(snippet) > 120:
|
|
236
|
+
snippet = snippet[:120] + "..."
|
|
237
|
+
proj = _project_name(meta.cwd)[:40] if meta.cwd else ""
|
|
238
|
+
print(f" {i}. [{meta.tool}] {meta.title[:60]}")
|
|
239
|
+
print(f" ref: {meta.ref}")
|
|
240
|
+
if proj:
|
|
241
|
+
print(f" project: {proj}")
|
|
242
|
+
print(f" {snippet}")
|
|
243
|
+
print()
|
|
244
|
+
return 0
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def cmd_show(args):
|
|
248
|
+
"""Display messages from a session in the terminal."""
|
|
249
|
+
parts = args.ref.split(":", 1)
|
|
250
|
+
if len(parts) != 2:
|
|
251
|
+
print(f"Error: invalid reference '{args.ref}'. Use TOOL:SESSION_ID", file=sys.stderr)
|
|
252
|
+
return 1
|
|
253
|
+
tool_name, session_id = parts
|
|
254
|
+
|
|
255
|
+
cls = ADAPTERS.get(tool_name)
|
|
256
|
+
if not cls:
|
|
257
|
+
print(f"Error: unknown tool '{tool_name}'", file=sys.stderr)
|
|
258
|
+
return 1
|
|
259
|
+
|
|
260
|
+
adapter = cls()
|
|
261
|
+
if not adapter.is_available():
|
|
262
|
+
print(f"Error: '{tool_name}' data not found", file=sys.stderr)
|
|
263
|
+
return 1
|
|
264
|
+
|
|
265
|
+
meta = adapter.get_session(session_id)
|
|
266
|
+
if not meta:
|
|
267
|
+
print(f"Session not found: {args.ref}", file=sys.stderr)
|
|
268
|
+
return 1
|
|
269
|
+
|
|
270
|
+
print(f"Session: {meta.ref}")
|
|
271
|
+
print(f"Title: {meta.title}")
|
|
272
|
+
print(f"Tool: {meta.tool}")
|
|
273
|
+
print(f"CWD: {meta.cwd or '?'}")
|
|
274
|
+
print(f"Created: {_fmt_time(meta.created_at)}")
|
|
275
|
+
print(f"Updated: {_fmt_time(meta.updated_at)}")
|
|
276
|
+
print(f"Model: {meta.model or '?'}")
|
|
277
|
+
print(f"Messages: {meta.message_count}")
|
|
278
|
+
print()
|
|
279
|
+
|
|
280
|
+
messages = adapter.read_messages(session_id, window=args.window)
|
|
281
|
+
if not messages:
|
|
282
|
+
print("(No messages found or message reading not supported for this tool)")
|
|
283
|
+
return 0
|
|
284
|
+
|
|
285
|
+
for msg in messages:
|
|
286
|
+
role_label = {"user": "👤 USER", "assistant": "🤖 ASSISTANT", "system": "⚙️ SYSTEM"}
|
|
287
|
+
label = role_label.get(msg.role, f"📋 {msg.role.upper()}")
|
|
288
|
+
ts = _fmt_time(msg.timestamp) if msg.timestamp else ""
|
|
289
|
+
print(f"{'─' * 80}")
|
|
290
|
+
print(f"{label} {ts} {msg.msg_id[:20] if msg.msg_id else ''}")
|
|
291
|
+
print(f"{'─' * 80}")
|
|
292
|
+
content = msg.content
|
|
293
|
+
if args.max_chars and len(content) > args.max_chars:
|
|
294
|
+
content = content[: args.max_chars] + f"\n... ({len(msg.content)} chars total)"
|
|
295
|
+
print(content)
|
|
296
|
+
if msg.tool_name:
|
|
297
|
+
print(f"\n[tool: {msg.tool_name}]")
|
|
298
|
+
print()
|
|
299
|
+
return 0
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def cmd_export(args):
|
|
303
|
+
"""Export a session to a Markdown file."""
|
|
304
|
+
parts = args.ref.split(":", 1)
|
|
305
|
+
if len(parts) != 2:
|
|
306
|
+
print(f"Error: invalid reference '{args.ref}'. Use TOOL:SESSION_ID", file=sys.stderr)
|
|
307
|
+
return 1
|
|
308
|
+
tool_name, session_id = parts
|
|
309
|
+
|
|
310
|
+
cls = ADAPTERS.get(tool_name)
|
|
311
|
+
if not cls:
|
|
312
|
+
print(f"Error: unknown tool '{tool_name}'", file=sys.stderr)
|
|
313
|
+
return 1
|
|
314
|
+
|
|
315
|
+
adapter = cls()
|
|
316
|
+
if not adapter.is_available():
|
|
317
|
+
print(f"Error: '{tool_name}' data not found", file=sys.stderr)
|
|
318
|
+
return 1
|
|
319
|
+
|
|
320
|
+
meta = adapter.get_session(session_id)
|
|
321
|
+
if not meta:
|
|
322
|
+
print(f"Session not found: {args.ref}", file=sys.stderr)
|
|
323
|
+
return 1
|
|
324
|
+
|
|
325
|
+
messages = adapter.read_messages(session_id)
|
|
326
|
+
if not messages:
|
|
327
|
+
print("No messages found to export.", file=sys.stderr)
|
|
328
|
+
return 1
|
|
329
|
+
|
|
330
|
+
# Build Markdown content
|
|
331
|
+
lines: list[str] = []
|
|
332
|
+
# Front matter
|
|
333
|
+
lines.append(f"# {meta.title or meta.ref}")
|
|
334
|
+
lines.append("")
|
|
335
|
+
lines.append("| Field | Value |")
|
|
336
|
+
lines.append("|-------|-------|")
|
|
337
|
+
lines.append(f"| **Tool** | {meta.tool} |")
|
|
338
|
+
lines.append(f"| **Session ID** | `{meta.session_id}` |")
|
|
339
|
+
lines.append(f"| **Project** | `{meta.cwd or '?'}` |")
|
|
340
|
+
lines.append(f"| **Created** | {_fmt_time(meta.created_at)} |")
|
|
341
|
+
lines.append(f"| **Updated** | {_fmt_time(meta.updated_at)} |")
|
|
342
|
+
if meta.model:
|
|
343
|
+
lines.append(f"| **Model** | {meta.model} |")
|
|
344
|
+
lines.append(f"| **Messages** | {len(messages)} |")
|
|
345
|
+
lines.append("")
|
|
346
|
+
lines.append("---")
|
|
347
|
+
lines.append("")
|
|
348
|
+
|
|
349
|
+
for msg in messages:
|
|
350
|
+
role_label = {
|
|
351
|
+
"user": "User",
|
|
352
|
+
"assistant": "Assistant",
|
|
353
|
+
"system": "System",
|
|
354
|
+
"tool": "Tool",
|
|
355
|
+
}.get(msg.role, msg.role.capitalize())
|
|
356
|
+
|
|
357
|
+
ts = _fmt_time(msg.timestamp) if msg.timestamp else ""
|
|
358
|
+
# Message header as H3
|
|
359
|
+
header_parts = [f"### {role_label}"]
|
|
360
|
+
if ts:
|
|
361
|
+
header_parts.append(ts)
|
|
362
|
+
if msg.model:
|
|
363
|
+
header_parts.append(f"_{msg.model}_")
|
|
364
|
+
lines.append(" ".join(header_parts))
|
|
365
|
+
lines.append("")
|
|
366
|
+
|
|
367
|
+
# Content
|
|
368
|
+
content = msg.content.strip()
|
|
369
|
+
if not content and msg.tool_name:
|
|
370
|
+
content = f"_(tool call: {msg.tool_name})_"
|
|
371
|
+
if content:
|
|
372
|
+
lines.append(content)
|
|
373
|
+
lines.append("")
|
|
374
|
+
|
|
375
|
+
# Tool call annotation
|
|
376
|
+
if msg.tool_name:
|
|
377
|
+
lines.append(f"> 🔧 **Tool**: `{msg.tool_name}`")
|
|
378
|
+
lines.append("")
|
|
379
|
+
|
|
380
|
+
if msg.reasoning:
|
|
381
|
+
lines.append("<details><summary>💭 Reasoning</summary>")
|
|
382
|
+
lines.append("")
|
|
383
|
+
lines.append(msg.reasoning.strip())
|
|
384
|
+
lines.append("")
|
|
385
|
+
lines.append("</details>")
|
|
386
|
+
lines.append("")
|
|
387
|
+
|
|
388
|
+
md_content = "\n".join(lines)
|
|
389
|
+
|
|
390
|
+
# Determine output path
|
|
391
|
+
if args.output:
|
|
392
|
+
out_path = Path(args.output)
|
|
393
|
+
else:
|
|
394
|
+
# Default: ./<tool>-<session_id>.md
|
|
395
|
+
safe_id = meta.session_id.replace("/", "_")[:40]
|
|
396
|
+
out_path = Path(f"{meta.tool}-{safe_id}.md")
|
|
397
|
+
|
|
398
|
+
out_path.write_text(md_content, encoding="utf-8")
|
|
399
|
+
print(f"Exported {len(messages)} messages to {out_path}")
|
|
400
|
+
return 0
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def cmd_tools(args):
|
|
404
|
+
print("Available adapters:\n")
|
|
405
|
+
print(f"{'TOOL':<16} {'STATUS':<10} {'LOCATION'}")
|
|
406
|
+
print("-" * 90)
|
|
407
|
+
for name, cls in ADAPTERS.items():
|
|
408
|
+
instance = cls()
|
|
409
|
+
status = "✅ active" if instance.is_available() else "❌ not found"
|
|
410
|
+
# Show data location
|
|
411
|
+
for attr in ("base_dir", "db_path"):
|
|
412
|
+
if hasattr(instance, attr):
|
|
413
|
+
loc = str(getattr(instance, attr))
|
|
414
|
+
break
|
|
415
|
+
else:
|
|
416
|
+
loc = "?"
|
|
417
|
+
print(f"{name:<16} {status:<10} {loc}")
|
|
418
|
+
return 0
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def main():
|
|
422
|
+
parser = argparse.ArgumentParser(
|
|
423
|
+
prog="converseek",
|
|
424
|
+
description="Cross-tool conversation search, browse, and export",
|
|
425
|
+
)
|
|
426
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
427
|
+
|
|
428
|
+
# list
|
|
429
|
+
p_list = sub.add_parser("list", help="List sessions")
|
|
430
|
+
p_list.add_argument("--tool", "-t", help="Comma-separated tool names (default: all)")
|
|
431
|
+
p_list.add_argument("--limit", "-n", type=int, default=20)
|
|
432
|
+
p_list.add_argument("--since", help="Only sessions since date (YYYY-MM-DD)")
|
|
433
|
+
p_list.add_argument("--cwd", help="Filter by working directory prefix (exact match)")
|
|
434
|
+
p_list.add_argument("--project", "-p", help="Filter by project name/path (fuzzy match)")
|
|
435
|
+
p_list.set_defaults(func=cmd_list)
|
|
436
|
+
|
|
437
|
+
# search
|
|
438
|
+
p_search = sub.add_parser("search", help="Search sessions by keyword")
|
|
439
|
+
p_search.add_argument("query", help="Search query")
|
|
440
|
+
p_search.add_argument("--tool", "-t", help="Comma-separated tool names (default: all)")
|
|
441
|
+
p_search.add_argument("--limit", "-n", type=int, default=20)
|
|
442
|
+
p_search.add_argument("--project", "-p", help="Filter by project name/path (fuzzy match)")
|
|
443
|
+
p_search.set_defaults(func=cmd_search)
|
|
444
|
+
|
|
445
|
+
# show
|
|
446
|
+
p_show = sub.add_parser("show", help="Show a session's messages")
|
|
447
|
+
p_show.add_argument("ref", help="Session reference: TOOL:SESSION_ID")
|
|
448
|
+
p_show.add_argument("--window", "-w", type=int, help="Only show last N messages")
|
|
449
|
+
p_show.add_argument("--max-chars", type=int, default=2000, help="Max chars per message")
|
|
450
|
+
p_show.set_defaults(func=cmd_show)
|
|
451
|
+
|
|
452
|
+
# export
|
|
453
|
+
p_export = sub.add_parser("export", help="Export a session to a Markdown file")
|
|
454
|
+
p_export.add_argument("ref", help="Session reference: TOOL:SESSION_ID")
|
|
455
|
+
p_export.add_argument("-o", "--output", help="Output file path (default: ./<tool>-<session_id>.md)")
|
|
456
|
+
p_export.set_defaults(func=cmd_export)
|
|
457
|
+
|
|
458
|
+
# projects
|
|
459
|
+
p_projects = sub.add_parser("projects", help="List all projects with session counts")
|
|
460
|
+
p_projects.add_argument("--tool", "-t", help="Comma-separated tool names (default: all)")
|
|
461
|
+
p_projects.add_argument("--limit", "-n", type=int, default=50, help="Max projects to show")
|
|
462
|
+
p_projects.set_defaults(func=cmd_projects)
|
|
463
|
+
|
|
464
|
+
# tools
|
|
465
|
+
p_tools = sub.add_parser("tools", help="List available adapters")
|
|
466
|
+
p_tools.set_defaults(func=cmd_tools)
|
|
467
|
+
|
|
468
|
+
args = parser.parse_args()
|
|
469
|
+
return args.func(args)
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
if __name__ == "__main__":
|
|
473
|
+
sys.exit(main())
|
|
File without changes
|