Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Changelog

All notable changes to this project are documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [Unreleased]

### Added

- `__version__` in `app.py` for release tracking (`0.1.0.dev0` until the first `v0.1.0` git tag)
- Startup guard refusing `--debug` with a non-loopback `--host` (including bracketed IPv6 loopback such as `[::1]`)
- [Deprecation policy](docs/deprecation-policy.md) for API and JSON field changes
- API field **stability** tables in `docs/api-reference.md` (stable / experimental / deprecated)

### Changed

- README notes that the server enforces the debug + host safety rule at startup

### Deprecated

- `export_count` on `GET /api/export/state` (documented only; still returned). Use `last_export_session_count`. Removal planned in a follow-up release per [deprecation policy](docs/deprecation-policy.md).

[Unreleased]: https://github.com/cppalliance/claude-code-chat-browser/compare/f70505982d435f8b1f754cb18c0c9f65609f11b4...HEAD
13 changes: 12 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,16 @@ Useful flags:

- `--base-dir PATH` — point at a different `projects/` tree (for tests or fixtures)
- `--exclude-rules PATH` — session exclusion rules file
- `--host 0.0.0.0` — listen on all interfaces (use only on trusted networks)
- `--host 0.0.0.0` — listen on all interfaces (use only on trusted networks; never with `--debug`)
- `--debug` — Flask/Werkzeug debug mode (loopback hosts only; enforced when starting via `python app.py`, not `flask run` or WSGI). Extending the guard to `FLASK_DEBUG` / `flask run` is a planned follow-up.

## API and release policy

- [CHANGELOG.md](CHANGELOG.md) — user-visible changes per release
- [docs/deprecation-policy.md](docs/deprecation-policy.md) — how deprecated API fields are removed
- [docs/api-reference.md](docs/api-reference.md) — field **stability** (`stable` / `experimental` / `deprecated`)

When changing JSON response shapes, update the API reference stability column and CHANGELOG before removing fields.

## Running tests

Expand Down Expand Up @@ -116,6 +125,8 @@ npm run test:coverage # optional
| SPA shell + routing | [`static/index.html`](static/index.html), [`static/js/app.js`](static/js/app.js) |
| Shared frontend utilities | [`static/js/shared/`](static/js/shared/) |
| API documentation | [`docs/api-reference.md`](docs/api-reference.md) |
| Deprecation policy | [`docs/deprecation-policy.md`](docs/deprecation-policy.md) |
| Changelog | [`CHANGELOG.md`](CHANGELOG.md) |

## Architecture

Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,16 @@ python app.py

Options:
```bash
python app.py --port 8080 --host 0.0.0.0
python app.py --port 8080 --host 0.0.0.0 # never add --debug on 0.0.0.0
python app.py --base-dir /path/to/claude/projects
```

> **Security warning:** Do not use `--host 0.0.0.0` together with `--debug` on untrusted networks.
> That combination exposes [Werkzeug's interactive debugger](https://werkzeug.palletsprojects.com/en/stable/debug/),
> which allows arbitrary code execution from any browser that can reach the server.
> For typical local browsing, keep the default `--host 127.0.0.1` and omit `--debug`.
> The server **refuses to start** if `--debug` is combined with a non-loopback `--host` (e.g. `0.0.0.0`).
> That check runs only when you start the app with **`python app.py`** (not via `flask run` or other WSGI entrypoints).

### CLI Export

Expand Down
58 changes: 57 additions & 1 deletion app.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Flask app that serves the web GUI for browsing sessions."""

__version__ = "0.1.0.dev0"

import argparse
import os
import sys
Expand All @@ -13,6 +15,59 @@
from utils.exclusion_rules import resolve_exclusion_rules_path, load_rules


def _normalize_bind_host(host: str) -> str:
"""Lowercase host for checks; strip optional IPv6 brackets (e.g. ``[::1]`` → ``::1``)."""
h = (host or "").strip().lower()
if len(h) >= 2 and h.startswith("[") and h.endswith("]"):
return h[1:-1]
return h


def is_loopback_host(host: str) -> bool:
"""True if ``host`` binds only to the local machine (safe with ``--debug``).

Accepts ``127.0.0.1``, ``localhost``, ``::1``, ``[::1]``, and other ``127.x.x.x`` addresses.
Rejects all-interfaces forms such as ``0.0.0.0`` and bare ``::`` (not loopback).
"""
h = _normalize_bind_host(host)
if h in ("127.0.0.1", "localhost", "::1"):
Comment thread
clean6378-max-it marked this conversation as resolved.
return True
if h.startswith("127.") and h.count(".") == 3:
parts = h.split(".")
try:
return all(0 <= int(p) <= 255 for p in parts)
except ValueError:
return False
return False
Comment thread
coderabbitai[bot] marked this conversation as resolved.


def format_listen_url(host: str, port: int) -> str:
"""Return a valid ``http://`` URL for the startup banner (IPv6 hosts bracketed)."""
h = (host or "").strip()
if not h:
raise ValueError("host must not be empty")
if h.startswith("[") and h.endswith("]"):
display_host = h
elif ":" in h:
display_host = f"[{h}]"
else:
display_host = h
return f"http://{display_host}:{port}"


def validate_startup_cli(args: argparse.Namespace) -> None:
"""Refuse ``--debug`` when ``--host`` is reachable off loopback."""
if args.debug and not is_loopback_host(args.host):
print(
"error: --debug is only allowed with a loopback --host "
"(127.0.0.1, localhost, ::1, [::1], or 127.x.x.x). "
"Combining --debug with a network-visible --host exposes the "
"Werkzeug debugger and session data to other machines.",
file=sys.stderr,
)
sys.exit(1)


def create_app(
base_dir: str | None = None,
exclusion_rules_path: str | None = None,
Expand Down Expand Up @@ -60,9 +115,10 @@ def build_cli_parser() -> argparse.ArgumentParser:

if __name__ == "__main__":
args = build_cli_parser().parse_args()
validate_startup_cli(args)
Comment thread
clean6378-max-it marked this conversation as resolved.

app = create_app(base_dir=args.base_dir, exclusion_rules_path=args.exclude_rules)
print(f"Claude Code Chat Browser running at http://{args.host}:{args.port}")
print(f"Claude Code Chat Browser running at {format_listen_url(args.host, args.port)}")
# Reloader follows --debug on Unix only (Werkzeug file watcher, not the interactive debugger).
app.run(
host=args.host,
Expand Down
126 changes: 71 additions & 55 deletions docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,22 @@ HTTP API for **claude-code-chat-browser**. All `/api/*` routes return JSON unles

**Source of truth for error codes:** [`api/error_codes.py`](../api/error_codes.py)

**Deprecation and removal:** See [`deprecation-policy.md`](deprecation-policy.md) for how stability labels are applied and fields are removed. Field labels are defined in [API field stability](#api-field-stability) below.

---

## API field stability

Each response field below is labeled:

| Label | Meaning |
|-------|---------|
| **stable** | Will not be renamed or removed without a documented deprecation period |
| **experimental** | May change in any release; do not build long-lived integrations on these fields |
| **deprecated** | Still returned; use the documented replacement; removal announced in [CHANGELOG](../CHANGELOG.md) |

**Migration:** Breaking changes use additive deprecation first (new field → deprecate old → remove after policy period). Versioned routes (e.g. `/api/v2/...`) are reserved for future breaking reshapes; none exist today.

---

## Authentication
Expand Down Expand Up @@ -100,13 +116,13 @@ None.

`application/json` — array of project objects:

| Field | Type | Description |
|-------|------|-------------|
| `name` | string | Directory name under `~/.claude/projects/` (e.g. `F--boost-capy`) |
| `path` | string | Absolute path to project directory |
| `display_name` | string | Friendly name derived from session `cwd` when available |
| `session_count` | integer | Count of titled sessions (updated in handler) |
| `last_modified` | string (ISO 8601) | Latest message timestamp across titled sessions |
| Field | Type | Stability | Description |
|-------|------|-----------|-------------|
| `name` | string | stable | Directory name under `~/.claude/projects/` (e.g. `F--boost-capy`) |
| `path` | string | stable | Absolute path to project directory |
| `display_name` | string | stable | Friendly name derived from session `cwd` when available |
| `session_count` | integer | stable | Count of titled sessions (updated in handler) |
| `last_modified` | string (ISO 8601) | stable | Latest message timestamp across titled sessions |

```json
[
Expand Down Expand Up @@ -148,19 +164,19 @@ Lists sessions in one project with summary fields for the workspace sidebar. Ski

`application/json` — array of session row objects:

| Field | Type | Description |
|-------|------|-------------|
| `id` | string | Session id (filename without `.jsonl`) |
| `path` | string | Absolute path to JSONL file |
| `size_bytes` | integer | File size |
| `modified` | number | File mtime (epoch seconds) |
| `title` | string | Parsed session title |
| `models` | string[] | Models used in session |
| `tokens` | integer | Sum of input + output tokens |
| `tool_calls` | integer | Total tool calls |
| `first_timestamp` | string \| null | First message timestamp |
| `last_timestamp` | string \| null | Last message timestamp |
| `error` | boolean | Optional; `true` if parse failed (card shows error state) |
| Field | Type | Stability | Description |
|-------|------|-----------|-------------|
| `id` | string | stable | Session id (filename without `.jsonl`) |
| `path` | string | stable | Absolute path to JSONL file |
| `size_bytes` | integer | stable | File size |
| `modified` | number | stable | File mtime (epoch seconds) |
| `title` | string | stable | Parsed session title |
| `models` | string[] | stable | Models used in session |
| `tokens` | integer | stable | Sum of input + output tokens |
| `tool_calls` | integer | stable | Total tool calls |
| `first_timestamp` | string \| null | stable | First message timestamp |
| `last_timestamp` | string \| null | stable | Last message timestamp |
| `error` | boolean | stable | Optional; `true` if parse failed (card shows error state) |

#### Errors

Expand Down Expand Up @@ -191,14 +207,14 @@ Returns the full parsed session: title, metadata, and messages (including tool c

`application/json` — session object:

| Top-level field | Type | Description |
|-----------------|------|-------------|
| `session_id` | string | Session identifier |
| `title` | string | Inferred title from first human message |
| `messages` | array | Ordered message objects (`role`, `text`/`content`, tool fields, etc.) |
| `metadata` | object | Tokens, models, timestamps, file activity, tool counts, `cwd`, `git_branch`, … |
| Top-level field | Type | Stability | Description |
|-----------------|------|-----------|-------------|
| `session_id` | string | stable | Session identifier |
| `title` | string | stable | Inferred title from first human message |
| `messages` | array | stable | Ordered message objects (`role`, `text`/`content`, tool fields, etc.) |
| `metadata` | object | stable | Tokens, models, timestamps, file activity, tool counts, `cwd`, `git_branch`, … |

See [`utils/jsonl_parser.py`](../utils/jsonl_parser.py) `parse_session()` for the full metadata shape.
Nested keys inside `messages[]` and `metadata` follow the parser output; new parser fields may appear as **experimental** until listed here. See [`utils/jsonl_parser.py`](../utils/jsonl_parser.py) `parse_session()` for the full metadata shape.

#### Errors

Expand Down Expand Up @@ -228,21 +244,21 @@ Same as session detail.

`application/json` — stats object from [`utils/session_stats.py`](../utils/session_stats.py) `compute_stats()`:

| Field | Type | Description |
|-------|------|-------------|
| `files_touched` | object | `read`, `written`, `created`, `total_unique` file lists |
| `commands_run` | array | Bash commands with exit metadata |
| `urls_accessed` | string[] | Web fetch URLs |
| `conversation_turns` | integer | Human/assistant turn count |
| `wall_clock_seconds` | number \| null | Session duration |
| `wall_clock_display` | string \| null | Human-readable duration |
| `cost_estimate_usd` | number | Best-effort USD estimate from token usage |
| `tool_result_summary` | object | Aggregated tool result stats |
| `stop_reason_summary` | object | Stop reason counts |
| `entry_type_counts` | object | JSONL entry type counts |
| `sidechain_message_count` | integer | Sidechain entries |
| `api_error_count` | integer | API errors in session |
| `compaction_events` | array | Context compaction markers |
| Field | Type | Stability | Description |
|-------|------|-----------|-------------|
| `files_touched` | object | stable | `read`, `written`, `created`, `total_unique` file lists |
| `commands_run` | array | stable | Bash commands with exit metadata |
| `urls_accessed` | string[] | stable | Web fetch URLs |
| `conversation_turns` | integer | stable | Human/assistant turn count |
| `wall_clock_seconds` | number \| null | stable | Session duration |
| `wall_clock_display` | string \| null | stable | Human-readable duration |
| `cost_estimate_usd` | number | stable | Best-effort USD estimate from token usage |
| `tool_result_summary` | object | stable | Aggregated tool result stats |
| `stop_reason_summary` | object | stable | Stop reason counts |
| `entry_type_counts` | object | stable | JSONL entry type counts |
| `sidechain_message_count` | integer | stable | Sidechain entries |
| `api_error_count` | integer | stable | API errors in session |
| `compaction_events` | array | stable | Context compaction markers |

#### Errors

Expand Down Expand Up @@ -276,14 +292,14 @@ Case-insensitive substring search across all non-excluded messages in all projec

`application/json` — array of hit objects:

| Field | Type | Description |
|-------|------|-------------|
| `project` | string | Project `name` |
| `session_id` | string | Session id |
| `title` | string | Session title |
| `role` | string | Message role (`human`, `assistant`, …) |
| `timestamp` | string \| null | Message timestamp |
| `snippet` | string | ~160 chars around match |
| Field | Type | Stability | Description |
|-------|------|-----------|-------------|
| `project` | string | stable | Project `name` |
| `session_id` | string | stable | Session id |
| `title` | string | stable | Session title |
| `role` | string | stable | Message role (`human`, `assistant`, …) |
| `timestamp` | string \| null | stable | Message timestamp |
| `snippet` | string | experimental | ~160 chars around match; length may change |

#### Errors

Expand All @@ -306,11 +322,11 @@ Read-only snapshot of bulk-export state persisted under `~/.claude-code-chat-bro

#### Response — `200 OK`

| Field | Type | Description |
|-------|------|-------------|
| `last_export_time` | string \| null | ISO timestamp of last completed bulk export |
| `last_export_session_count` | integer | Sessions in last bulk export run |
| `export_count` | integer | **Legacy alias** — same value as `last_export_session_count`; prefer `last_export_session_count` in new integrations (kept for SPA backwards compatibility) |
| Field | Type | Stability | Description |
|-------|------|-----------|-------------|
| `last_export_time` | string \| null | stable | ISO timestamp of last completed bulk export |
| `last_export_session_count` | integer | stable | Sessions in last bulk export run |
| `export_count` | integer | deprecated | Legacy alias of `last_export_session_count`; prefer `last_export_session_count` in new code (still returned for SPA compatibility; removal per [deprecation-policy.md](deprecation-policy.md)) |

```json
{
Expand Down
46 changes: 46 additions & 0 deletions docs/deprecation-policy.md
Comment thread
clean6378-max-it marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Deprecation policy

This document defines how **claude-code-chat-browser** evolves its HTTP JSON API and CLI without breaking integrators and the bundled SPA unexpectedly.

## Principles

1. **Documented fields are a contract.** See [API reference](api-reference.md) — each field is marked `stable`, `experimental`, or `deprecated`.
2. **Additive first.** Prefer adding a new field over renaming an existing one.
3. **Deprecate before removing.** A deprecated field remains in responses for at least **one release** after the deprecation is announced in [CHANGELOG](../CHANGELOG.md) and the API reference. Fields still read by the bundled SPA need **at least two releases** — see [Removal criteria](#removal-criteria) below.
4. **SPA and scripts.** Update `static/js/*.js` and any internal callers before removing a field.

## How we announce deprecation

| Channel | What to update |
|---------|----------------|
| CHANGELOG | `### Deprecated` under the release that announces the change |
| API reference | Set field stability to `deprecated` with a short note and replacement |
| Response (optional) | Future: `Deprecation` header or JSON `_deprecated` map — not required today |

## Removal criteria

A deprecated field may be removed when:

- At least one release has shipped with the field still present but documented as deprecated, and
- The bundled SPA no longer reads the field, and
- Tests and CHANGELOG document the removal.

For fields actively read by the bundled SPA (which does not track an external API version), removal happens no earlier than **two tagged releases** after the release that documented the deprecation in CHANGELOG (for example, deprecated in `0.1.0`, removable from `0.3.0` at earliest when versions advance `0.1.0` → `0.2.0` → `0.3.0`), and no earlier than **14 calendar days** after that deprecation announcement.

## Example (in progress)

| Field | Endpoint | Status | Replacement |
|-------|----------|--------|-------------|
| `export_count` | `GET /api/export/state` | deprecated | `last_export_session_count` |

## Versioning

Release versions follow `MAJOR.MINOR.PATCH` in `app.__version__` and [CHANGELOG](../CHANGELOG.md). Until the first git tag ships, `main` may carry a `.dev0` suffix (for example `0.1.0.dev0`); the CHANGELOG `[Unreleased]` section is the source of truth for what is not yet tagged.

| Bump | Pre-1.0 meaning |
|------|-----------------|
| **Patch** | Bug fixes and documentation; no intentional API removals |
| **Minor** | Additive API/features; deprecations may be announced |
| **Major** | Reserved for the `1.0.0` line and later: signals a stable HTTP JSON contract for external integrators and may include breaking removals that completed their deprecation period |

While the project is pre-1.0, treat **minor** bumps as the usual vehicle for deprecations and **patch** bumps for safe fixes.
Loading
Loading