ccq 0.1.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.
- ccq/__init__.py +3 -0
- ccq/cli.py +425 -0
- ccq/db.py +479 -0
- ccq/format.py +88 -0
- ccq/pricing.py +68 -0
- ccq/server.py +317 -0
- ccq-0.1.0.dist-info/METADATA +150 -0
- ccq-0.1.0.dist-info/RECORD +11 -0
- ccq-0.1.0.dist-info/WHEEL +4 -0
- ccq-0.1.0.dist-info/entry_points.txt +3 -0
- ccq-0.1.0.dist-info/licenses/LICENSE +21 -0
ccq/__init__.py
ADDED
ccq/cli.py
ADDED
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
"""ccq - query your own Claude Code agent history.
|
|
2
|
+
|
|
3
|
+
Read-only DuckDB over ~/.claude/projects/*.jsonl. Never writes the transcripts.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
import click
|
|
12
|
+
|
|
13
|
+
from ccq import db
|
|
14
|
+
from ccq.format import render
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from collections.abc import Sequence
|
|
18
|
+
|
|
19
|
+
import duckdb
|
|
20
|
+
|
|
21
|
+
FORMATS = ["table", "json", "csv"]
|
|
22
|
+
_fmt_option = click.option(
|
|
23
|
+
"-f",
|
|
24
|
+
"--format",
|
|
25
|
+
"fmt",
|
|
26
|
+
type=click.Choice(FORMATS),
|
|
27
|
+
default="table",
|
|
28
|
+
help="Output format.",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _emit(columns: list[str], rows: Sequence[tuple[object, ...]], fmt: str) -> None:
|
|
33
|
+
click.echo(render(columns, rows, fmt))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _con(ctx: click.Context) -> duckdb.DuckDBPyConnection:
|
|
37
|
+
"""Connect live, or to the materialized snapshot when --fast is set.
|
|
38
|
+
|
|
39
|
+
In fast mode the snapshot is built on first use if it does not exist yet.
|
|
40
|
+
"""
|
|
41
|
+
if ctx.obj.get("fast"):
|
|
42
|
+
projects_dir = ctx.obj["projects_dir"]
|
|
43
|
+
try:
|
|
44
|
+
con = db.connect_fast()
|
|
45
|
+
except FileNotFoundError:
|
|
46
|
+
click.echo("Building snapshot (first --fast run)...", err=True)
|
|
47
|
+
db.build_cache(projects_dir)
|
|
48
|
+
return db.connect_fast()
|
|
49
|
+
# Use the existing snapshot but flag staleness (rebuilding on every query
|
|
50
|
+
# would thrash during an active session as transcripts grow).
|
|
51
|
+
if db.is_cache_stale(projects_dir=projects_dir):
|
|
52
|
+
click.echo("Note: snapshot is stale; run `ccq cache build` to refresh.", err=True)
|
|
53
|
+
return con
|
|
54
|
+
return db.connect(ctx.obj["projects_dir"])
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@click.group(context_settings={"help_option_names": ["-h", "--help"]})
|
|
58
|
+
@click.option(
|
|
59
|
+
"--projects-dir",
|
|
60
|
+
default=str(db.DEFAULT_PROJECTS_DIR),
|
|
61
|
+
help="Directory of Claude Code project transcripts.",
|
|
62
|
+
show_default=True,
|
|
63
|
+
)
|
|
64
|
+
@click.option(
|
|
65
|
+
"--fast",
|
|
66
|
+
"-F",
|
|
67
|
+
is_flag=True,
|
|
68
|
+
help="Query the materialized snapshot (instant repeat queries; build with `ccq cache build`).",
|
|
69
|
+
)
|
|
70
|
+
@click.version_option(package_name="ccq")
|
|
71
|
+
@click.pass_context
|
|
72
|
+
def cli(ctx: click.Context, projects_dir: str, fast: bool) -> None:
|
|
73
|
+
"""Query your own Claude Code agent history."""
|
|
74
|
+
ctx.ensure_object(dict)
|
|
75
|
+
ctx.obj["projects_dir"] = projects_dir
|
|
76
|
+
ctx.obj["fast"] = fast
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# --------------------------------------------------------------------------- #
|
|
80
|
+
# sessions
|
|
81
|
+
# --------------------------------------------------------------------------- #
|
|
82
|
+
_SESSION_SORTS = {
|
|
83
|
+
"recent": "started_at",
|
|
84
|
+
"cost": "cost_usd",
|
|
85
|
+
"duration": "duration_min",
|
|
86
|
+
"messages": "messages",
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@cli.command()
|
|
91
|
+
@click.option("--project", "-p", help="Filter to projects matching this (case-insensitive).")
|
|
92
|
+
@click.option("--since", help="Only sessions started on/after this date (YYYY-MM-DD).")
|
|
93
|
+
@click.option("--sort", type=click.Choice(list(_SESSION_SORTS)), default="recent", help="Sort key.")
|
|
94
|
+
@click.option("--limit", "-n", default=25, help="Max rows.")
|
|
95
|
+
@_fmt_option
|
|
96
|
+
@click.pass_context
|
|
97
|
+
def sessions(
|
|
98
|
+
ctx: click.Context, project: str | None, since: str | None, sort: str, limit: int, fmt: str
|
|
99
|
+
) -> None:
|
|
100
|
+
"""List sessions with project, span, volume, tokens and estimated cost."""
|
|
101
|
+
order = _SESSION_SORTS[sort]
|
|
102
|
+
sql = f"""
|
|
103
|
+
SELECT left(session_id, 8) AS id, project, started_at, duration_min AS dur_min,
|
|
104
|
+
messages AS msgs, models, output_tokens AS out_tok,
|
|
105
|
+
round(cost_usd, 2) AS cost_usd
|
|
106
|
+
FROM sessions
|
|
107
|
+
WHERE (? IS NULL OR project ILIKE '%' || ? || '%')
|
|
108
|
+
AND (? IS NULL OR started_at >= TRY_CAST(? AS TIMESTAMP))
|
|
109
|
+
ORDER BY {order} DESC NULLS LAST
|
|
110
|
+
LIMIT ?
|
|
111
|
+
"""
|
|
112
|
+
cols, rows = db.query(_con(ctx), sql, [project, project, since, since, limit])
|
|
113
|
+
_emit(cols, rows, fmt)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# --------------------------------------------------------------------------- #
|
|
117
|
+
# cost
|
|
118
|
+
# --------------------------------------------------------------------------- #
|
|
119
|
+
@cli.command()
|
|
120
|
+
@click.option(
|
|
121
|
+
"--by",
|
|
122
|
+
type=click.Choice(["project", "model", "day", "session"]),
|
|
123
|
+
default="project",
|
|
124
|
+
help="Cost rollup dimension.",
|
|
125
|
+
)
|
|
126
|
+
@click.option("--since", help="Only count usage on/after this date (YYYY-MM-DD).")
|
|
127
|
+
@click.option("--limit", "-n", default=30, help="Max rows.")
|
|
128
|
+
@_fmt_option
|
|
129
|
+
@click.pass_context
|
|
130
|
+
def cost(ctx: click.Context, by: str, since: str | None, limit: int, fmt: str) -> None:
|
|
131
|
+
"""Estimated cost rollups. Main-loop only - subagent spend is unpriced (see `agents`)."""
|
|
132
|
+
since_pred = "(? IS NULL OR ts >= TRY_CAST(? AS TIMESTAMP))"
|
|
133
|
+
params: list[object]
|
|
134
|
+
if by == "model":
|
|
135
|
+
sql = f"""
|
|
136
|
+
SELECT model, count(*) AS turns,
|
|
137
|
+
sum(input_tokens + cache_creation_tokens + cache_read_tokens) AS in_tok,
|
|
138
|
+
sum(output_tokens) AS out_tok, round(sum(cost_usd), 2) AS cost_usd
|
|
139
|
+
FROM message_usage WHERE {since_pred}
|
|
140
|
+
GROUP BY 1 ORDER BY cost_usd DESC LIMIT ?
|
|
141
|
+
"""
|
|
142
|
+
params = [since, since, limit]
|
|
143
|
+
elif by == "day":
|
|
144
|
+
sql = f"""
|
|
145
|
+
SELECT CAST(ts AS DATE) AS day, count(*) AS turns,
|
|
146
|
+
sum(output_tokens) AS out_tok, round(sum(cost_usd), 2) AS cost_usd
|
|
147
|
+
FROM message_usage WHERE {since_pred} AND ts IS NOT NULL
|
|
148
|
+
GROUP BY 1 ORDER BY day DESC LIMIT ?
|
|
149
|
+
"""
|
|
150
|
+
params = [since, since, limit]
|
|
151
|
+
elif by == "session":
|
|
152
|
+
sql = """
|
|
153
|
+
SELECT left(session_id, 8) AS id, project, started_at,
|
|
154
|
+
output_tokens AS out_tok, round(cost_usd, 2) AS cost_usd
|
|
155
|
+
FROM sessions WHERE (? IS NULL OR started_at >= TRY_CAST(? AS TIMESTAMP))
|
|
156
|
+
ORDER BY cost_usd DESC LIMIT ?
|
|
157
|
+
"""
|
|
158
|
+
params = [since, since, limit]
|
|
159
|
+
else: # project
|
|
160
|
+
sql = f"""
|
|
161
|
+
SELECT project, count(DISTINCT session_id) AS sessions,
|
|
162
|
+
sum(output_tokens) AS out_tok, round(sum(cost_usd), 2) AS cost_usd
|
|
163
|
+
FROM message_usage WHERE {since_pred}
|
|
164
|
+
GROUP BY 1 ORDER BY cost_usd DESC LIMIT ?
|
|
165
|
+
"""
|
|
166
|
+
params = [since, since, limit]
|
|
167
|
+
_emit(*db.query(_con(ctx), sql, params), fmt=fmt)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# --------------------------------------------------------------------------- #
|
|
171
|
+
# tools
|
|
172
|
+
# --------------------------------------------------------------------------- #
|
|
173
|
+
@cli.command()
|
|
174
|
+
@click.option("--project", "-p", help="Filter to projects matching this.")
|
|
175
|
+
@click.option("--bash", is_flag=True, help="Break down Bash invocations by leading command.")
|
|
176
|
+
@click.option("--limit", "-n", default=30, help="Max rows.")
|
|
177
|
+
@_fmt_option
|
|
178
|
+
@click.pass_context
|
|
179
|
+
def tools(ctx: click.Context, project: str | None, bash: bool, limit: int, fmt: str) -> None:
|
|
180
|
+
"""Tool-use frequency across your history (optionally a Bash command breakdown)."""
|
|
181
|
+
proj_pred = "(? IS NULL OR project ILIKE '%' || ? || '%')"
|
|
182
|
+
if bash:
|
|
183
|
+
sql = f"""
|
|
184
|
+
SELECT lower(split_part(trim(tool_input->>'$.command'), ' ', 1)) AS command,
|
|
185
|
+
count(*) AS calls, count(DISTINCT session_id) AS sessions
|
|
186
|
+
FROM tool_calls
|
|
187
|
+
WHERE tool_name = 'Bash' AND {proj_pred}
|
|
188
|
+
GROUP BY 1 ORDER BY calls DESC LIMIT ?
|
|
189
|
+
"""
|
|
190
|
+
else:
|
|
191
|
+
sql = f"""
|
|
192
|
+
SELECT tool_name, count(*) AS calls,
|
|
193
|
+
count(DISTINCT session_id) AS sessions,
|
|
194
|
+
count(DISTINCT project) AS projects
|
|
195
|
+
FROM tool_calls WHERE {proj_pred}
|
|
196
|
+
GROUP BY 1 ORDER BY calls DESC LIMIT ?
|
|
197
|
+
"""
|
|
198
|
+
_emit(*db.query(_con(ctx), sql, [project, project, limit]), fmt=fmt)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
# --------------------------------------------------------------------------- #
|
|
202
|
+
# errors
|
|
203
|
+
# --------------------------------------------------------------------------- #
|
|
204
|
+
@cli.command()
|
|
205
|
+
@click.option("--project", "-p", help="Filter to projects matching this.")
|
|
206
|
+
@click.option("--list", "list_", is_flag=True, help="List recent error events instead of a rollup.")
|
|
207
|
+
@click.option("--limit", "-n", default=30, help="Max rows.")
|
|
208
|
+
@_fmt_option
|
|
209
|
+
@click.pass_context
|
|
210
|
+
def errors(ctx: click.Context, project: str | None, list_: bool, limit: int, fmt: str) -> None:
|
|
211
|
+
"""API errors and retries (rate limits, transient failures), by project/status."""
|
|
212
|
+
proj_pred = "(? IS NULL OR project ILIKE '%' || ? || '%')"
|
|
213
|
+
if list_:
|
|
214
|
+
sql = f"""
|
|
215
|
+
SELECT left(session_id, 8) AS id, project, ts, status, model
|
|
216
|
+
FROM errors WHERE {proj_pred}
|
|
217
|
+
ORDER BY ts DESC NULLS LAST LIMIT ?
|
|
218
|
+
"""
|
|
219
|
+
else:
|
|
220
|
+
sql = f"""
|
|
221
|
+
SELECT project, status, count(*) AS hits,
|
|
222
|
+
count(DISTINCT session_id) AS sessions
|
|
223
|
+
FROM errors WHERE {proj_pred}
|
|
224
|
+
GROUP BY 1, 2 ORDER BY hits DESC LIMIT ?
|
|
225
|
+
"""
|
|
226
|
+
_emit(*db.query(_con(ctx), sql, [project, project, limit]), fmt=fmt)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
# --------------------------------------------------------------------------- #
|
|
230
|
+
# agents
|
|
231
|
+
# --------------------------------------------------------------------------- #
|
|
232
|
+
@cli.command()
|
|
233
|
+
@click.option(
|
|
234
|
+
"--by",
|
|
235
|
+
type=click.Choice(["type", "model", "session", "project"]),
|
|
236
|
+
default="type",
|
|
237
|
+
help="Group subagent dispatches by this dimension.",
|
|
238
|
+
)
|
|
239
|
+
@click.option("--limit", "-n", default=30, help="Max rows.")
|
|
240
|
+
@_fmt_option
|
|
241
|
+
@click.pass_context
|
|
242
|
+
def agents(ctx: click.Context, by: str, limit: int, fmt: str) -> None:
|
|
243
|
+
"""Subagent (Agent tool) dispatches and their token totals (unpriced)."""
|
|
244
|
+
dim = {
|
|
245
|
+
"type": "subagent_type",
|
|
246
|
+
"model": "model",
|
|
247
|
+
"session": "left(session_id, 8)",
|
|
248
|
+
"project": "project",
|
|
249
|
+
}[by]
|
|
250
|
+
sql = f"""
|
|
251
|
+
SELECT {dim} AS {by}, count(*) AS dispatches,
|
|
252
|
+
sum(subagent_tokens) AS subagent_tokens,
|
|
253
|
+
CAST(round(avg(subagent_tokens)) AS BIGINT) AS avg_tokens
|
|
254
|
+
FROM agents
|
|
255
|
+
GROUP BY 1 ORDER BY dispatches DESC NULLS LAST LIMIT ?
|
|
256
|
+
"""
|
|
257
|
+
_emit(*db.query(_con(ctx), sql, [limit]), fmt=fmt)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
# --------------------------------------------------------------------------- #
|
|
261
|
+
# session <id>
|
|
262
|
+
# --------------------------------------------------------------------------- #
|
|
263
|
+
@cli.command()
|
|
264
|
+
@click.argument("session_id")
|
|
265
|
+
@click.option("--limit", "-n", default=80, help="Max timeline rows.")
|
|
266
|
+
@click.pass_context
|
|
267
|
+
def session(ctx: click.Context, session_id: str, limit: int) -> None:
|
|
268
|
+
"""Show one session's decision timeline (prompts, tool calls, errors) by id prefix."""
|
|
269
|
+
con = _con(ctx)
|
|
270
|
+
_, rows = db.query(
|
|
271
|
+
con,
|
|
272
|
+
"SELECT session_id, project, git_branch, started_at, ended_at, duration_min, "
|
|
273
|
+
"messages, models, round(cost_usd, 2) FROM sessions "
|
|
274
|
+
"WHERE session_id LIKE ? || '%' LIMIT 2",
|
|
275
|
+
[session_id],
|
|
276
|
+
)
|
|
277
|
+
if not rows:
|
|
278
|
+
click.echo(f"No session matching prefix {session_id!r}.", err=True)
|
|
279
|
+
sys.exit(1)
|
|
280
|
+
if len(rows) > 1:
|
|
281
|
+
click.echo(f"Prefix {session_id!r} is ambiguous - be more specific.", err=True)
|
|
282
|
+
sys.exit(1)
|
|
283
|
+
full_id, project, branch, started, ended, dur, msgs, models, usd = rows[0]
|
|
284
|
+
click.echo(f"session {full_id}")
|
|
285
|
+
click.echo(f"project {project} branch {branch}")
|
|
286
|
+
click.echo(f"span {started} -> {ended} ({dur} min, {msgs} messages)")
|
|
287
|
+
model_list = models if isinstance(models, list) else []
|
|
288
|
+
click.echo(f"models {', '.join(str(m) for m in model_list)}")
|
|
289
|
+
click.echo(f"est cost ${usd}")
|
|
290
|
+
click.echo("-" * 60)
|
|
291
|
+
timeline_sql = """
|
|
292
|
+
SELECT ts, 'prompt' AS kind,
|
|
293
|
+
left(regexp_replace(text, '\\s+', ' ', 'g'), 110) AS detail
|
|
294
|
+
FROM prompts WHERE session_id = ? AND kind = 'prompt'
|
|
295
|
+
UNION ALL
|
|
296
|
+
SELECT ts, 'tool:' || tool_name,
|
|
297
|
+
left(coalesce(tool_input->>'$.command', tool_input->>'$.file_path',
|
|
298
|
+
tool_input->>'$.description', tool_input->>'$.pattern',
|
|
299
|
+
tool_input->>'$.query', tool_input->>'$.subagent_type', ''), 110)
|
|
300
|
+
FROM tool_calls WHERE session_id = ?
|
|
301
|
+
UNION ALL
|
|
302
|
+
SELECT ts, 'ERROR ' || status, coalesce(model, '')
|
|
303
|
+
FROM errors WHERE session_id = ?
|
|
304
|
+
ORDER BY ts NULLS FIRST
|
|
305
|
+
LIMIT ?
|
|
306
|
+
"""
|
|
307
|
+
tcols, trows = db.query(con, timeline_sql, [full_id, full_id, full_id, limit])
|
|
308
|
+
_emit(tcols, trows, "table")
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
# --------------------------------------------------------------------------- #
|
|
312
|
+
# search
|
|
313
|
+
# --------------------------------------------------------------------------- #
|
|
314
|
+
@cli.command()
|
|
315
|
+
@click.argument("text")
|
|
316
|
+
@click.option("--limit", "-n", default=25, help="Max matching sessions.")
|
|
317
|
+
@_fmt_option
|
|
318
|
+
@click.pass_context
|
|
319
|
+
def search(ctx: click.Context, text: str, limit: int, fmt: str) -> None:
|
|
320
|
+
"""Full-text search your typed prompts and session titles; returns matching sessions."""
|
|
321
|
+
sql = """
|
|
322
|
+
SELECT left(p.session_id, 8) AS id, s.project, s.started_at,
|
|
323
|
+
count(*) AS hits,
|
|
324
|
+
left(regexp_replace(any_value(p.text), '\\s+', ' ', 'g'), 90) AS sample
|
|
325
|
+
FROM prompts p LEFT JOIN sessions s ON s.session_id = p.session_id
|
|
326
|
+
WHERE lower(p.text) LIKE '%' || lower(?) || '%'
|
|
327
|
+
GROUP BY 1, 2, 3 ORDER BY s.started_at DESC NULLS LAST LIMIT ?
|
|
328
|
+
"""
|
|
329
|
+
_emit(*db.query(_con(ctx), sql, [text, limit]), fmt=fmt)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
# --------------------------------------------------------------------------- #
|
|
333
|
+
# sql
|
|
334
|
+
# --------------------------------------------------------------------------- #
|
|
335
|
+
@cli.command(name="sql")
|
|
336
|
+
@click.argument("query")
|
|
337
|
+
@_fmt_option
|
|
338
|
+
@click.pass_context
|
|
339
|
+
def sql_cmd(ctx: click.Context, query: str, fmt: str) -> None:
|
|
340
|
+
"""Run an arbitrary read-only SQL SELECT over the views (sessions, message_usage,
|
|
341
|
+
tool_calls, errors, agents, agent_results, events, prompts, model_pricing).
|
|
342
|
+
"""
|
|
343
|
+
try:
|
|
344
|
+
cols, rows = db.run_read_only(_con(ctx), query)
|
|
345
|
+
except db.UnsafeSQLError as exc:
|
|
346
|
+
click.echo(f"Refused: {exc}", err=True)
|
|
347
|
+
sys.exit(2)
|
|
348
|
+
_emit(cols, rows, fmt)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
# --------------------------------------------------------------------------- #
|
|
352
|
+
# serve (thin local web viewer)
|
|
353
|
+
# --------------------------------------------------------------------------- #
|
|
354
|
+
@cli.command()
|
|
355
|
+
@click.option("--port", "-p", default=8787, help="Port to bind on localhost.")
|
|
356
|
+
@click.pass_context
|
|
357
|
+
def serve(ctx: click.Context, port: int) -> None:
|
|
358
|
+
"""Launch a local web dashboard over your history (localhost only)."""
|
|
359
|
+
from ccq.server import serve as make_server # noqa: PLC0415 - optional/lazy import
|
|
360
|
+
|
|
361
|
+
httpd = make_server(_con(ctx), port=port)
|
|
362
|
+
url = f"http://127.0.0.1:{port}/"
|
|
363
|
+
click.echo(f"ccq viewer on {url} (Ctrl-C to stop)")
|
|
364
|
+
try:
|
|
365
|
+
httpd.serve_forever()
|
|
366
|
+
except KeyboardInterrupt:
|
|
367
|
+
click.echo("\nstopped.")
|
|
368
|
+
finally:
|
|
369
|
+
httpd.server_close()
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
# --------------------------------------------------------------------------- #
|
|
373
|
+
# cache (materialized snapshot for --fast)
|
|
374
|
+
# --------------------------------------------------------------------------- #
|
|
375
|
+
@cli.group()
|
|
376
|
+
def cache() -> None:
|
|
377
|
+
"""Manage the materialized snapshot that powers --fast."""
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
@cache.command(name="build")
|
|
381
|
+
@click.pass_context
|
|
382
|
+
def cache_build(ctx: click.Context) -> None:
|
|
383
|
+
"""(Re)build the snapshot from the transcripts."""
|
|
384
|
+
path = db.build_cache(ctx.obj["projects_dir"])
|
|
385
|
+
size_mb = path.stat().st_size / 1e6
|
|
386
|
+
click.echo(f"Built snapshot: {path} ({size_mb:.0f} MB)")
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
@cache.command(name="status")
|
|
390
|
+
def cache_status() -> None:
|
|
391
|
+
"""Show snapshot location, size, and age relative to the newest transcript."""
|
|
392
|
+
path = db.default_cache_path()
|
|
393
|
+
if not path.exists():
|
|
394
|
+
click.echo(f"No snapshot at {path}. Build one with `ccq cache build`.")
|
|
395
|
+
return
|
|
396
|
+
stale = db.is_cache_stale()
|
|
397
|
+
size_mb = path.stat().st_size / 1e6
|
|
398
|
+
click.echo(f"snapshot : {path} ({size_mb:.0f} MB)")
|
|
399
|
+
click.echo(f"status : {'STALE - transcripts changed since build' if stale else 'up to date'}")
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
@cache.command(name="path")
|
|
403
|
+
def cache_path() -> None:
|
|
404
|
+
"""Print the snapshot path."""
|
|
405
|
+
click.echo(str(db.default_cache_path()))
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
@cache.command(name="clear")
|
|
409
|
+
def cache_clear() -> None:
|
|
410
|
+
"""Delete the snapshot."""
|
|
411
|
+
path = db.default_cache_path()
|
|
412
|
+
if path.exists():
|
|
413
|
+
path.unlink()
|
|
414
|
+
click.echo(f"Removed {path}")
|
|
415
|
+
else:
|
|
416
|
+
click.echo("No snapshot to remove.")
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def main() -> None:
|
|
420
|
+
"""Console-script entry point."""
|
|
421
|
+
cli(obj={})
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
if __name__ == "__main__":
|
|
425
|
+
main()
|