introspy 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.
Files changed (106) hide show
  1. introspy-0.1.0/.claude/hooks/session-start.sh +14 -0
  2. introspy-0.1.0/.claude/mcp.json +8 -0
  3. introspy-0.1.0/.claude/settings.json +14 -0
  4. introspy-0.1.0/.claude/skills/nolegend/SKILL.md +207 -0
  5. introspy-0.1.0/.claude/skills/python-review/SKILL.md +110 -0
  6. introspy-0.1.0/.github/workflows/ci.yml +72 -0
  7. introspy-0.1.0/.github/workflows/release.yml +36 -0
  8. introspy-0.1.0/.gitignore +19 -0
  9. introspy-0.1.0/CLAUDE.md +50 -0
  10. introspy-0.1.0/PKG-INFO +14 -0
  11. introspy-0.1.0/README.md +97 -0
  12. introspy-0.1.0/docs/architecture.md +197 -0
  13. introspy-0.1.0/pyproject.toml +159 -0
  14. introspy-0.1.0/queries.sql +124 -0
  15. introspy-0.1.0/schema-notes.md +74 -0
  16. introspy-0.1.0/scripts/install-hooks.sh +9 -0
  17. introspy-0.1.0/scripts/pre-commit.sh +32 -0
  18. introspy-0.1.0/src/introspect/__init__.py +1 -0
  19. introspy-0.1.0/src/introspect/api/__init__.py +0 -0
  20. introspy-0.1.0/src/introspect/api/handlers/__init__.py +0 -0
  21. introspy-0.1.0/src/introspect/api/handlers/_helpers.py +489 -0
  22. introspy-0.1.0/src/introspect/api/handlers/bash.py +173 -0
  23. introspy-0.1.0/src/introspect/api/handlers/cost_breakdown.py +441 -0
  24. introspy-0.1.0/src/introspect/api/handlers/cost_overview.py +789 -0
  25. introspy-0.1.0/src/introspect/api/handlers/dashboard.py +92 -0
  26. introspy-0.1.0/src/introspect/api/handlers/mcps.py +120 -0
  27. introspy-0.1.0/src/introspect/api/handlers/raw.py +102 -0
  28. introspy-0.1.0/src/introspect/api/handlers/refresh.py +204 -0
  29. introspy-0.1.0/src/introspect/api/handlers/search.py +73 -0
  30. introspy-0.1.0/src/introspect/api/handlers/sessions.py +1780 -0
  31. introspy-0.1.0/src/introspect/api/handlers/stats.py +306 -0
  32. introspy-0.1.0/src/introspect/api/handlers/subagents.py +305 -0
  33. introspy-0.1.0/src/introspect/api/handlers/tokenscape.py +1468 -0
  34. introspy-0.1.0/src/introspect/api/handlers/tools.py +140 -0
  35. introspy-0.1.0/src/introspect/api/main.py +154 -0
  36. introspy-0.1.0/src/introspect/api/routes.py +190 -0
  37. introspy-0.1.0/src/introspect/cli.py +587 -0
  38. introspy-0.1.0/src/introspect/db.py +1012 -0
  39. introspy-0.1.0/src/introspect/mcp/__init__.py +0 -0
  40. introspy-0.1.0/src/introspect/mcp/_register.py +29 -0
  41. introspy-0.1.0/src/introspect/mcp/refresh_bridge.py +47 -0
  42. introspy-0.1.0/src/introspect/mcp/server.py +15 -0
  43. introspy-0.1.0/src/introspect/mcp/tools.py +441 -0
  44. introspy-0.1.0/src/introspect/pricing.py +156 -0
  45. introspy-0.1.0/src/introspect/projects.py +44 -0
  46. introspy-0.1.0/src/introspect/refresh.py +277 -0
  47. introspy-0.1.0/src/introspect/search.py +281 -0
  48. introspy-0.1.0/src/introspect/sql_fragments.py +165 -0
  49. introspy-0.1.0/src/introspect/templates/_cost_portfolio_panel.html +143 -0
  50. introspy-0.1.0/src/introspect/templates/_daily_cost_panel.html +45 -0
  51. introspy-0.1.0/src/introspect/templates/_hourly_cost_panel.html +21 -0
  52. introspy-0.1.0/src/introspect/templates/_refresh_indicator.html +58 -0
  53. introspy-0.1.0/src/introspect/templates/_session_cost.html +107 -0
  54. introspy-0.1.0/src/introspect/templates/_session_cost_bloat.html +104 -0
  55. introspy-0.1.0/src/introspect/templates/_session_messages.html +107 -0
  56. introspy-0.1.0/src/introspect/templates/_session_subagents.html +85 -0
  57. introspy-0.1.0/src/introspect/templates/_session_tokenscape.html +130 -0
  58. introspy-0.1.0/src/introspect/templates/_spend_shapes.html +37 -0
  59. introspy-0.1.0/src/introspect/templates/base.html +373 -0
  60. introspy-0.1.0/src/introspect/templates/bash.html +127 -0
  61. introspy-0.1.0/src/introspect/templates/cost_overview.html +16 -0
  62. introspy-0.1.0/src/introspect/templates/dashboard.html +92 -0
  63. introspy-0.1.0/src/introspect/templates/mcps.html +125 -0
  64. introspy-0.1.0/src/introspect/templates/partial.html +2 -0
  65. introspy-0.1.0/src/introspect/templates/raw.html +175 -0
  66. introspy-0.1.0/src/introspect/templates/search.html +70 -0
  67. introspy-0.1.0/src/introspect/templates/session_detail.html +68 -0
  68. introspy-0.1.0/src/introspect/templates/sessions.html +139 -0
  69. introspy-0.1.0/src/introspect/templates/stats.html +345 -0
  70. introspy-0.1.0/src/introspect/templates/tools.html +129 -0
  71. introspy-0.1.0/tests/__init__.py +0 -0
  72. introspy-0.1.0/tests/conftest.py +145 -0
  73. introspy-0.1.0/tests/e2e/__init__.py +0 -0
  74. introspy-0.1.0/tests/e2e/conftest.py +278 -0
  75. introspy-0.1.0/tests/e2e/data/projects/project-alpha/aaaa1111-aaaa-aaaa-aaaa-aaaaaaaaaaaa.jsonl +9 -0
  76. introspy-0.1.0/tests/e2e/data/projects/project-beta/bbbb2222-bbbb-bbbb-bbbb-bbbbbbbbbbbb.jsonl +11 -0
  77. introspy-0.1.0/tests/e2e/test_crawl.py +33 -0
  78. introspy-0.1.0/tests/e2e/test_flows.py +75 -0
  79. introspy-0.1.0/tests/routes/__init__.py +0 -0
  80. introspy-0.1.0/tests/routes/conftest.py +241 -0
  81. introspy-0.1.0/tests/routes/cost_helpers.py +255 -0
  82. introspy-0.1.0/tests/routes/test_app_lifecycle.py +158 -0
  83. introspy-0.1.0/tests/routes/test_cost_breakdown.py +393 -0
  84. introspy-0.1.0/tests/routes/test_cost_chart.py +386 -0
  85. introspy-0.1.0/tests/routes/test_cost_overview.py +603 -0
  86. introspy-0.1.0/tests/routes/test_cost_tab.py +330 -0
  87. introspy-0.1.0/tests/routes/test_dashboard_stats.py +150 -0
  88. introspy-0.1.0/tests/routes/test_raw_page.py +81 -0
  89. introspy-0.1.0/tests/routes/test_refresh_ui.py +319 -0
  90. introspy-0.1.0/tests/routes/test_search_page.py +116 -0
  91. introspy-0.1.0/tests/routes/test_session_detail.py +311 -0
  92. introspy-0.1.0/tests/routes/test_session_subagents.py +537 -0
  93. introspy-0.1.0/tests/routes/test_sessions_list.py +261 -0
  94. introspy-0.1.0/tests/routes/test_spend_shape.py +420 -0
  95. introspy-0.1.0/tests/routes/test_tokenscape.py +364 -0
  96. introspy-0.1.0/tests/routes/test_tokenscape_subagents.py +628 -0
  97. introspy-0.1.0/tests/routes/test_tool_pages.py +254 -0
  98. introspy-0.1.0/tests/routes/tokenscape_helpers.py +221 -0
  99. introspy-0.1.0/tests/test_cli.py +206 -0
  100. introspy-0.1.0/tests/test_db.py +646 -0
  101. introspy-0.1.0/tests/test_mcp_tools.py +510 -0
  102. introspy-0.1.0/tests/test_pricing.py +135 -0
  103. introspy-0.1.0/tests/test_projects.py +185 -0
  104. introspy-0.1.0/tests/test_refresh.py +422 -0
  105. introspy-0.1.0/tests/test_search.py +325 -0
  106. introspy-0.1.0/uv.lock +1390 -0
@@ -0,0 +1,14 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+
4
+ # Only run in remote (Claude Code on the web) environments
5
+ if [ "${CLAUDE_CODE_REMOTE:-}" != "true" ]; then
6
+ exit 0
7
+ fi
8
+
9
+ # Install dependencies
10
+ cd "$CLAUDE_PROJECT_DIR"
11
+ uv sync --quiet
12
+
13
+ # Install pre-commit hooks
14
+ bash scripts/install-hooks.sh
@@ -0,0 +1,8 @@
1
+ {
2
+ "mcpServers": {
3
+ "introspect": {
4
+ "type": "http",
5
+ "url": "http://127.0.0.1:8000/mcp"
6
+ }
7
+ }
8
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "hooks": {
3
+ "SessionStart": [
4
+ {
5
+ "hooks": [
6
+ {
7
+ "type": "command",
8
+ "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/session-start.sh"
9
+ }
10
+ ]
11
+ }
12
+ ]
13
+ }
14
+ }
@@ -0,0 +1,207 @@
1
+ ---
2
+ name: nolegend
3
+ description: >
4
+ Tufte-style Plotly charts in introspect's FastAPI/Jinja stack. Use this
5
+ whenever you add or modify a chart — bar, line, scatter, sparkline, or
6
+ any go.Figure-driven visualization. Trigger on words like "clean",
7
+ "minimal", "professional", "boardroom-ready", or whenever a new
8
+ visualization is being built or an existing one is unclear. Charts in
9
+ this repo are built server-side with go.Figure + nolegend.activate()
10
+ and bootstrapped client-side via Plotly.newPlot in base.html — there is
11
+ no Plotly Express or marimo here.
12
+ ---
13
+
14
+ # nolegend — Plotly visualization rules for introspect
15
+
16
+ *Remove the legend to become one.*
17
+
18
+ Every chart should tell a story, not decorate the page. Apply Tufte's
19
+ principles, then refine with the helpers from the `nolegend` package.
20
+
21
+ ## Where charts live in this project
22
+
23
+ - Server side: `go.Figure` built in a handler (e.g.
24
+ `cost_breakdown.py:_build_figure`, `sessions.py:_render_multi_chart`),
25
+ serialised via `fig.to_json()`, embedded into a `<script
26
+ type="application/json">` tag.
27
+ - Client side: `base.html`'s `initCostChart` JS finds elements with
28
+ `class="cost-chart"`, parses the JSON, and runs `Plotly.newPlot`. Click
29
+ / select handlers are added via `el.on('plotly_click', ...)` →
30
+ `htmx.ajax(...)` for HTMX fragment swaps.
31
+ - Template: `nolegend.activate()` is called lazily on first render
32
+ (`_ensure_template()` pattern — keeps imports side-effect-free).
33
+ - Plotly Express is **not** used. Build figures directly with
34
+ `go.Figure` and `add_trace`. Keep Plotly Express advice from upstream
35
+ nolegend out of this project.
36
+
37
+ ## Core principles
38
+
39
+ 1. **Maximize data-ink ratio.** Every pixel encodes data. Remove
40
+ gridlines, borders, backgrounds, decorations. The `tufte` template
41
+ does most of this — don't undo it with overrides.
42
+ 2. **No chartjunk.** No 3D, no gradient fills, no drop shadows, no
43
+ coloured backgrounds.
44
+ 3. **Direct-label over legends.** This is the load-bearing rule. With a
45
+ view toggle that flips trace visibility, set
46
+ `showlegend=False` on the layout and put the label on the line itself
47
+ (see "Direct labels on toggled views" below).
48
+ 4. **Titles convey insight, not description.** When the chart has its
49
+ own caption (most do here, via the surrounding `<h2>`), keep the
50
+ in-figure title empty so the plot area isn't pushed down.
51
+ 5. **Range frames.** Axes span the data range, not arbitrary round
52
+ numbers. The `tufte` template does this for line/scatter; bars
53
+ default to starting at 0.
54
+ 6. **Restrained colour.** ≤ 5 colours per chart. Gray (`#b0b0b0`) is
55
+ for context. Keep the most expensive / most important series in the
56
+ highest-contrast slot (e.g. `_INVOCATION_COLOR_PALETTE` puts red at
57
+ index 0 so the runaway subagent stands out).
58
+
59
+ ## Direct labels on toggled views
60
+
61
+ The session detail Cost tab is the canonical example: one figure with
62
+ multiple Scatter traces, view toggle (`Total / Agent / Category /
63
+ Invocations`) flips visibility via `Plotly.restyle`. Without a legend
64
+ the user can't tell which line is which — so we direct-label every
65
+ line trace at its endpoint.
66
+
67
+ Pattern (from `_render_multi_chart`):
68
+
69
+ ```python
70
+ text_labels = [""] * len(ys)
71
+ if ys:
72
+ text_labels[-1] = f" {name}" # leading space avoids hugging the line
73
+
74
+ fig.add_trace(go.Scatter(
75
+ x=x_axis,
76
+ y=ys,
77
+ mode="lines+text",
78
+ name=name,
79
+ line={"color": color, "width": 1.5},
80
+ text=text_labels,
81
+ textposition="middle right",
82
+ textfont={"color": color, "size": 11},
83
+ cliponaxis=False,
84
+ customdata=...,
85
+ hovertemplate=...,
86
+ ))
87
+ ```
88
+
89
+ Plus, on the layout:
90
+
91
+ ```python
92
+ fig.update_layout(
93
+ showlegend=False,
94
+ margin={"l": 60, "r": 110, "t": 20, "b": 60}, # right margin holds labels
95
+ )
96
+ ```
97
+
98
+ Why per-trace `text` rather than `add_annotation`:
99
+
100
+ - Annotations are figure-wide and **don't toggle** with trace visibility.
101
+ When the user flips from "Total" to "By agent", annotation labels for
102
+ hidden traces would still float in space.
103
+ - Per-trace `text` is part of the trace, so visibility toggling
104
+ (`Plotly.restyle({visible: ...})`) hides the labels too, automatically.
105
+ - One label per trace at the last point (rest empty) is enough — the
106
+ reader sees the colour + name pair at the line endpoint.
107
+
108
+ For stacked bar charts (cost-overview daily/hourly), use
109
+ `add_annotation` instead — bars don't have an "endpoint" to attach text
110
+ to, so place the label at the peak segment of each top-N group. See
111
+ `cost_breakdown.py:_compute_top_group_annotations` for the pattern.
112
+
113
+ ## Hover, click, select interactivity
114
+
115
+ Charts in this repo are interactive surfaces, not static images. Keep
116
+ this in mind:
117
+
118
+ - `customdata` is your friend. Put per-point identifiers
119
+ (uuid, day, session_id) in `customdata` so the click handler can fire
120
+ the right HTMX request. Example: session cost chart's bucket points
121
+ carry `[first_uuid, last_uuid, msg_count]`.
122
+ - `hovertemplate` should reference customdata — it gives the user the
123
+ context they need before they decide to click.
124
+ - `dragmode='select'` for any chart that supports box-filtering. Force
125
+ it via `Plotly.relayout` after `newPlot` in case the modebar resets it.
126
+ - Line-only traces don't reliably populate `evt.points` on box-select.
127
+ Read `evt.range.x` and look up customdata from `el.data[trace_idx]`.
128
+ - Markers always-visible: when a chart has a "marker overlay" trace
129
+ (spike/slope highlights), keep it visible across all view toggles by
130
+ flagging its trace index in the JS `viewMap` logic.
131
+
132
+ ## Colour rules (hard constraints)
133
+
134
+ - Never use more than 5 colours in a single chart (excluding the
135
+ marker overlay).
136
+ - Never use rainbow / jet colorscales — not perceptually uniform.
137
+ - Never use red and green together without another differentiator.
138
+ - Use `_MAIN_AGENT_COLOR` for "main / total" series so the same colour
139
+ consistently means the same thing across the app's charts.
140
+ - For sequential data, `nolegend.SEQUENTIAL` or `viridis`.
141
+ - When in doubt: fewer colours. One colour + gray usually suffices.
142
+
143
+ ## Pinned colour map for re-rendered panels
144
+
145
+ When two panels show overlapping groups (e.g. daily totals and hourly
146
+ drill-down by project), build a single canonical colour map from the
147
+ all-time aggregate and pass it to both renderers. This way "project
148
+ foo" is the same colour in every panel. See
149
+ `cost_breakdown.py:_canonical_color_map` for the pattern.
150
+
151
+ ## Chart type selection
152
+
153
+ Match the chart to the question, not the other way around.
154
+
155
+ | Question type | Chart |
156
+ |---|---|
157
+ | How did it change over time? | Line |
158
+ | How do groups compare? | Horizontal bar |
159
+ | What's the relationship? | Scatter |
160
+ | What's the distribution? | Histogram or box |
161
+ | What's the composition? | Stacked bar |
162
+ | What's the trend per group? | Small multiples (faceted bars/lines) |
163
+ | What's the quick trend? | Sparkline |
164
+
165
+ **Never use:** pie charts, donut charts, 3D charts, radar/spider charts,
166
+ gauge charts.
167
+
168
+ ## Typography and titles
169
+
170
+ - **In-card heading carries the title.** The surrounding `<h2>` in the
171
+ Jinja template is the chart's title — keep `fig.update_layout(title=...)`
172
+ empty so the plot area sits at the top of its card.
173
+ - **Axis labels: only when non-obvious.** "Day" on a daily-bucket
174
+ x-axis is fine. "USD" on a cost y-axis is useful. Skip generic
175
+ "Value" / "Count".
176
+ - **Font: serif (Georgia).** The `tufte` template handles this. The
177
+ base.html stylesheet pins `#js-plotly-tester` to Georgia too —
178
+ don't remove that rule, it prevents axis-title displacement after
179
+ HTMX swaps.
180
+
181
+ ## Anti-patterns to catch and fix
182
+
183
+ | Anti-pattern | Fix |
184
+ |---|---|
185
+ | Legend box with 2+ entries on a toggled chart | Direct labels via per-trace `text` |
186
+ | Gridlines visible | `showgrid=False` (template handles it) |
187
+ | Default Plotly blue everywhere | Use the project palette constants |
188
+ | Title says "Chart of X by Y" | Rewrite the surrounding `<h2>` as the insight |
189
+ | Pie chart | Replace with horizontal bar |
190
+ | Too many colours (>5) | Aggregate, fold into "Other", or use spotlight |
191
+ | Y-axis starts at 0 when data starts at 800 | Trust the tufte template's range frame |
192
+ | Annotations on a toggled-trace chart | Use per-trace `text` instead |
193
+ | `fig.update_layout(title=...)` set | Drop it — the `<h2>` is the title |
194
+ | HTMX swap rebuilds chart from scratch and loses selection state | Use `Plotly.restyle` for visibility flips |
195
+
196
+ ## Checklist before merging a chart change
197
+
198
+ - [ ] `nolegend.activate()` called (lazy `_ensure_template()` pattern)
199
+ - [ ] No legend box (direct labels or single series)
200
+ - [ ] No gridlines, no in-figure title
201
+ - [ ] ≤ 5 colours, palette pinned across panels if shared
202
+ - [ ] Axes have units where non-obvious
203
+ - [ ] Right margin ≥ 80–110px when direct-labeling
204
+ - [ ] `customdata` carries enough info for click/select handlers
205
+ - [ ] `cliponaxis=False` on text-bearing traces so end labels render
206
+ - [ ] Test added in `tests/routes/` asserting the figure JSON
207
+ shape (e.g. trace name, customdata presence, no `<polyline>`)
@@ -0,0 +1,110 @@
1
+ ---
2
+ name: python-review
3
+ context: fork
4
+ description: Deep Python code quality review. Auto-invoke when finishing a task, before marking work complete, when the user asks to review code, or when preparing a PR. Focuses on design judgment, naming, performance, and test quality — things that ruff and ty cannot catch.
5
+ ---
6
+
7
+ # Deep Code Review
8
+
9
+ Review the changes for design quality — what automated tools miss. Report findings as 🔴 Must Fix, 🟡 Should Fix, 🟢 Suggestion.
10
+
11
+ First, confirm `uv run poe check` passes (ruff, ty, tests). Fix those before proceeding.
12
+
13
+ ---
14
+
15
+ ## 1. Function Design
16
+
17
+ Prefer pure functions: data in, data out, no side effects. When side effects are necessary (I/O, logging, state mutation), isolate them — push them to the edges, keep the core logic pure and testable.
18
+
19
+ - Functions that both compute and mutate → split into a pure computation and a thin side-effecting wrapper.
20
+ - Over ~30 lines → smell. Justify or split.
21
+ - Mixed abstraction levels (HTTP parsing interleaved with business logic).
22
+ - \>5 parameters → group into a dataclass or split.
23
+ - Boolean params make call sites unreadable (`process(data, True, False)`) → enum or separate functions.
24
+ - `@staticmethod` on a single-method class, or classes with only `__init__` + one method → just a function.
25
+ - Inheritance for code reuse where composition is simpler.
26
+
27
+ ## 2. Naming
28
+
29
+ - Single-letter variables outside comprehensions/lambdas. `d`, `x`, `r` — name the thing.
30
+ - Generic names that mean nothing: `data`, `result`, `info`, `item`, `obj`, `tmp`, `val`, `manager`, `handler`, `processor`, `helper`, `utils`. Name the *what*.
31
+ - Misleading: `users` that's a count, `get_` that mutates, `status` that's a bool.
32
+ - Inconsistent: `user_id` / `userId` / `uid` across functions.
33
+ - Unnecessary abbreviation. `cfg` is fine. `proc_dat_xfrm` is not.
34
+ - Negated booleans (`not_found`, `is_not_valid`) → invert the name.
35
+
36
+ ## 3. Comments
37
+
38
+ Comments exist for *why*, never *what*.
39
+
40
+ - Restating code (`# increment counter` above `counter += 1`) → delete.
41
+ - Commented-out code → it's in git, delete.
42
+ - TODOs without a ticket reference → link to tracker or delete.
43
+ - *Missing* why-comments on magic numbers, workarounds, performance hacks, non-obvious decisions.
44
+ - Docstrings that parrot the signature (`"""Gets the user."""` on `get_user()`) → explain behavior/constraints/edge cases or remove.
45
+ - Excessive inline comments on straightforward code → the code is too clever, simplify it.
46
+
47
+ ## 4. Error Handling Design
48
+
49
+ Strategy, not syntax (ruff handles syntax).
50
+
51
+ - `try` blocks wrapping 20 lines → wrap only what can throw.
52
+ - `except Exception as e: raise Exception(str(e))` → destroys traceback/type. `raise` or wrap in domain exception.
53
+ - `raise OtherError("failed")` discarding original → chain with `from e`.
54
+ - Error types that are `str` or bare `Exception` → domain-specific exceptions.
55
+ - `assert` for runtime validation in non-test code → stripped by `-O`, use `if`/`raise`.
56
+ - String-based error discrimination (`if "not found" in str(e)`) → exception types.
57
+ - Inconsistent strategy within a module: some functions return `None`, others raise. Pick one.
58
+
59
+ ## 5. Performance
60
+
61
+ Patterns that cause real problems, not micro-optimization.
62
+
63
+ - `in` on a `list` that should be a `set` (>~20 elements: O(n) vs O(1)).
64
+ - `+` string concatenation in a loop → `"".join()`.
65
+ - `await` in a loop → `asyncio.gather()` / `TaskGroup`.
66
+ - N+1: querying a DB or API per item instead of batching.
67
+ - `f.read()` on large files when line-by-line streaming works.
68
+ - Repeated computation inside a loop → hoist or `@cache`.
69
+ - Building a list only to immediately iterate it → use the iterator.
70
+ - Unnecessary `.copy()` / `deepcopy` → restructure ownership.
71
+
72
+ ## 6. Test Quality
73
+
74
+ - No assertions — calling code without checking results is not a test.
75
+ - Weak assertions: `assert result is not None` without checking actual value.
76
+ - Mocking 5 things → testing setup, not behavior. Restructure for testability.
77
+ - 3+ tests differing only in input/output → `@pytest.mark.parametrize`.
78
+ - Missing edge cases: empty input, error paths, boundary conditions.
79
+ - Useless names: `test_process_data` → `test_returns_error_on_empty_input`.
80
+ - 20 lines of setup for 2 lines of test → extract fixtures.
81
+
82
+ ## 7. Duplicated Logic
83
+
84
+ - Functions/blocks doing the same thing with minor variations → extract with concrete params.
85
+ - Watch for: HTTP handling, validation, serialization boilerplate, similar conditional chains.
86
+ - Copy-paste with slight modifications — the second copy will drift into a bug.
87
+
88
+ ## 8. Module Design
89
+
90
+ - God modules (>500 lines, mixed responsibilities) → suggest split.
91
+ - In-function imports for lazy loading must have a `# lazy:` comment. Uncommented → why?
92
+ - Side effects at import time (starting servers, DB connections, thread spawning at module level) → 🔴. Import must be inert.
93
+ - Public names (`no _` prefix) that aren't intended API.
94
+ - CLI entrypoints containing logic instead of parsing args and delegating.
95
+
96
+ ## 9. Dependencies
97
+
98
+ - Mixed HTTP clients (`requests` + `httpx`) → pick one.
99
+ - Mixed serialization (`json` + `orjson`/`msgspec`) → use what's in the dep tree.
100
+ - Vendored code that should be a dep, or trivial deps that could be a 10-line util.
101
+
102
+ ---
103
+
104
+ ## Output
105
+
106
+ Group by severity, then area. Each finding: **what** + **where** (file:line) + **why** + **concrete fix**.
107
+
108
+ End with: X must-fix, Y should-fix, Z suggestions.
109
+
110
+ $ARGUMENTS
@@ -0,0 +1,72 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ lint:
11
+ name: Lint
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ - uses: astral-sh/setup-uv@v5
16
+ - run: uv sync --frozen
17
+ - run: uv run ruff check .
18
+
19
+ format:
20
+ name: Format
21
+ runs-on: ubuntu-latest
22
+ steps:
23
+ - uses: actions/checkout@v4
24
+ - uses: astral-sh/setup-uv@v5
25
+ - run: uv sync --frozen
26
+ - run: uv run ruff format --check .
27
+
28
+ typecheck:
29
+ name: Type Check
30
+ runs-on: ubuntu-latest
31
+ steps:
32
+ - uses: actions/checkout@v4
33
+ - uses: astral-sh/setup-uv@v5
34
+ - run: uv sync --frozen
35
+ - run: uv run ty check
36
+
37
+ vulns:
38
+ name: Dependency Vulnerabilities
39
+ runs-on: ubuntu-latest
40
+ steps:
41
+ - uses: actions/checkout@v4
42
+ - uses: astral-sh/setup-uv@v5
43
+ - run: uv sync --frozen
44
+ - run: uvx pysentry-rs .
45
+ continue-on-error: true
46
+
47
+ dead-code:
48
+ name: Dead Code Detection
49
+ runs-on: ubuntu-latest
50
+ steps:
51
+ - uses: actions/checkout@v4
52
+ - uses: astral-sh/setup-uv@v5
53
+ - run: uv sync --frozen
54
+ - run: uv run vulture src
55
+
56
+ unused-deps:
57
+ name: Unused Dependencies
58
+ runs-on: ubuntu-latest
59
+ steps:
60
+ - uses: actions/checkout@v4
61
+ - uses: astral-sh/setup-uv@v5
62
+ - run: uv sync --frozen
63
+ - run: uv run deptry .
64
+
65
+ test:
66
+ name: Test
67
+ runs-on: ubuntu-latest
68
+ steps:
69
+ - uses: actions/checkout@v4
70
+ - uses: astral-sh/setup-uv@v5
71
+ - run: uv sync --frozen
72
+ - run: uv run pytest -q --tb=short
@@ -0,0 +1,36 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*'
7
+ workflow_dispatch:
8
+
9
+ jobs:
10
+ build:
11
+ name: Build distribution
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ - uses: astral-sh/setup-uv@v5
16
+ - run: uv build
17
+ - uses: actions/upload-artifact@v4
18
+ with:
19
+ name: dist
20
+ path: dist/
21
+
22
+ publish:
23
+ name: Publish to PyPI
24
+ needs: build
25
+ runs-on: ubuntu-latest
26
+ environment:
27
+ name: pypi
28
+ url: https://pypi.org/p/introspy
29
+ permissions:
30
+ id-token: write
31
+ steps:
32
+ - uses: actions/download-artifact@v4
33
+ with:
34
+ name: dist
35
+ path: dist/
36
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,19 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .eggs/
8
+ *.egg
9
+ .venv/
10
+ cf/
11
+ di/
12
+ venv/
13
+ .env
14
+ *.so
15
+ .coverage
16
+ htmlcov/
17
+ .pytest_cache/
18
+ .ruff_cache/
19
+ .mypy_cache/
@@ -0,0 +1,50 @@
1
+ # Introspect
2
+
3
+ Explore Claude Code conversation logs via CLI, web UI, MCP server.
4
+
5
+ ## Architecture
6
+
7
+ - `db.py` — DuckDB schema over `~/.claude/projects/**/*.jsonl`; materialized at server startup, lazy views as fallback
8
+ - `refresh.py` — background rebuild loop + window picker (`1`/`7`/`30`/`month`)
9
+ - `pricing.py` — model pricing as Python rates + DuckDB `CASE` SQL
10
+ - `sql_fragments.py` — shared SQL building blocks (cost / tool / file / command rollups)
11
+ - `projects.py` — git worktree-aware `cwd` → canonical project
12
+ - `search.py` — FTS via BM25, ILIKE fallback
13
+ - `api/routes.py` → `api/handlers/<name>.py` → `templates/<name>.html`
14
+ - `api/handlers/_helpers.py` — shared: `parent(request)`, `conn(request)`, pagination, sort allowlists; re-exports SQL fragments
15
+ - `mcp/` — FastMCP tools mounted on FastAPI; `refresh_bridge.py` plumbs `app.state` to stateless tool fns
16
+ - `cli.py` — Typer commands
17
+
18
+ ## Key Patterns
19
+
20
+ - **Adding a page**: handler in `handlers/`, route in `routes.py`, template, tests in `tests/routes/`
21
+ - **DB access**: `request.state.conn` (read-only, per-request), `json_extract()` for JSON fields, `# noqa: S608` for dynamic SQL
22
+ - **Pagination**: 1-based, fetch `size+1` to detect next page
23
+ - **HTMX**: `parent(request)` selects `base.html` (full) vs `partial.html` (fragment)
24
+ - **Charts**: build `plotly.graph_objects.Figure` server-side, style with `nolegend.activate()`, embed JSON for `Plotly.newPlot` (see `/python-review` skill `nolegend`)
25
+ - **Cost SQL**: reuse `SESSION_COST_SUBQUERY` / `session_cost_subquery_filtered()` from `sql_fragments.py` — never hand-roll cost math in handlers
26
+ - **Materialization**: `materialize_views()` runs on web startup and rebuilds derived tables (incl. `session_stats`, `assistant_message_costs`, `session_messages_enriched`); CLI commands call `ensure_materialized()` so they share the on-disk DB
27
+ - **Views/tables** (`db.py`): `raw_data`, `raw_messages`, `project_map`, `logical_sessions`, `assistant_message_costs`, `tool_calls`, `session_messages_enriched`, `conversation_turns`, `session_titles`, `message_commands`, `file_reads`, `file_writes`, `session_stats`, `search_corpus`, `materialize_meta`
28
+
29
+ ## Test Fixtures (`conftest.py`)
30
+
31
+ `make_user_message()`, `make_assistant_message()`, `write_jsonl()`, `glob_pattern()`. Route tests use `_patched_client()` context manager (defined in `tests/routes/conftest.py`).
32
+
33
+ ## Commands
34
+
35
+ - `uv run introspect query "SELECT ..."` — ad-hoc SQL against the views (use this to study real data; `introspect views` lists them)
36
+ - `uv run poe check` — run lint, typecheck, vulns, then tests
37
+ - `uv run poe fix` — auto-format and fix lint issues
38
+ - `uv run poe test` — run tests only
39
+ - `uv run poe check-all` — run all checks including dead-code and unused-deps
40
+
41
+ ## Stack
42
+
43
+ uv, ruff (lint/format), ty (type check), pytest, poethepoet (task runner)
44
+
45
+ ## Notes
46
+
47
+ - ty is in beta — may produce false positives. Prefer `# ty: ignore[rule]` over blanket suppression.
48
+ - Pre-commit hook auto-fixes and restages files. Only blocks on unfixable issues.
49
+ - All user-facing features must have tests. When adding new routes, template variables, query parameters, or UI functionality, add corresponding tests in `tests/routes/`.
50
+ - **IMPORTANT**: After completing any task, you MUST run the `/python-review` skill to review all changes. Apply all 🔴 Must Fix and 🟡 Should Fix findings before marking work as complete.
@@ -0,0 +1,14 @@
1
+ Metadata-Version: 2.4
2
+ Name: introspy
3
+ Version: 0.1.0
4
+ Summary: Explore and search Claude Code conversation logs
5
+ Requires-Python: >=3.11
6
+ Requires-Dist: duckdb>=1.2.0
7
+ Requires-Dist: fastapi>=0.115.0
8
+ Requires-Dist: jinja2>=3.1.0
9
+ Requires-Dist: mcp>=1.0.0
10
+ Requires-Dist: nolegend>=0.1.2
11
+ Requires-Dist: plotly>=5.0
12
+ Requires-Dist: rich>=13.0.0
13
+ Requires-Dist: typer>=0.15.0
14
+ Requires-Dist: uvicorn>=0.34.0
@@ -0,0 +1,97 @@
1
+ # Introspect
2
+
3
+ Explore and search your Claude Code conversation logs using SQL, full-text search, a web UI, or an MCP server.
4
+
5
+ ## Prerequisites
6
+
7
+ - Python 3.11+
8
+ - [uv](https://docs.astral.sh/uv/)
9
+
10
+ ## Installation
11
+
12
+ ```bash
13
+ uv sync
14
+ ```
15
+
16
+ Activate the project's virtual environment before running the `introspect`
17
+ commands below (or prefix each one with `uv run`):
18
+
19
+ ```bash
20
+ source .venv/bin/activate
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ ### Web UI
26
+
27
+ ```bash
28
+ introspect serve
29
+ # Runs on http://127.0.0.1:8000 by default
30
+ introspect serve --port 3000 --host 0.0.0.0
31
+ ```
32
+
33
+ ### CLI
34
+
35
+ ```bash
36
+ # List recent sessions
37
+ introspect sessions
38
+
39
+ # Show summary statistics
40
+ introspect stats
41
+
42
+ # Search conversation logs
43
+ introspect search "some query"
44
+
45
+ # Show tool call history
46
+ introspect tools
47
+ introspect tools --failed
48
+ introspect tools --name Bash
49
+
50
+ # Run an ad-hoc SQL query
51
+ introspect query "SELECT * FROM logical_sessions LIMIT 5"
52
+
53
+ # Rebuild the search index
54
+ introspect refresh
55
+ ```
56
+
57
+ ### MCP Server
58
+
59
+ ```bash
60
+ introspect mcp
61
+ ```
62
+
63
+ This starts an MCP server over stdio for integration with Claude Code.
64
+
65
+ Alternatively, the web server exposes the same MCP tools over HTTP at
66
+ `http://127.0.0.1:8000/mcp`. To launch a Claude Code session wired up to it:
67
+
68
+ ```bash
69
+ # In one terminal
70
+ introspect serve
71
+
72
+ # In another
73
+ uv run poe claude
74
+ ```
75
+
76
+ The `claude` poe task runs `claude --mcp-config .claude/mcp.json`, so the MCP
77
+ server is only registered for that session — no changes to your global Claude
78
+ Code config.
79
+
80
+ ## Development
81
+
82
+ ```bash
83
+ # Install dependencies (including dev tools)
84
+ uv sync
85
+
86
+ # Auto-format and fix lint issues
87
+ uv run poe fix
88
+
89
+ # Run lint, typecheck, security scan, and tests
90
+ uv run poe check
91
+
92
+ # Run tests only
93
+ uv run poe test
94
+
95
+ # Run all checks including dead-code and unused-deps
96
+ uv run poe check-all
97
+ ```