reprompt-cli 0.1.1__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.
- reprompt/__init__.py +5 -0
- reprompt/adapters/__init__.py +3 -0
- reprompt/adapters/base.py +25 -0
- reprompt/adapters/claude_code.py +140 -0
- reprompt/adapters/openclaw.py +79 -0
- reprompt/cli.py +177 -0
- reprompt/config.py +44 -0
- reprompt/core/__init__.py +1 -0
- reprompt/core/analyzer.py +68 -0
- reprompt/core/dedup.py +89 -0
- reprompt/core/library.py +91 -0
- reprompt/core/models.py +24 -0
- reprompt/core/pipeline.py +172 -0
- reprompt/embeddings/__init__.py +3 -0
- reprompt/embeddings/base.py +21 -0
- reprompt/embeddings/ollama.py +54 -0
- reprompt/embeddings/tfidf.py +22 -0
- reprompt/output/__init__.py +1 -0
- reprompt/output/json_out.py +11 -0
- reprompt/output/markdown.py +46 -0
- reprompt/output/terminal.py +65 -0
- reprompt/py.typed +0 -0
- reprompt/storage/__init__.py +1 -0
- reprompt/storage/db.py +314 -0
- reprompt_cli-0.1.1.dist-info/METADATA +198 -0
- reprompt_cli-0.1.1.dist-info/RECORD +29 -0
- reprompt_cli-0.1.1.dist-info/WHEEL +4 -0
- reprompt_cli-0.1.1.dist-info/entry_points.txt +2 -0
- reprompt_cli-0.1.1.dist-info/licenses/LICENSE +21 -0
reprompt/storage/db.py
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
"""SQLite storage for prompts, patterns, and term statistics."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import sqlite3
|
|
8
|
+
from datetime import datetime, timedelta, timezone
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PromptDB:
|
|
14
|
+
"""SQLite-backed storage for prompt data."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, path: Path) -> None:
|
|
17
|
+
self.path = path
|
|
18
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
19
|
+
self._init_schema()
|
|
20
|
+
|
|
21
|
+
def _conn(self) -> sqlite3.Connection:
|
|
22
|
+
"""Get a connection with row_factory set."""
|
|
23
|
+
conn = sqlite3.connect(str(self.path))
|
|
24
|
+
conn.row_factory = sqlite3.Row
|
|
25
|
+
return conn
|
|
26
|
+
|
|
27
|
+
def _init_schema(self) -> None:
|
|
28
|
+
"""Create tables if they don't exist."""
|
|
29
|
+
conn = self._conn()
|
|
30
|
+
try:
|
|
31
|
+
conn.executescript("""
|
|
32
|
+
CREATE TABLE IF NOT EXISTS prompts (
|
|
33
|
+
id INTEGER PRIMARY KEY,
|
|
34
|
+
hash TEXT UNIQUE,
|
|
35
|
+
text TEXT NOT NULL,
|
|
36
|
+
source TEXT NOT NULL,
|
|
37
|
+
project TEXT,
|
|
38
|
+
session_id TEXT,
|
|
39
|
+
timestamp TEXT,
|
|
40
|
+
char_count INTEGER,
|
|
41
|
+
embedding BLOB,
|
|
42
|
+
cluster_id INTEGER,
|
|
43
|
+
duplicate_of INTEGER REFERENCES prompts(id)
|
|
44
|
+
);
|
|
45
|
+
CREATE TABLE IF NOT EXISTS processed_sessions (
|
|
46
|
+
file_path TEXT PRIMARY KEY,
|
|
47
|
+
processed_at TEXT,
|
|
48
|
+
source TEXT
|
|
49
|
+
);
|
|
50
|
+
CREATE TABLE IF NOT EXISTS prompt_patterns (
|
|
51
|
+
id INTEGER PRIMARY KEY,
|
|
52
|
+
pattern_text TEXT,
|
|
53
|
+
frequency INTEGER,
|
|
54
|
+
avg_length REAL,
|
|
55
|
+
projects TEXT,
|
|
56
|
+
category TEXT,
|
|
57
|
+
first_seen TEXT,
|
|
58
|
+
last_seen TEXT,
|
|
59
|
+
examples TEXT
|
|
60
|
+
);
|
|
61
|
+
CREATE TABLE IF NOT EXISTS term_stats (
|
|
62
|
+
term TEXT PRIMARY KEY,
|
|
63
|
+
count INTEGER,
|
|
64
|
+
df INTEGER,
|
|
65
|
+
tfidf_avg REAL
|
|
66
|
+
);
|
|
67
|
+
""")
|
|
68
|
+
conn.commit()
|
|
69
|
+
finally:
|
|
70
|
+
conn.close()
|
|
71
|
+
|
|
72
|
+
@staticmethod
|
|
73
|
+
def _hash(text: str) -> str:
|
|
74
|
+
"""SHA-256 hash of stripped text."""
|
|
75
|
+
return hashlib.sha256(text.strip().encode()).hexdigest()
|
|
76
|
+
|
|
77
|
+
def insert_prompt(
|
|
78
|
+
self,
|
|
79
|
+
text: str,
|
|
80
|
+
*,
|
|
81
|
+
source: str,
|
|
82
|
+
project: str | None = None,
|
|
83
|
+
session_id: str = "",
|
|
84
|
+
timestamp: str = "",
|
|
85
|
+
) -> bool:
|
|
86
|
+
"""Insert a prompt. Returns True if new, False if duplicate by hash."""
|
|
87
|
+
stripped = text.strip()
|
|
88
|
+
h = self._hash(stripped)
|
|
89
|
+
conn = self._conn()
|
|
90
|
+
try:
|
|
91
|
+
conn.execute(
|
|
92
|
+
"""INSERT INTO prompts (hash, text, source, project, session_id,
|
|
93
|
+
timestamp, char_count) VALUES (?, ?, ?, ?, ?, ?, ?)""",
|
|
94
|
+
(h, stripped, source, project, session_id, timestamp, len(stripped)),
|
|
95
|
+
)
|
|
96
|
+
conn.commit()
|
|
97
|
+
return True
|
|
98
|
+
except sqlite3.IntegrityError:
|
|
99
|
+
return False
|
|
100
|
+
finally:
|
|
101
|
+
conn.close()
|
|
102
|
+
|
|
103
|
+
def get_all_prompts(self) -> list[dict[str, Any]]:
|
|
104
|
+
"""Return all prompts as dicts."""
|
|
105
|
+
conn = self._conn()
|
|
106
|
+
try:
|
|
107
|
+
rows = conn.execute("SELECT * FROM prompts ORDER BY id").fetchall()
|
|
108
|
+
return [dict(r) for r in rows]
|
|
109
|
+
finally:
|
|
110
|
+
conn.close()
|
|
111
|
+
|
|
112
|
+
def get_prompts_without_embedding(self) -> list[dict[str, Any]]:
|
|
113
|
+
"""Return prompts that have no embedding yet."""
|
|
114
|
+
conn = self._conn()
|
|
115
|
+
try:
|
|
116
|
+
rows = conn.execute(
|
|
117
|
+
"SELECT * FROM prompts WHERE embedding IS NULL AND duplicate_of IS NULL ORDER BY id"
|
|
118
|
+
).fetchall()
|
|
119
|
+
return [dict(r) for r in rows]
|
|
120
|
+
finally:
|
|
121
|
+
conn.close()
|
|
122
|
+
|
|
123
|
+
def update_embedding(self, prompt_id: int, embedding: bytes) -> None:
|
|
124
|
+
"""Store an embedding blob for a prompt."""
|
|
125
|
+
conn = self._conn()
|
|
126
|
+
try:
|
|
127
|
+
conn.execute("UPDATE prompts SET embedding = ? WHERE id = ?", (embedding, prompt_id))
|
|
128
|
+
conn.commit()
|
|
129
|
+
finally:
|
|
130
|
+
conn.close()
|
|
131
|
+
|
|
132
|
+
def mark_duplicate(self, prompt_id: int, duplicate_of: int) -> None:
|
|
133
|
+
"""Mark a prompt as a duplicate of another."""
|
|
134
|
+
conn = self._conn()
|
|
135
|
+
try:
|
|
136
|
+
conn.execute(
|
|
137
|
+
"UPDATE prompts SET duplicate_of = ? WHERE id = ?", (duplicate_of, prompt_id)
|
|
138
|
+
)
|
|
139
|
+
conn.commit()
|
|
140
|
+
finally:
|
|
141
|
+
conn.close()
|
|
142
|
+
|
|
143
|
+
def mark_session_processed(self, file_path: str, source: str = "") -> None:
|
|
144
|
+
"""Record that a session file has been processed."""
|
|
145
|
+
conn = self._conn()
|
|
146
|
+
try:
|
|
147
|
+
conn.execute(
|
|
148
|
+
"INSERT OR REPLACE INTO processed_sessions (file_path, processed_at, source) "
|
|
149
|
+
"VALUES (?, ?, ?)",
|
|
150
|
+
(file_path, datetime.now(timezone.utc).isoformat(), source),
|
|
151
|
+
)
|
|
152
|
+
conn.commit()
|
|
153
|
+
finally:
|
|
154
|
+
conn.close()
|
|
155
|
+
|
|
156
|
+
def is_session_processed(self, file_path: str) -> bool:
|
|
157
|
+
"""Check if a session file has already been processed."""
|
|
158
|
+
conn = self._conn()
|
|
159
|
+
try:
|
|
160
|
+
row = conn.execute(
|
|
161
|
+
"SELECT 1 FROM processed_sessions WHERE file_path = ?", (file_path,)
|
|
162
|
+
).fetchone()
|
|
163
|
+
return row is not None
|
|
164
|
+
finally:
|
|
165
|
+
conn.close()
|
|
166
|
+
|
|
167
|
+
def insert_pattern(
|
|
168
|
+
self,
|
|
169
|
+
pattern_text: str,
|
|
170
|
+
frequency: int,
|
|
171
|
+
avg_length: float,
|
|
172
|
+
projects: list[str],
|
|
173
|
+
category: str,
|
|
174
|
+
first_seen: str,
|
|
175
|
+
last_seen: str,
|
|
176
|
+
examples: list[str],
|
|
177
|
+
) -> int:
|
|
178
|
+
"""Insert a prompt pattern. Returns the new pattern ID."""
|
|
179
|
+
conn = self._conn()
|
|
180
|
+
try:
|
|
181
|
+
cursor = conn.execute(
|
|
182
|
+
"""INSERT INTO prompt_patterns
|
|
183
|
+
(pattern_text, frequency, avg_length, projects, category,
|
|
184
|
+
first_seen, last_seen, examples)
|
|
185
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
186
|
+
(
|
|
187
|
+
pattern_text,
|
|
188
|
+
frequency,
|
|
189
|
+
avg_length,
|
|
190
|
+
json.dumps(projects),
|
|
191
|
+
category,
|
|
192
|
+
first_seen,
|
|
193
|
+
last_seen,
|
|
194
|
+
json.dumps(examples),
|
|
195
|
+
),
|
|
196
|
+
)
|
|
197
|
+
conn.commit()
|
|
198
|
+
pattern_id = cursor.lastrowid
|
|
199
|
+
assert pattern_id is not None
|
|
200
|
+
return pattern_id
|
|
201
|
+
finally:
|
|
202
|
+
conn.close()
|
|
203
|
+
|
|
204
|
+
def get_patterns(self, category: str | None = None) -> list[dict[str, Any]]:
|
|
205
|
+
"""Return all patterns, optionally filtered by category."""
|
|
206
|
+
conn = self._conn()
|
|
207
|
+
try:
|
|
208
|
+
if category:
|
|
209
|
+
rows = conn.execute(
|
|
210
|
+
"SELECT * FROM prompt_patterns WHERE category = ? ORDER BY frequency DESC",
|
|
211
|
+
(category,),
|
|
212
|
+
).fetchall()
|
|
213
|
+
else:
|
|
214
|
+
rows = conn.execute(
|
|
215
|
+
"SELECT * FROM prompt_patterns ORDER BY frequency DESC"
|
|
216
|
+
).fetchall()
|
|
217
|
+
result = []
|
|
218
|
+
for r in rows:
|
|
219
|
+
d = dict(r)
|
|
220
|
+
d["projects"] = json.loads(d["projects"]) if d["projects"] else []
|
|
221
|
+
d["examples"] = json.loads(d["examples"]) if d["examples"] else []
|
|
222
|
+
result.append(d)
|
|
223
|
+
return result
|
|
224
|
+
finally:
|
|
225
|
+
conn.close()
|
|
226
|
+
|
|
227
|
+
def clear_patterns(self) -> None:
|
|
228
|
+
"""Delete all stored patterns (called before re-computing)."""
|
|
229
|
+
conn = self._conn()
|
|
230
|
+
try:
|
|
231
|
+
conn.execute("DELETE FROM prompt_patterns")
|
|
232
|
+
conn.commit()
|
|
233
|
+
finally:
|
|
234
|
+
conn.close()
|
|
235
|
+
|
|
236
|
+
def upsert_term_stats(self, term: str, count: int, df: int, tfidf_avg: float) -> None:
|
|
237
|
+
"""Insert or update term statistics."""
|
|
238
|
+
conn = self._conn()
|
|
239
|
+
try:
|
|
240
|
+
conn.execute(
|
|
241
|
+
"""INSERT INTO term_stats (term, count, df, tfidf_avg)
|
|
242
|
+
VALUES (?, ?, ?, ?)
|
|
243
|
+
ON CONFLICT(term) DO UPDATE SET
|
|
244
|
+
count = excluded.count,
|
|
245
|
+
df = excluded.df,
|
|
246
|
+
tfidf_avg = excluded.tfidf_avg""",
|
|
247
|
+
(term, count, df, tfidf_avg),
|
|
248
|
+
)
|
|
249
|
+
conn.commit()
|
|
250
|
+
finally:
|
|
251
|
+
conn.close()
|
|
252
|
+
|
|
253
|
+
def get_term_stats(self, limit: int = 50) -> list[dict[str, Any]]:
|
|
254
|
+
"""Return top terms by count."""
|
|
255
|
+
conn = self._conn()
|
|
256
|
+
try:
|
|
257
|
+
rows = conn.execute(
|
|
258
|
+
"SELECT * FROM term_stats ORDER BY count DESC LIMIT ?", (limit,)
|
|
259
|
+
).fetchall()
|
|
260
|
+
return [dict(r) for r in rows]
|
|
261
|
+
finally:
|
|
262
|
+
conn.close()
|
|
263
|
+
|
|
264
|
+
def purge_old_prompts(self, retention_days: int = 90) -> int:
|
|
265
|
+
"""Delete prompts older than retention_days. Returns count deleted.
|
|
266
|
+
|
|
267
|
+
Two-pass: first remove duplicates pointing to old prompts,
|
|
268
|
+
then remove the old prompts themselves.
|
|
269
|
+
"""
|
|
270
|
+
cutoff = (datetime.now(timezone.utc) - timedelta(days=retention_days)).isoformat()
|
|
271
|
+
conn = self._conn()
|
|
272
|
+
try:
|
|
273
|
+
# Pass 1: clear duplicate_of references to soon-deleted prompts
|
|
274
|
+
conn.execute(
|
|
275
|
+
"""UPDATE prompts SET duplicate_of = NULL
|
|
276
|
+
WHERE duplicate_of IN (
|
|
277
|
+
SELECT id FROM prompts WHERE timestamp < ? AND timestamp != ''
|
|
278
|
+
)""",
|
|
279
|
+
(cutoff,),
|
|
280
|
+
)
|
|
281
|
+
# Pass 2: delete old prompts
|
|
282
|
+
cursor = conn.execute(
|
|
283
|
+
"DELETE FROM prompts WHERE timestamp < ? AND timestamp != ''",
|
|
284
|
+
(cutoff,),
|
|
285
|
+
)
|
|
286
|
+
deleted = cursor.rowcount
|
|
287
|
+
conn.commit()
|
|
288
|
+
return deleted
|
|
289
|
+
finally:
|
|
290
|
+
conn.close()
|
|
291
|
+
|
|
292
|
+
def get_stats(self) -> dict[str, Any]:
|
|
293
|
+
"""Return summary statistics."""
|
|
294
|
+
conn = self._conn()
|
|
295
|
+
try:
|
|
296
|
+
total = conn.execute("SELECT COUNT(*) FROM prompts").fetchone()[0]
|
|
297
|
+
unique = conn.execute(
|
|
298
|
+
"SELECT COUNT(*) FROM prompts WHERE duplicate_of IS NULL"
|
|
299
|
+
).fetchone()[0]
|
|
300
|
+
sessions = conn.execute("SELECT COUNT(*) FROM processed_sessions").fetchone()[0]
|
|
301
|
+
patterns = conn.execute("SELECT COUNT(*) FROM prompt_patterns").fetchone()[0]
|
|
302
|
+
date_range = conn.execute(
|
|
303
|
+
"SELECT MIN(timestamp), MAX(timestamp) FROM prompts WHERE timestamp != ''"
|
|
304
|
+
).fetchone()
|
|
305
|
+
return {
|
|
306
|
+
"total_prompts": total,
|
|
307
|
+
"unique_prompts": unique,
|
|
308
|
+
"sessions_processed": sessions,
|
|
309
|
+
"patterns": patterns,
|
|
310
|
+
"earliest": date_range[0] if date_range else None,
|
|
311
|
+
"latest": date_range[1] if date_range else None,
|
|
312
|
+
}
|
|
313
|
+
finally:
|
|
314
|
+
conn.close()
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: reprompt-cli
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Discover, analyze, and evolve your best prompts from AI coding sessions
|
|
5
|
+
Project-URL: Homepage, https://github.com/reprompt-dev/reprompt
|
|
6
|
+
Project-URL: Repository, https://github.com/reprompt-dev/reprompt
|
|
7
|
+
Project-URL: Issues, https://github.com/reprompt-dev/reprompt/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/reprompt-dev/reprompt/blob/main/CHANGELOG.md
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: ai,analytics,claude-code,cli,llm,prompt
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Requires-Dist: pydantic-settings>=2.0
|
|
25
|
+
Requires-Dist: rich>=13.0
|
|
26
|
+
Requires-Dist: scikit-learn>=1.4
|
|
27
|
+
Requires-Dist: typer>=0.9
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: mypy>=1.0; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
32
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
33
|
+
Provides-Extra: local
|
|
34
|
+
Requires-Dist: sentence-transformers>=2.0; extra == 'local'
|
|
35
|
+
Provides-Extra: ollama
|
|
36
|
+
Requires-Dist: requests>=2.31; extra == 'ollama'
|
|
37
|
+
Provides-Extra: openai
|
|
38
|
+
Requires-Dist: openai>=1.0; extra == 'openai'
|
|
39
|
+
Description-Content-Type: text/markdown
|
|
40
|
+
|
|
41
|
+
# reprompt
|
|
42
|
+
|
|
43
|
+
[](https://github.com/reprompt-dev/reprompt/actions/workflows/ci.yml)
|
|
44
|
+
[](https://pypi.org/project/reprompt-cli/)
|
|
45
|
+
[](https://pypi.org/project/reprompt-cli/)
|
|
46
|
+
[](https://opensource.org/licenses/MIT)
|
|
47
|
+
|
|
48
|
+
> Discover, analyze, and evolve your best prompts from AI coding sessions.
|
|
49
|
+
|
|
50
|
+
Every developer's AI session history contains reusable prompt patterns -- scattered across hundreds of session files. **reprompt** extracts them, deduplicates, analyzes frequency, and builds a personal prompt library that evolves over time.
|
|
51
|
+
|
|
52
|
+
## Quick Start
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pipx install reprompt-cli
|
|
56
|
+
reprompt scan
|
|
57
|
+
reprompt report
|
|
58
|
+
reprompt library
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Features
|
|
62
|
+
|
|
63
|
+
- **Auto-detection** -- finds Claude Code and OpenClaw sessions automatically
|
|
64
|
+
- **Two-layer dedup** -- SHA-256 exact + TF-IDF semantic similarity
|
|
65
|
+
- **Hot terms analysis** -- TF-IDF discovers your most-used technical terms
|
|
66
|
+
- **K-means clustering** -- groups similar prompts into themes
|
|
67
|
+
- **Prompt library** -- extracts high-frequency patterns, auto-categorizes (debug/implement/test/review/refactor/explain/config)
|
|
68
|
+
- **Rich reports** -- beautiful terminal output with tables and bar charts
|
|
69
|
+
- **Multiple formats** -- terminal, JSON (for pipelines), Markdown (for docs)
|
|
70
|
+
- **Pluggable adapters** -- add support for any AI coding tool
|
|
71
|
+
- **Zero config** -- works out of the box, customize via env vars or TOML
|
|
72
|
+
|
|
73
|
+
## Supported AI Tools
|
|
74
|
+
|
|
75
|
+
| Tool | Status | Session Path |
|
|
76
|
+
|------|--------|-------------|
|
|
77
|
+
| Claude Code | Supported | `~/.claude/projects/` |
|
|
78
|
+
| OpenClaw / OpenCode | Supported | `~/.opencode/sessions/` |
|
|
79
|
+
| Cursor | Planned | -- |
|
|
80
|
+
| Codex CLI | Planned | -- |
|
|
81
|
+
| Gemini CLI | Planned | -- |
|
|
82
|
+
|
|
83
|
+
## Usage
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
# Scan all detected AI tools
|
|
87
|
+
reprompt scan
|
|
88
|
+
|
|
89
|
+
# Scan specific source
|
|
90
|
+
reprompt scan --source claude-code
|
|
91
|
+
|
|
92
|
+
# Scan custom path
|
|
93
|
+
reprompt scan --path ~/custom/sessions
|
|
94
|
+
|
|
95
|
+
# Rich terminal report
|
|
96
|
+
reprompt report
|
|
97
|
+
|
|
98
|
+
# JSON output (for CI/pipelines)
|
|
99
|
+
reprompt report --format json
|
|
100
|
+
|
|
101
|
+
# View your prompt library
|
|
102
|
+
reprompt library
|
|
103
|
+
|
|
104
|
+
# Filter by category
|
|
105
|
+
reprompt library --category debug
|
|
106
|
+
|
|
107
|
+
# Export prompt library as Markdown
|
|
108
|
+
reprompt library prompts.md
|
|
109
|
+
|
|
110
|
+
# Database stats
|
|
111
|
+
reprompt status
|
|
112
|
+
|
|
113
|
+
# Auto-scan after sessions
|
|
114
|
+
reprompt install-hook
|
|
115
|
+
|
|
116
|
+
# Cleanup old data
|
|
117
|
+
reprompt purge --older-than 90d
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Terminal Report
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
reprompt -- AI Session Analytics
|
|
124
|
+
========================================
|
|
125
|
+
|
|
126
|
+
Overview
|
|
127
|
+
Total prompts: 1,247
|
|
128
|
+
Unique (deduped): 832
|
|
129
|
+
Sessions scanned: 156
|
|
130
|
+
Sources: claude-code, openclaw
|
|
131
|
+
|
|
132
|
+
Top Prompt Patterns
|
|
133
|
+
# | Pattern | Count | Category
|
|
134
|
+
1 | fix the failing test... | 42 | debug
|
|
135
|
+
2 | add unit tests for... | 38 | test
|
|
136
|
+
3 | refactor X to use... | 27 | refactor
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Configuration
|
|
140
|
+
|
|
141
|
+
Zero config by default. Customize with environment variables or TOML:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
# Environment variables (prefix: REPROMPT_)
|
|
145
|
+
REPROMPT_EMBEDDING_BACKEND=ollama reprompt scan
|
|
146
|
+
REPROMPT_DB_PATH=~/custom/reprompt.db reprompt status
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
```toml
|
|
150
|
+
# ~/.config/reprompt/config.toml
|
|
151
|
+
[embedding]
|
|
152
|
+
backend = "tfidf" # tfidf | ollama | local | openai
|
|
153
|
+
|
|
154
|
+
[storage]
|
|
155
|
+
db_path = "~/.local/share/reprompt/reprompt.db"
|
|
156
|
+
|
|
157
|
+
[dedup]
|
|
158
|
+
semantic_threshold = 0.85
|
|
159
|
+
|
|
160
|
+
[library]
|
|
161
|
+
min_frequency = 3
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Optional Backends
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
pip install reprompt-cli[ollama] # Ollama API embeddings
|
|
168
|
+
pip install reprompt-cli[local] # sentence-transformers (CPU)
|
|
169
|
+
pip install reprompt-cli[openai] # OpenAI API embeddings
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Adding an Adapter
|
|
173
|
+
|
|
174
|
+
Create a new adapter by subclassing `BaseAdapter`:
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
from reprompt.adapters.base import BaseAdapter
|
|
178
|
+
from reprompt.core.models import Prompt
|
|
179
|
+
|
|
180
|
+
class MyToolAdapter(BaseAdapter):
|
|
181
|
+
name = "my-tool"
|
|
182
|
+
default_session_path = "~/.my-tool/sessions"
|
|
183
|
+
|
|
184
|
+
def parse_session(self, path):
|
|
185
|
+
# Parse session file -> list[Prompt]
|
|
186
|
+
...
|
|
187
|
+
|
|
188
|
+
def detect_installed(self):
|
|
189
|
+
return Path(self.default_session_path).expanduser().exists()
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Contributing
|
|
193
|
+
|
|
194
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and guidelines.
|
|
195
|
+
|
|
196
|
+
## License
|
|
197
|
+
|
|
198
|
+
MIT
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
reprompt/__init__.py,sha256=ssgV9xNer2bW8O0achjE8LP17vwUmTOMVoX9_T4Ute4,149
|
|
2
|
+
reprompt/cli.py,sha256=GrI_21s6QOpdrcjnF2ERY2N_nJt6eMchPKbOH8QYbDU,5849
|
|
3
|
+
reprompt/config.py,sha256=Wt5MqVKC18jU07lAsa6wKqSktw7FB2k-Kb6Uw7sVLVg,1061
|
|
4
|
+
reprompt/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
reprompt/adapters/__init__.py,sha256=_LbAhUS_85bD9weFW9LnM-4x-EkIo-sTPZGP6z1fgVM,88
|
|
6
|
+
reprompt/adapters/base.py,sha256=rdG6yUDB7EWw7i066l2V6WmBE663LNAUdJ29sF3iveU,617
|
|
7
|
+
reprompt/adapters/claude_code.py,sha256=jLQ2zog2N0HBEaeq2xS9k4th5a58UajHmJ601wj8zSI,3825
|
|
8
|
+
reprompt/adapters/openclaw.py,sha256=_yVRT5eKYkE6LB9zGmm-obbqzYg4BRlf3fdO6o52jkM,2555
|
|
9
|
+
reprompt/core/__init__.py,sha256=GuzztUmW-OJWi7iC_4IRg3L_tGAe-dcdyP4URC_s7KE,36
|
|
10
|
+
reprompt/core/analyzer.py,sha256=E9qnq3eHTie5EPRXw0Y6qlVHWlXNnNubQM79dEQ5NM4,2050
|
|
11
|
+
reprompt/core/dedup.py,sha256=z_3_qLjxvqmRJhWr53KkdOCzdaCGs2X_D3vhHSjkS04,2855
|
|
12
|
+
reprompt/core/library.py,sha256=mPIOdz0unBEfuB_fNTzBwkVEv3SVplFg_pPvVavLsXk,3268
|
|
13
|
+
reprompt/core/models.py,sha256=eJsiBe_nN6BkjKCEVy_x12EayVEVa2AlvKVtDuzSKgI,568
|
|
14
|
+
reprompt/core/pipeline.py,sha256=j59JNJZDzzqii9wstpyiwnCHvVjcuK-qLdJhov2SdRw,5209
|
|
15
|
+
reprompt/embeddings/__init__.py,sha256=l1ssUD_h7Y0Ax1lAx3-KI9RUGM1oTFpibDWBSMP-DEM,86
|
|
16
|
+
reprompt/embeddings/base.py,sha256=XvftNdsWiSg67JE_W8zpmO0NL2KJhc0VhYnOH0167Ms,629
|
|
17
|
+
reprompt/embeddings/ollama.py,sha256=Rpi54N4q3t5pwrDzQIy-YaVFoMFwsFH2gHSKjYoovi4,1549
|
|
18
|
+
reprompt/embeddings/tfidf.py,sha256=gJd9Tvf35Nxwbm9Zxm9iSE4tUrAlBcBpRl_FVA-PgFo,702
|
|
19
|
+
reprompt/output/__init__.py,sha256=xwGXiWjcpVgKoLDYwAdN9LbpviSKNnO1qgUBexj5IhQ,46
|
|
20
|
+
reprompt/output/json_out.py,sha256=ip70Oj2EImHepevKCPtf_ztBYH98FEQwVJcDKW96kT0,249
|
|
21
|
+
reprompt/output/markdown.py,sha256=4nCXiBYKS0NQqD_h2p71sqDzWKPFkn1fj1XOfqEuXh0,1656
|
|
22
|
+
reprompt/output/terminal.py,sha256=UFFy715T6IeYZiAz24iIg3ql_Y8Pz6f-mHY5CkO4PMk,2383
|
|
23
|
+
reprompt/storage/__init__.py,sha256=etUD91sg2hoIGrPQUSZWI6jTbaOc_d-lWjlmrxvF9Mk,40
|
|
24
|
+
reprompt/storage/db.py,sha256=Yoh_9G_EC7nfVj1uoODltbVUQHg_UZNxV8BM6iqF1IM,11061
|
|
25
|
+
reprompt_cli-0.1.1.dist-info/METADATA,sha256=VhPTPVFIPe-F1bDXrl6Wmr5nv88Hc3A-BVArE7UuG_0,5848
|
|
26
|
+
reprompt_cli-0.1.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
27
|
+
reprompt_cli-0.1.1.dist-info/entry_points.txt,sha256=VQTi0S-vVU8YIfEZU_aOm6SHBpRPuN3Wh7MzLhemb_Y,46
|
|
28
|
+
reprompt_cli-0.1.1.dist-info/licenses/LICENSE,sha256=5xJBBHKoDOoUfG1iXzCmBScu7I-L4qIxWEm8jaYnzgo,1069
|
|
29
|
+
reprompt_cli-0.1.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 reprompt-dev
|
|
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.
|