cc-pushback 0.1.0__tar.gz

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.
@@ -0,0 +1,133 @@
1
+ Required Notice: Copyright Yasyf Mohamedali (https://github.com/yasyf/cc-pushback)
2
+
3
+ # PolyForm Noncommercial License 1.0.0
4
+
5
+ <https://polyformproject.org/licenses/noncommercial/1.0.0>
6
+
7
+ ## Acceptance
8
+
9
+ In order to get any license under these terms, you must agree
10
+ to them as both strict obligations and conditions to all
11
+ your licenses.
12
+
13
+ ## Copyright License
14
+
15
+ The licensor grants you a copyright license for the
16
+ software to do everything you might do with the software
17
+ that would otherwise infringe the licensor's copyright
18
+ in it for any permitted purpose. However, you may
19
+ only distribute the software according to [Distribution
20
+ License](#distribution-license) and make changes or new works
21
+ based on the software according to [Changes and New Works
22
+ License](#changes-and-new-works-license).
23
+
24
+ ## Distribution License
25
+
26
+ The licensor grants you an additional copyright license
27
+ to distribute copies of the software. Your license
28
+ to distribute covers distributing the software with
29
+ changes and new works permitted by [Changes and New Works
30
+ License](#changes-and-new-works-license).
31
+
32
+ ## Notices
33
+
34
+ You must ensure that anyone who gets a copy of any part of
35
+ the software from you also gets a copy of these terms or the
36
+ URL for them above, as well as copies of any plain-text lines
37
+ beginning with `Required Notice:` that the licensor provided
38
+ with the software. For example:
39
+
40
+ > Required Notice: Copyright Yoyodyne, Inc. (http://example.com)
41
+
42
+ ## Changes and New Works License
43
+
44
+ The licensor grants you an additional copyright license to
45
+ make changes and new works based on the software for any
46
+ permitted purpose.
47
+
48
+ ## Patent License
49
+
50
+ The licensor grants you a patent license for the software that
51
+ covers patent claims the licensor can license, or becomes able
52
+ to license, that you would infringe by using the software.
53
+
54
+ ## Noncommercial Purposes
55
+
56
+ Any noncommercial purpose is a permitted purpose.
57
+
58
+ ## Personal Uses
59
+
60
+ Personal use for research, experiment, and testing for
61
+ the benefit of public knowledge, personal study, private
62
+ entertainment, hobby projects, amateur pursuits, or religious
63
+ observance, without any anticipated commercial application,
64
+ is use for a permitted purpose.
65
+
66
+ ## Noncommercial Organizations
67
+
68
+ Use by any charitable organization, educational institution,
69
+ public research organization, public safety or health
70
+ organization, environmental protection organization,
71
+ or government institution is use for a permitted purpose
72
+ regardless of the source of funding or obligations resulting
73
+ from the funding.
74
+
75
+ ## Fair Use
76
+
77
+ You may have "fair use" rights for the software under the
78
+ law. These terms do not limit them.
79
+
80
+ ## No Other Rights
81
+
82
+ These terms do not allow you to sublicense or transfer any of
83
+ your licenses to anyone else, or prevent the licensor from
84
+ granting licenses to anyone else. These terms do not imply
85
+ any other licenses.
86
+
87
+ ## Patent Defense
88
+
89
+ If you make any written claim that the software infringes or
90
+ contributes to infringement of any patent, your patent license
91
+ for the software granted under these terms ends immediately. If
92
+ your company makes such a claim, your patent license ends
93
+ immediately for work on behalf of your company.
94
+
95
+ ## Violations
96
+
97
+ The first time you are notified in writing that you have
98
+ violated any of these terms, or done anything with the software
99
+ not covered by your licenses, your licenses can nonetheless
100
+ continue if you come into full compliance with these terms,
101
+ and take practical steps to correct past violations, within
102
+ 32 days of receiving notice. Otherwise, all your licenses
103
+ end immediately.
104
+
105
+ ## No Liability
106
+
107
+ ***As far as the law allows, the software comes as is, without
108
+ any warranty or condition, and the licensor will not be liable
109
+ to you for any damages arising out of these terms or the use
110
+ or nature of the software, under any kind of legal claim.***
111
+
112
+ ## Definitions
113
+
114
+ The **licensor** is the individual or entity offering these
115
+ terms, and the **software** is the software the licensor makes
116
+ available under these terms.
117
+
118
+ **You** refers to the individual or entity agreeing to these
119
+ terms.
120
+
121
+ **Your company** is any legal entity, sole proprietorship,
122
+ or other kind of organization that you work for, plus all
123
+ organizations that have control over, are under the control of,
124
+ or are under common control with that organization. **Control**
125
+ means ownership of substantially all the assets of an entity,
126
+ or the power to direct its management and policies by vote,
127
+ contract, or otherwise. Control can be direct or indirect.
128
+
129
+ **Your licenses** are all the licenses granted to you for the
130
+ software under these terms.
131
+
132
+ **Use** means anything you do with the software requiring one
133
+ of your licenses.
@@ -0,0 +1,135 @@
1
+ Metadata-Version: 2.4
2
+ Name: cc-pushback
3
+ Version: 0.1.0
4
+ Summary: Learn your pushback style from past Claude Code feedback and code reviews, and replicate it with a language model.
5
+ Keywords:
6
+ Author: Yasyf Mohamedali
7
+ Author-email: Yasyf Mohamedali <yasyfm@gmail.com>
8
+ License-Expression: PolyForm-Noncommercial-1.0.0
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Typing :: Typed
16
+ Requires-Dist: aiohttp>=3.10
17
+ Requires-Dist: anyio>=4.4
18
+ Requires-Dist: cc-transcript>=0.7,<0.8
19
+ Requires-Dist: click>=8
20
+ Requires-Dist: spawnllm>=0.1
21
+ Requires-Dist: pytest>=8.0 ; extra == 'dev'
22
+ Requires-Dist: ty>=0.0.44 ; extra == 'dev'
23
+ Requires-Dist: ruff>=0.8 ; extra == 'dev'
24
+ Requires-Python: >=3.13
25
+ Project-URL: Homepage, https://github.com/yasyf/cc-pushback
26
+ Project-URL: Documentation, https://yasyf.github.io/cc-pushback/
27
+ Project-URL: Repository, https://github.com/yasyf/cc-pushback
28
+ Project-URL: Issues, https://github.com/yasyf/cc-pushback/issues
29
+ Project-URL: Changelog, https://github.com/yasyf/cc-pushback/blob/main/CHANGELOG.md
30
+ Provides-Extra: dev
31
+ Description-Content-Type: text/markdown
32
+
33
+ # cc-pushback
34
+
35
+ [![PyPI](https://img.shields.io/pypi/v/cc-pushback.svg)](https://pypi.org/project/cc-pushback/)
36
+ [![Python](https://img.shields.io/pypi/pyversions/cc-pushback.svg)](https://pypi.org/project/cc-pushback/)
37
+ [![Docs](https://img.shields.io/github/actions/workflow/status/yasyf/cc-pushback/docs.yml?branch=main&label=docs)](https://yasyf.github.io/cc-pushback/)
38
+ [![License: PolyForm-Noncommercial-1.0.0](https://img.shields.io/badge/License-PolyForm--Noncommercial--1.0.0-blue.svg)](https://github.com/yasyf/cc-pushback/blob/main/LICENSE)
39
+
40
+ cc-pushback mines your Claude Code transcripts for the moments you pushed back — corrections, interrupts, rejected plans, code-review comments, "no, do it this way" — and collects them, with the surrounding conversational context, into a local database. That corpus is the raw material for learning your pushback style; this first release builds it.
41
+
42
+ ## Install
43
+
44
+ No install needed — run everything through [uvx](https://docs.astral.sh/uv/):
45
+
46
+ ```bash
47
+ uvx cc-pushback --help
48
+ ```
49
+
50
+ `uvx` fetches cc-pushback into a throwaway environment and runs it. To add it
51
+ to a project instead:
52
+
53
+ ```bash
54
+ uv add cc-pushback
55
+ ```
56
+
57
+ ## Quickstart
58
+
59
+ Scan your transcripts for the moments you pushed back and accumulate them into a
60
+ local feedback database:
61
+
62
+ ```bash
63
+ uvx cc-pushback scan
64
+ ```
65
+
66
+ ```
67
+ scanned 412 files, 1473 new rows
68
+ ```
69
+
70
+ `scan` is incremental and idempotent. Each transcript is parsed only when it is
71
+ new or has changed since the last scan, and every candidate is keyed by a content
72
+ digest, so re-running over unchanged inputs adds nothing. Recording a file and
73
+ inserting its candidates commit in one transaction — interrupt a scan and the
74
+ database is never left half-written. A transcript that fails to parse (one Claude
75
+ Code is still writing, say) is skipped and retried next time, never aborting the run.
76
+
77
+ The database lives at `~/.cc-pushback/feedback.db` by default (override with
78
+ `--db`). Inspect what has been collected:
79
+
80
+ ```bash
81
+ uvx cc-pushback stats # counts by source kind, and the scanned-file count
82
+ uvx cc-pushback list # recent feedback, newest first
83
+ uvx cc-pushback list --source plan_review --limit 50
84
+ ```
85
+
86
+ ### What gets collected
87
+
88
+ `scan` runs four detectors over each transcript:
89
+
90
+ - **Transcript messages** (`transcript_message`) — the pushback you typed
91
+ mid-session, after trivial acknowledgements and structural noise are filtered out.
92
+ - **Plan reviews** (`plan_review`) — rejected `ExitPlanMode` plans (with the
93
+ feedback you gave) and plan-mode re-entries right after an edit cycle, i.e.
94
+ "let's rethink this."
95
+ - **Interrupts and rejections** (`interrupt_rejection`) — permission denials and
96
+ `[Request interrupted by user]` corrections, with the denied tool and your
97
+ follow-up captured.
98
+ - **Review comments** (`review_comment`) — structured code-review messages
99
+ exploded into one row per inline comment.
100
+
101
+ Each row carries the conversational window around the feedback — the assistant
102
+ action it responded to, plus a few turns either side — captured at collection
103
+ time, because transcripts are ephemeral.
104
+
105
+ Restrict to specific kinds with `--source` (repeatable), or force a full re-mine
106
+ of every transcript (after a detector change, say) with `--full`:
107
+
108
+ ```bash
109
+ uvx cc-pushback list --source transcript_message --source plan_review
110
+ uvx cc-pushback scan --full
111
+ ```
112
+
113
+ ### Mining transcripts from another machine
114
+
115
+ Transcripts live under `~/.claude/projects`. To mine a remote machine's history,
116
+ mirror its projects directory locally with `rsync`, then scan that directory:
117
+
118
+ ```bash
119
+ rsync -az yasyf@yasyf:.claude/projects/ ~/.cc-pushback/mirrors/yasyf/
120
+ uvx cc-pushback scan --transcripts ~/.cc-pushback/mirrors/yasyf/
121
+ ```
122
+
123
+ `--transcripts` is repeatable, so you can fold several mirrors into one scan.
124
+ Because discovery is mtime-keyed, repeating the `rsync` and re-scanning only
125
+ ingests what changed.
126
+
127
+ ## What problems does this solve?
128
+
129
+ - **Your corrections evaporate.** Every "don't do it that way" you've typed into Claude Code is sitting unused in transcript files. cc-pushback turns that history into a structured dataset.
130
+ - **CLAUDE.md only captures what you remember to write down.** Most of your taste is tacit — you only notice a rule when it's violated. Collecting real pushbacks recovers the rules you never articulated.
131
+ - **The signal is buried in noise.** Trivial acknowledgements, structural reminders, and tool chatter drown out the moments that matter; cc-pushback keeps the pushback and discards the rest.
132
+
133
+ ## Docs
134
+
135
+ [Read the docs](https://yasyf.github.io/cc-pushback/) for the full guide and API reference.
@@ -0,0 +1,103 @@
1
+ # cc-pushback
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/cc-pushback.svg)](https://pypi.org/project/cc-pushback/)
4
+ [![Python](https://img.shields.io/pypi/pyversions/cc-pushback.svg)](https://pypi.org/project/cc-pushback/)
5
+ [![Docs](https://img.shields.io/github/actions/workflow/status/yasyf/cc-pushback/docs.yml?branch=main&label=docs)](https://yasyf.github.io/cc-pushback/)
6
+ [![License: PolyForm-Noncommercial-1.0.0](https://img.shields.io/badge/License-PolyForm--Noncommercial--1.0.0-blue.svg)](https://github.com/yasyf/cc-pushback/blob/main/LICENSE)
7
+
8
+ cc-pushback mines your Claude Code transcripts for the moments you pushed back — corrections, interrupts, rejected plans, code-review comments, "no, do it this way" — and collects them, with the surrounding conversational context, into a local database. That corpus is the raw material for learning your pushback style; this first release builds it.
9
+
10
+ ## Install
11
+
12
+ No install needed — run everything through [uvx](https://docs.astral.sh/uv/):
13
+
14
+ ```bash
15
+ uvx cc-pushback --help
16
+ ```
17
+
18
+ `uvx` fetches cc-pushback into a throwaway environment and runs it. To add it
19
+ to a project instead:
20
+
21
+ ```bash
22
+ uv add cc-pushback
23
+ ```
24
+
25
+ ## Quickstart
26
+
27
+ Scan your transcripts for the moments you pushed back and accumulate them into a
28
+ local feedback database:
29
+
30
+ ```bash
31
+ uvx cc-pushback scan
32
+ ```
33
+
34
+ ```
35
+ scanned 412 files, 1473 new rows
36
+ ```
37
+
38
+ `scan` is incremental and idempotent. Each transcript is parsed only when it is
39
+ new or has changed since the last scan, and every candidate is keyed by a content
40
+ digest, so re-running over unchanged inputs adds nothing. Recording a file and
41
+ inserting its candidates commit in one transaction — interrupt a scan and the
42
+ database is never left half-written. A transcript that fails to parse (one Claude
43
+ Code is still writing, say) is skipped and retried next time, never aborting the run.
44
+
45
+ The database lives at `~/.cc-pushback/feedback.db` by default (override with
46
+ `--db`). Inspect what has been collected:
47
+
48
+ ```bash
49
+ uvx cc-pushback stats # counts by source kind, and the scanned-file count
50
+ uvx cc-pushback list # recent feedback, newest first
51
+ uvx cc-pushback list --source plan_review --limit 50
52
+ ```
53
+
54
+ ### What gets collected
55
+
56
+ `scan` runs four detectors over each transcript:
57
+
58
+ - **Transcript messages** (`transcript_message`) — the pushback you typed
59
+ mid-session, after trivial acknowledgements and structural noise are filtered out.
60
+ - **Plan reviews** (`plan_review`) — rejected `ExitPlanMode` plans (with the
61
+ feedback you gave) and plan-mode re-entries right after an edit cycle, i.e.
62
+ "let's rethink this."
63
+ - **Interrupts and rejections** (`interrupt_rejection`) — permission denials and
64
+ `[Request interrupted by user]` corrections, with the denied tool and your
65
+ follow-up captured.
66
+ - **Review comments** (`review_comment`) — structured code-review messages
67
+ exploded into one row per inline comment.
68
+
69
+ Each row carries the conversational window around the feedback — the assistant
70
+ action it responded to, plus a few turns either side — captured at collection
71
+ time, because transcripts are ephemeral.
72
+
73
+ Restrict to specific kinds with `--source` (repeatable), or force a full re-mine
74
+ of every transcript (after a detector change, say) with `--full`:
75
+
76
+ ```bash
77
+ uvx cc-pushback list --source transcript_message --source plan_review
78
+ uvx cc-pushback scan --full
79
+ ```
80
+
81
+ ### Mining transcripts from another machine
82
+
83
+ Transcripts live under `~/.claude/projects`. To mine a remote machine's history,
84
+ mirror its projects directory locally with `rsync`, then scan that directory:
85
+
86
+ ```bash
87
+ rsync -az yasyf@yasyf:.claude/projects/ ~/.cc-pushback/mirrors/yasyf/
88
+ uvx cc-pushback scan --transcripts ~/.cc-pushback/mirrors/yasyf/
89
+ ```
90
+
91
+ `--transcripts` is repeatable, so you can fold several mirrors into one scan.
92
+ Because discovery is mtime-keyed, repeating the `rsync` and re-scanning only
93
+ ingests what changed.
94
+
95
+ ## What problems does this solve?
96
+
97
+ - **Your corrections evaporate.** Every "don't do it that way" you've typed into Claude Code is sitting unused in transcript files. cc-pushback turns that history into a structured dataset.
98
+ - **CLAUDE.md only captures what you remember to write down.** Most of your taste is tacit — you only notice a rule when it's violated. Collecting real pushbacks recovers the rules you never articulated.
99
+ - **The signal is buried in noise.** Trivial acknowledgements, structural reminders, and tool chatter drown out the moments that matter; cc-pushback keeps the pushback and discards the rest.
100
+
101
+ ## Docs
102
+
103
+ [Read the docs](https://yasyf.github.io/cc-pushback/) for the full guide and API reference.
@@ -0,0 +1,28 @@
1
+ """Collect developer pushback signals from existing Claude Code transcripts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from cc_pushback.context import ContextSnapshot, ContextTurn, build_snapshot
6
+ from cc_pushback.detectors import Detector, detect
7
+ from cc_pushback.models import DedupKey, FeedbackCandidate, SourceKind, dedup_key
8
+ from cc_pushback.scan import ScanReport, scan
9
+ from cc_pushback.spec import PUSHBACK_SPEC
10
+ from cc_pushback.store import FeedbackStore
11
+
12
+ # Not the retired export-control convention: this exists only so great-docs' API
13
+ # reference skips the SourceKind (Literal) and DedupKey (NewType) aliases, which its
14
+ # dynamic walker cannot render ("Cannot handle auto for object kind: TYPE_ALIAS").
15
+ # great-docs documents __all__ when present; keep it in sync with the re-exports above.
16
+ __all__ = [
17
+ "PUSHBACK_SPEC",
18
+ "ContextSnapshot",
19
+ "ContextTurn",
20
+ "Detector",
21
+ "FeedbackCandidate",
22
+ "FeedbackStore",
23
+ "ScanReport",
24
+ "build_snapshot",
25
+ "dedup_key",
26
+ "detect",
27
+ "scan",
28
+ ]
@@ -0,0 +1,6 @@
1
+ from __future__ import annotations
2
+
3
+ from cc_pushback.cli import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -0,0 +1,46 @@
1
+ """A thin shell-out to the ``claude`` CLI for a single headless completion.
2
+
3
+ Argv construction and envelope parsing come from the shared ``spawnllm`` library;
4
+ the spawn stays local (``anyio.run_process``). It uses the user's existing Claude
5
+ Code auth (no API key), so the package stays offline unless ``claude`` is
6
+ actually on the path.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import shutil
12
+ import subprocess
13
+
14
+ import anyio
15
+ from spawnllm import ClaudeCliBackend, parse_result_envelope
16
+
17
+ CLAUDE_TIMEOUT = 180
18
+
19
+
20
+ def claude_available() -> bool:
21
+ """Returns whether the ``claude`` CLI is on ``PATH``."""
22
+ return shutil.which("claude") is not None
23
+
24
+
25
+ async def run_claude(prompt: str, *, system: str, model: str) -> str:
26
+ """Runs one headless ``claude`` turn and returns its text result.
27
+
28
+ Args:
29
+ prompt: The user message to send.
30
+ system: The system prompt.
31
+ model: The model to run, for example ``claude-sonnet-4-6``.
32
+
33
+ Returns:
34
+ The assistant's text response — the ``result`` field of the JSON output.
35
+
36
+ Raises:
37
+ subprocess.SubprocessError: If ``claude`` exits non-zero, times out, or
38
+ reports an error in its JSON envelope.
39
+ """
40
+ argv = ClaudeCliBackend.cc_sentiment(system_prompt=system).build_argv(prompt, model=model)
41
+ try:
42
+ with anyio.fail_after(CLAUDE_TIMEOUT):
43
+ result = await anyio.run_process(argv, check=True)
44
+ except TimeoutError as exc:
45
+ raise subprocess.TimeoutExpired(argv, CLAUDE_TIMEOUT) from exc
46
+ return parse_result_envelope(result.stdout, argv=argv, stderr=result.stderr)
@@ -0,0 +1,138 @@
1
+ """The ``cc-pushback`` command-line interface: scan, stats, list, and view-samples."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import functools
6
+ from collections.abc import Awaitable, Callable
7
+ from pathlib import Path
8
+
9
+ import anyio
10
+ import click
11
+ from cc_transcript import CLAUDE_PROJECTS_DIR
12
+
13
+ from cc_pushback.models import PUSHBACK_SOURCE_KINDS, SourceKind
14
+ from cc_pushback.report import Sample, build_summary, render_html
15
+ from cc_pushback.scan import scan as run_scan
16
+ from cc_pushback.serve import serve
17
+ from cc_pushback.store import FeedbackStore
18
+
19
+ SOURCE_KINDS = [*PUSHBACK_SOURCE_KINDS]
20
+
21
+
22
+ def coro[**P, R](fn: Callable[P, Awaitable[R]]) -> Callable[P, R]:
23
+ """Adapts an async command body into the sync callback Click expects."""
24
+
25
+ @functools.wraps(fn)
26
+ def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
27
+ return anyio.run(functools.partial(fn, *args, **kwargs))
28
+
29
+ return wrapper
30
+
31
+
32
+ @click.group()
33
+ @click.version_option(package_name="cc-pushback")
34
+ def main() -> None:
35
+ """Collect developer pushback signals from existing Claude Code transcripts."""
36
+
37
+
38
+ @main.command()
39
+ @click.option(
40
+ "--transcripts",
41
+ "transcripts",
42
+ multiple=True,
43
+ type=click.Path(exists=True, file_okay=False, path_type=Path),
44
+ help="Transcript directories to scan. Defaults to ~/.claude/projects.",
45
+ )
46
+ @click.option("--full", is_flag=True, help="Re-scan every transcript, ignoring recorded mtimes.")
47
+ @click.option(
48
+ "--db",
49
+ type=click.Path(dir_okay=False, path_type=Path),
50
+ default=None,
51
+ help="Database path. Defaults to ~/.cc-pushback/feedback.db.",
52
+ )
53
+ @coro
54
+ async def scan(transcripts: tuple[Path, ...], full: bool, db: Path | None) -> None:
55
+ """Scan transcripts for feedback, incrementally.
56
+
57
+ Each transcript is parsed only when new or modified since the last scan, and
58
+ every candidate is inserted with ``INSERT OR IGNORE`` keyed by a content
59
+ digest, so re-running ``scan`` over unchanged inputs is a no-op. Recording a
60
+ file and inserting its candidates commit in one transaction.
61
+ """
62
+ roots = transcripts or (CLAUDE_PROJECTS_DIR,)
63
+ async with await FeedbackStore.open(db or FeedbackStore.default_path()) as store:
64
+ report = await run_scan(store, roots, full=full)
65
+ click.echo(f"scanned {report.scanned} files, {report.inserted} new rows")
66
+
67
+
68
+ @main.command()
69
+ @click.option(
70
+ "--db",
71
+ type=click.Path(dir_okay=False, path_type=Path),
72
+ default=None,
73
+ help="Database path. Defaults to ~/.cc-pushback/feedback.db.",
74
+ )
75
+ @coro
76
+ async def stats(db: Path | None) -> None:
77
+ """Print ingestion counts by source kind and the scanned-file count."""
78
+ async with await FeedbackStore.open(db or FeedbackStore.default_path()) as store:
79
+ report = await store.stats()
80
+ click.echo(f"total: {report.total} files: {report.files}")
81
+ for kind, count in report.by_source.items():
82
+ click.echo(f" {kind}: {count}")
83
+
84
+
85
+ @main.command(name="list")
86
+ @click.option(
87
+ "--source",
88
+ "source",
89
+ type=click.Choice(SOURCE_KINDS),
90
+ default=None,
91
+ help="Restrict to one source kind.",
92
+ )
93
+ @click.option("--limit", type=int, default=20, show_default=True, help="Maximum events to show.")
94
+ @click.option(
95
+ "--db",
96
+ type=click.Path(dir_okay=False, path_type=Path),
97
+ default=None,
98
+ help="Database path. Defaults to ~/.cc-pushback/feedback.db.",
99
+ )
100
+ @coro
101
+ async def list_(source: SourceKind | None, limit: int, db: Path | None) -> None:
102
+ """List recent feedback events, newest first."""
103
+ async with await FeedbackStore.open(db or FeedbackStore.default_path()) as store:
104
+ rows = await store.recent(source_kind=source, limit=limit)
105
+ for row in rows:
106
+ click.echo(f"[{row['source_kind']}] {row['occurred_at']} {str(row['text'])[:200]}")
107
+
108
+
109
+ @main.command(name="view-samples")
110
+ @click.option(
111
+ "--db",
112
+ type=click.Path(dir_okay=False, path_type=Path),
113
+ default=None,
114
+ help="Database path. Defaults to ~/.cc-pushback/feedback.db.",
115
+ )
116
+ @click.option(
117
+ "--llm/--no-llm",
118
+ default=True,
119
+ show_default=True,
120
+ help="Summarize with the claude CLI when it is on PATH, else use heuristics.",
121
+ )
122
+ @click.option("--model", default="claude-sonnet-4-6", show_default=True, help="Model for the claude CLI summary.")
123
+ @click.option("--port", type=int, default=0, show_default=True, help="Port to serve on; 0 picks a free one.")
124
+ @click.option("--open", "open_", is_flag=True, help="Open the page in a browser once serving.")
125
+ @coro
126
+ async def view_samples(db: Path | None, llm: bool, model: str, port: int, open_: bool) -> None:
127
+ """Render every collected sample into one HTML page and serve it locally.
128
+
129
+ The page leads with a corpus summary and highlights, then lists every sample
130
+ with a kind filter, a free-text search, and an expandable context window. It is
131
+ built in memory and served over a transient HTTP server whose URL is printed;
132
+ press Ctrl-C to stop. The summary is written by the ``claude`` CLI when ``--llm``
133
+ is set and ``claude`` is installed, falling back to deterministic heuristics.
134
+ """
135
+ async with await FeedbackStore.open(db or FeedbackStore.default_path()) as store:
136
+ samples = [Sample.from_row(row) for row in await store.events()]
137
+ summary = await build_summary(samples, use_llm=llm, model=model)
138
+ await serve(render_html(samples, summary).encode("utf-8"), port=port, open_browser=open_)
@@ -0,0 +1,11 @@
1
+ """Re-exports the conversational-window primitive from the mining domain.
2
+
3
+ Deprecated: import these names from :mod:`cc_transcript.domains.mining`. This shim
4
+ keeps cc-pushback's historical import paths working for at least one release.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from cc_transcript.domains.mining import ContextSnapshot, ContextTurn, build_snapshot, trigger_for, turn_for
10
+
11
+ __all__ = ["ContextSnapshot", "ContextTurn", "build_snapshot", "trigger_for", "turn_for"]