clawview 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.
Files changed (149) hide show
  1. clawview/__init__.py +0 -0
  2. clawview/__main__.py +5 -0
  3. clawview/app.py +140 -0
  4. clawview/ide.py +144 -0
  5. clawview/insights.py +1044 -0
  6. clawview/models.py +212 -0
  7. clawview/pricing.py +80 -0
  8. clawview/sessions.py +1734 -0
  9. clawview/stats.py +153 -0
  10. clawview/web/assets/InsightsPanel-XsanyCrx.js +36 -0
  11. clawview/web/assets/Tableau10-DYOhFoF_.js +1 -0
  12. clawview/web/assets/ar-SA-G6X2FPQ2-arIHp6y7.js +10 -0
  13. clawview/web/assets/arc-D0XJP_1w.js +1 -0
  14. clawview/web/assets/array-CPI_glx8.js +1 -0
  15. clawview/web/assets/az-AZ-76LH7QW2-B9S9q3QM.js +1 -0
  16. clawview/web/assets/band-BvgvOus1.js +1 -0
  17. clawview/web/assets/bg-BG-XCXSNQG7-B0DtsGcY.js +5 -0
  18. clawview/web/assets/blockDiagram-38ab4fdb-DddPqUt3.js +118 -0
  19. clawview/web/assets/bn-BD-2XOGV67Q-BOp1iUKN.js +5 -0
  20. clawview/web/assets/c4Diagram-3d4e48cf-BEtR2lrc.js +10 -0
  21. clawview/web/assets/ca-ES-6MX7JW3Y-TGrEO4jg.js +8 -0
  22. clawview/web/assets/channel-BQLZtiIk.js +1 -0
  23. clawview/web/assets/chunk-6U3AYISY-DTaCc_M3.js +12 -0
  24. clawview/web/assets/chunk-DseTPa7n.js +1 -0
  25. clawview/web/assets/chunk-EIO257PC-BgNTvmi4.js +22 -0
  26. clawview/web/assets/chunk-FX7ZIABN-zAkwZGjJ.js +35 -0
  27. clawview/web/assets/chunk-SQ5PDB2P-DHb1BAm-.js +7 -0
  28. clawview/web/assets/chunk-SRAX5OIU-DXkTQhLw.js +1 -0
  29. clawview/web/assets/chunk-Z3N5DIM6-Crgr0ERq.js +1 -0
  30. clawview/web/assets/classDiagram-70f12bd4-KCdG01OW.js +2 -0
  31. clawview/web/assets/classDiagram-v2-f2320105-Rx62xVFT.js +2 -0
  32. clawview/web/assets/clone-Bwsfh07r.js +1 -0
  33. clawview/web/assets/createText-2e5e7dd3-Cj375Ntc.js +7 -0
  34. clawview/web/assets/cs-CZ-2BRQDIVT-DrGxeLdy.js +11 -0
  35. clawview/web/assets/da-DK-5WZEPLOC-Bpu-r3Iv.js +5 -0
  36. clawview/web/assets/dagre-CVBS5NFI.js +1 -0
  37. clawview/web/assets/de-DE-XR44H4JA-GLhY2Ymr.js +8 -0
  38. clawview/web/assets/directory-open-01563666-DS7WJIfP.js +1 -0
  39. clawview/web/assets/directory-open-4ed118d0-d8uxSLmg.js +1 -0
  40. clawview/web/assets/dist-C9I7GKwI.js +1 -0
  41. clawview/web/assets/dist-CtElfPOo.js +7 -0
  42. clawview/web/assets/edges-e0da2a9e-CEkO_wx2.js +4 -0
  43. clawview/web/assets/el-GR-BZB4AONW-DcU9-C4i.js +10 -0
  44. clawview/web/assets/en-B4ZKOASM-B0JFK1jd.js +1 -0
  45. clawview/web/assets/erDiagram-9861fffd-BtkTd9nr.js +51 -0
  46. clawview/web/assets/es-ES-U4NZUMDT-DyufXalF.js +9 -0
  47. clawview/web/assets/eu-ES-A7QVB2H4-BjYdTz1f.js +11 -0
  48. clawview/web/assets/fa-IR-HGAKTJCU-B2j-XNlU.js +8 -0
  49. clawview/web/assets/fi-FI-Z5N7JZ37-DiKQ5AnI.js +6 -0
  50. clawview/web/assets/file-open-002ab408-Dvn3z30a.js +1 -0
  51. clawview/web/assets/file-open-7c801643-D8k8uIk5.js +1 -0
  52. clawview/web/assets/file-save-3189631c-DFENdDWg.js +1 -0
  53. clawview/web/assets/file-save-745eba88-CvN2mPEj.js +1 -0
  54. clawview/web/assets/flowDb-956e92f1-BRXT6BKy.js +10 -0
  55. clawview/web/assets/flowDiagram-66a62f08-Bl7lPH0M.js +4 -0
  56. clawview/web/assets/flowDiagram-v2-96b9c2cf-BxbAebjp.js +1 -0
  57. clawview/web/assets/flowchart-elk-definition-4a651766-Cib-VcrC.js +139 -0
  58. clawview/web/assets/fr-FR-RHASNOE6-C0gNrvQh.js +9 -0
  59. clawview/web/assets/ganttDiagram-c361ad54-CaAjUZ4Z.js +257 -0
  60. clawview/web/assets/gitGraphDiagram-72cf32ee-DgEpv1yR.js +70 -0
  61. clawview/web/assets/gl-ES-HMX3MZ6V-CVl7e3Yp.js +10 -0
  62. clawview/web/assets/graphlib-ByALzjAA.js +1 -0
  63. clawview/web/assets/he-IL-6SHJWFNN-BCBbdGZY.js +10 -0
  64. clawview/web/assets/hi-IN-IWLTKZ5I-CogRThUX.js +4 -0
  65. clawview/web/assets/hu-HU-A5ZG7DT2-B5ssBdl9.js +7 -0
  66. clawview/web/assets/id-ID-SAP4L64H-GwVSh0YI.js +10 -0
  67. clawview/web/assets/image-7KUKJ7J4-0bm1m09Z.js +1 -0
  68. clawview/web/assets/image-blob-reduce.esm-Bwolu1Yk.js +2 -0
  69. clawview/web/assets/index-3862675e-IUUc-T7r.js +1 -0
  70. clawview/web/assets/index-9IQHIY9t.css +2 -0
  71. clawview/web/assets/index-DMSYUwjY.js +61 -0
  72. clawview/web/assets/infoDiagram-f8f76790-QIlL2NYx.js +7 -0
  73. clawview/web/assets/init-BXY6xdg3.js +1 -0
  74. clawview/web/assets/it-IT-JPQ66NNP-DBEDRqCS.js +11 -0
  75. clawview/web/assets/ja-JP-DBVTYXUO-kE1CvFOC.js +8 -0
  76. clawview/web/assets/journeyDiagram-49397b02-CL29eFer.js +139 -0
  77. clawview/web/assets/jsx-runtime-B-G0DNmd.js +1 -0
  78. clawview/web/assets/kaa-6HZHGXH3-Dxf6RSvj.js +1 -0
  79. clawview/web/assets/kab-KAB-ZGHBKWFO-KdcpqtTg.js +8 -0
  80. clawview/web/assets/katex-DxfKw5bk.js +265 -0
  81. clawview/web/assets/kk-KZ-P5N5QNE5-D_UQhVok.js +1 -0
  82. clawview/web/assets/km-KH-HSX4SM5Z-D0SFzLhj.js +11 -0
  83. clawview/web/assets/ko-KR-MTYHY66A-CscmgsuS.js +9 -0
  84. clawview/web/assets/ku-TR-6OUDTVRD-DzYXt_-h.js +9 -0
  85. clawview/web/assets/line-CJPnuAmr.js +1 -0
  86. clawview/web/assets/linear-DzNjjHnD.js +1 -0
  87. clawview/web/assets/lt-LT-XHIRWOB4-BoqLc8my.js +3 -0
  88. clawview/web/assets/lv-LV-5QDEKY6T-BXMihRmq.js +7 -0
  89. clawview/web/assets/mermaid-b5860b54-DFGNN6kZ.js +89 -0
  90. clawview/web/assets/mindmap-definition-fc14e90a-DmSmJ5gU.js +415 -0
  91. clawview/web/assets/mr-IN-CRQNXWMA-Cht2SRn9.js +13 -0
  92. clawview/web/assets/my-MM-5M5IBNSE-BIf_PG2o.js +1 -0
  93. clawview/web/assets/nb-NO-T6EIAALU-D5FydAW6.js +10 -0
  94. clawview/web/assets/nl-NL-IS3SIHDZ-sbWnbazh.js +8 -0
  95. clawview/web/assets/nn-NO-6E72VCQL-DaA5Ng4z.js +8 -0
  96. clawview/web/assets/oc-FR-POXYY2M6-GIJFFa0g.js +8 -0
  97. clawview/web/assets/ordinal-DG-mcJZ_.js +1 -0
  98. clawview/web/assets/pa-IN-N4M65BXN-cdarwAvH.js +4 -0
  99. clawview/web/assets/path-CJP_60cg.js +1 -0
  100. clawview/web/assets/percentages-BXMCSKIN-Ddxggvgb.js +1 -0
  101. clawview/web/assets/pica-D00_uJyn.js +2 -0
  102. clawview/web/assets/pieDiagram-8a3498a8-C_Xy1L1P.js +35 -0
  103. clawview/web/assets/pl-PL-T2D74RX3-CBlAjM-a.js +9 -0
  104. clawview/web/assets/preload-helper-rov5CBGT.js +1 -0
  105. clawview/web/assets/prod-DsC-08Ys.js +149 -0
  106. clawview/web/assets/pt-BR-5N22H2LF-BVcpQZ6q.js +9 -0
  107. clawview/web/assets/pt-PT-UZXXM6DQ-B5v1EhyM.js +9 -0
  108. clawview/web/assets/quadrantDiagram-120e2f19-DNlYwwOD.js +7 -0
  109. clawview/web/assets/react-dom-yFv2xNpf.js +1 -0
  110. clawview/web/assets/requirementDiagram-deff3bca-DhuVcdo5.js +52 -0
  111. clawview/web/assets/ro-RO-JPDTUUEW-BEiz1UdL.js +11 -0
  112. clawview/web/assets/roundRect-BXf7JfZJ.js +1 -0
  113. clawview/web/assets/ru-RU-B4JR7IUQ-B41o3rg1.js +9 -0
  114. clawview/web/assets/sankeyDiagram-04a897e0-DwtfTQoP.js +8 -0
  115. clawview/web/assets/sequenceDiagram-704730f1-C-LO9nc3.js +122 -0
  116. clawview/web/assets/si-LK-N5RQ5JYF-wk9oo4_g.js +1 -0
  117. clawview/web/assets/sk-SK-C5VTKIMK-BKokcJeH.js +6 -0
  118. clawview/web/assets/sl-SI-NN7IZMDC-DRC4tvvu.js +6 -0
  119. clawview/web/assets/stateDiagram-587899a1-dEgNeBFL.js +1 -0
  120. clawview/web/assets/stateDiagram-v2-d93cdb3a-OiMRr12b.js +1 -0
  121. clawview/web/assets/step-CltBpEWI.js +1 -0
  122. clawview/web/assets/styles-6aaf32cf-yExJhkpA.js +207 -0
  123. clawview/web/assets/styles-9a916d00-0qGooOxP.js +160 -0
  124. clawview/web/assets/styles-c10674c1-BukgWV1G.js +116 -0
  125. clawview/web/assets/subset-shared.chunk-DnUq6rce.js +1 -0
  126. clawview/web/assets/subset-worker.chunk-Bx98g3iN.js +1 -0
  127. clawview/web/assets/sv-SE-XGPEYMSR-CMXGGiL0.js +10 -0
  128. clawview/web/assets/svgDrawCommon-08f97a94-ptfOfFLH.js +1 -0
  129. clawview/web/assets/ta-IN-2NMHFXQM-DQx8qTR7.js +9 -0
  130. clawview/web/assets/th-TH-HPSO5L25-DaUzuT7b.js +2 -0
  131. clawview/web/assets/time-DgeWXGcM.js +1 -0
  132. clawview/web/assets/timeline-definition-85554ec2-9SZ5X3yK.js +61 -0
  133. clawview/web/assets/tr-TR-DEFEU3FU-BYu0yuC4.js +7 -0
  134. clawview/web/assets/uk-UA-QMV73CPH--Hhr12a0.js +6 -0
  135. clawview/web/assets/vi-VN-M7AON7JQ-k5MWllYs.js +5 -0
  136. clawview/web/assets/with-selector-CF8hm-Bx.js +1 -0
  137. clawview/web/assets/xychartDiagram-e933f94c-uwNWTS5J.js +7 -0
  138. clawview/web/assets/zh-CN-LNUGB5OW-mt-jcU0T.js +10 -0
  139. clawview/web/assets/zh-HK-E62DVLB3-CqQFdXPt.js +1 -0
  140. clawview/web/assets/zh-TW-RAJ6MFWO-CGYv94Kb.js +9 -0
  141. clawview/web/favicon.svg +1 -0
  142. clawview/web/icons.svg +24 -0
  143. clawview/web/index.html +18 -0
  144. clawview/ws.py +208 -0
  145. clawview-0.1.0.dist-info/METADATA +123 -0
  146. clawview-0.1.0.dist-info/RECORD +149 -0
  147. clawview-0.1.0.dist-info/WHEEL +4 -0
  148. clawview-0.1.0.dist-info/entry_points.txt +2 -0
  149. clawview-0.1.0.dist-info/licenses/LICENSE +21 -0
clawview/__init__.py ADDED
File without changes
clawview/__main__.py ADDED
@@ -0,0 +1,5 @@
1
+ """Allow running with python -m clawview."""
2
+
3
+ from clawview.app import main
4
+
5
+ main()
clawview/app.py ADDED
@@ -0,0 +1,140 @@
1
+ """ClawView – real-time Claude Code session dashboard."""
2
+
3
+ import argparse
4
+ import asyncio
5
+ import os
6
+ from pathlib import Path
7
+
8
+ import uvicorn
9
+ from fastapi import FastAPI, Request, WebSocket
10
+ from fastapi.responses import FileResponse, JSONResponse
11
+ from fastapi.staticfiles import StaticFiles
12
+
13
+ from clawview.sessions import (
14
+ enrich_session_detail,
15
+ find_session_file,
16
+ load_memory_files,
17
+ load_skill_content,
18
+ parse_session_detail,
19
+ )
20
+ from clawview.ws import (
21
+ session_detail_websocket,
22
+ session_insights_websocket,
23
+ session_memory_websocket,
24
+ websocket_endpoint,
25
+ )
26
+
27
+ # Static files bundled inside the package (src/clawview/web/)
28
+ _DIST_DIR = Path(__file__).resolve().parent / "web"
29
+
30
+ app = FastAPI(title="ClawView")
31
+
32
+
33
+ def _setup_static_files() -> None:
34
+ """Mount static file serving if the dist directory exists."""
35
+ if not _DIST_DIR.is_dir():
36
+ return
37
+
38
+ # Serve /assets/ directly for hashed JS/CSS bundles
39
+ assets_dir = _DIST_DIR / "assets"
40
+ if assets_dir.is_dir():
41
+ app.mount("/assets", StaticFiles(directory=str(assets_dir)), name="assets")
42
+
43
+
44
+ _setup_static_files()
45
+
46
+
47
+ @app.websocket("/ws")
48
+ async def ws_route(ws: WebSocket) -> None:
49
+ await websocket_endpoint(ws)
50
+
51
+
52
+ @app.websocket("/ws/sessions/{session_id}")
53
+ async def ws_session_detail_route(ws: WebSocket, session_id: str) -> None:
54
+ await session_detail_websocket(ws, session_id)
55
+
56
+
57
+ @app.websocket("/ws/sessions/{session_id}/memory")
58
+ async def ws_session_memory_route(ws: WebSocket, session_id: str) -> None:
59
+ await session_memory_websocket(ws, session_id)
60
+
61
+
62
+ @app.websocket("/ws/insights/{session_id}")
63
+ async def ws_insights_route(ws: WebSocket, session_id: str) -> None:
64
+ await session_insights_websocket(ws, session_id)
65
+
66
+
67
+ @app.get("/api/sessions/{session_id}")
68
+ async def get_session_detail(session_id: str) -> JSONResponse:
69
+ """Return full session detail for a given session ID."""
70
+ fpath = await asyncio.to_thread(find_session_file, session_id)
71
+ if fpath is None:
72
+ return JSONResponse(
73
+ status_code=404,
74
+ content={"error": f"Session {session_id} not found"},
75
+ )
76
+
77
+ detail = await asyncio.to_thread(parse_session_detail, fpath)
78
+ if detail is None:
79
+ return JSONResponse(
80
+ status_code=500,
81
+ content={"error": f"Failed to parse session {session_id}"},
82
+ )
83
+
84
+ await asyncio.to_thread(enrich_session_detail, detail, fpath)
85
+
86
+ return JSONResponse(
87
+ content=detail.model_dump(by_alias=True, mode="json"),
88
+ )
89
+
90
+
91
+ @app.get("/api/sessions/{session_id}/memory")
92
+ async def get_session_memory(session_id: str) -> JSONResponse:
93
+ """Return memory files for the project that owns the given session."""
94
+ files = await asyncio.to_thread(load_memory_files, session_id)
95
+ return JSONResponse(
96
+ content=[f.model_dump(by_alias=True) for f in files],
97
+ )
98
+
99
+
100
+ @app.get("/api/sessions/{session_id}/skills/{skill_name:path}")
101
+ async def get_skill_content(session_id: str, skill_name: str) -> JSONResponse:
102
+ """Return the content of a skill file by name."""
103
+ result = await asyncio.to_thread(load_skill_content, session_id, skill_name)
104
+ if result is None:
105
+ return JSONResponse(
106
+ status_code=404,
107
+ content={"error": f"Skill '{skill_name}' not found"},
108
+ )
109
+ return JSONResponse(content={
110
+ "name": skill_name,
111
+ "content": result["content"],
112
+ "source": result["source"],
113
+ "path": result["path"],
114
+ })
115
+
116
+
117
+ @app.get("/{full_path:path}")
118
+ async def spa_fallback(request: Request, full_path: str) -> FileResponse:
119
+ """Serve static files or fall back to index.html for SPA routing."""
120
+ # Try to serve the exact file first (favicon.svg, icons.svg, etc.)
121
+ file_path = _DIST_DIR / full_path
122
+ if full_path and file_path.is_file():
123
+ return FileResponse(str(file_path))
124
+
125
+ # Fall back to index.html for all other paths (SPA routing)
126
+ return FileResponse(
127
+ str(_DIST_DIR / "index.html"),
128
+ headers={
129
+ "Cache-Control": "no-cache, no-store, must-revalidate",
130
+ "Pragma": "no-cache",
131
+ "Expires": "0",
132
+ },
133
+ )
134
+
135
+
136
+ def main() -> None:
137
+ parser = argparse.ArgumentParser(description="ClawView dashboard server")
138
+ parser.add_argument("--port", type=int, default=3333)
139
+ args = parser.parse_args()
140
+ uvicorn.run(app, host="0.0.0.0", port=args.port)
clawview/ide.py ADDED
@@ -0,0 +1,144 @@
1
+ """IDE / terminal detection — reads ~/.claude/ide/*.lock and inspects process ancestry."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import logging
7
+ import os
8
+ import subprocess
9
+ from pathlib import Path
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ # Known terminal emulators: lowercased substring -> display name.
14
+ _KNOWN_TERMINALS: dict[str, str] = {
15
+ "ghostty": "Ghostty",
16
+ "iterm2": "iTerm2",
17
+ "iterm": "iTerm2",
18
+ "alacritty": "Alacritty",
19
+ "kitty": "Kitty",
20
+ "wezterm": "WezTerm",
21
+ "hyper": "Hyper",
22
+ "warp": "Warp",
23
+ "terminal.app": "Terminal",
24
+ "apple_terminal": "Terminal",
25
+ "windowsterminal": "Windows Terminal",
26
+ "terminal": "Terminal",
27
+ }
28
+
29
+
30
+ def _is_pid_alive(pid: int) -> bool:
31
+ """Check whether a process with the given PID is still running."""
32
+ try:
33
+ os.kill(pid, 0)
34
+ return True
35
+ except (OSError, ProcessLookupError):
36
+ return False
37
+
38
+
39
+ def _build_process_table() -> tuple[dict[int, int], dict[int, str]]:
40
+ """Return (pid_to_ppid, pid_to_comm) maps from ``ps``."""
41
+ pid_to_ppid: dict[int, int] = {}
42
+ pid_to_comm: dict[int, str] = {}
43
+ try:
44
+ result = subprocess.run(
45
+ ["ps", "-o", "pid=,ppid=,comm=", "-ax"],
46
+ capture_output=True, text=True, timeout=5,
47
+ )
48
+ except (OSError, subprocess.TimeoutExpired):
49
+ return pid_to_ppid, pid_to_comm
50
+
51
+ for line in result.stdout.splitlines():
52
+ parts = line.split(None, 2)
53
+ if len(parts) >= 2:
54
+ try:
55
+ pid = int(parts[0])
56
+ ppid = int(parts[1])
57
+ except ValueError:
58
+ continue
59
+ pid_to_ppid[pid] = ppid
60
+ if len(parts) == 3:
61
+ pid_to_comm[pid] = parts[2]
62
+
63
+ return pid_to_ppid, pid_to_comm
64
+
65
+
66
+ def _get_ancestor_pids(pid: int, pid_to_ppid: dict[int, int]) -> list[int]:
67
+ """Return ancestor PIDs for *pid* (excluding pid 0/1), ordered from nearest to farthest."""
68
+ ancestors: list[int] = []
69
+ seen: set[int] = set()
70
+ current = pid
71
+ while current in pid_to_ppid:
72
+ parent = pid_to_ppid[current]
73
+ if parent <= 1 or parent in seen:
74
+ break
75
+ ancestors.append(parent)
76
+ seen.add(parent)
77
+ current = parent
78
+ return ancestors
79
+
80
+
81
+ def load_ide_pid_map(ide_dir: str) -> dict[int, str]:
82
+ """Read all .lock files in *ide_dir* and return ``{ide_pid: ide_name}``.
83
+
84
+ Lock files whose PID is no longer running are skipped (stale).
85
+ """
86
+ result: dict[int, str] = {}
87
+
88
+ ide_path = Path(ide_dir)
89
+ if not ide_path.is_dir():
90
+ return result
91
+
92
+ for entry in ide_path.iterdir():
93
+ if entry.is_dir() or entry.suffix != ".lock":
94
+ continue
95
+
96
+ try:
97
+ data = json.loads(entry.read_text())
98
+ except (OSError, json.JSONDecodeError) as exc:
99
+ logger.warning("Skipping malformed lock file %s: %s", entry, exc)
100
+ continue
101
+
102
+ ide_name: str = data.get("ideName", "")
103
+ if not ide_name:
104
+ continue
105
+
106
+ pid = data.get("pid")
107
+ if not isinstance(pid, int):
108
+ continue
109
+
110
+ if not _is_pid_alive(pid):
111
+ logger.debug("Skipping stale lock file %s (pid %d not running)", entry.name, pid)
112
+ continue
113
+
114
+ result[pid] = ide_name
115
+
116
+ return result
117
+
118
+
119
+ def resolve_client_for_pid(session_pid: int, ide_pid_map: dict[int, str]) -> str:
120
+ """Return the client name for *session_pid*.
121
+
122
+ First checks if the session is a descendant of a known IDE PID.
123
+ If not, walks the ancestor process names looking for a known terminal emulator.
124
+ Returns ``""`` if nothing matches.
125
+ """
126
+ pid_to_ppid, pid_to_comm = _build_process_table()
127
+ ancestors = _get_ancestor_pids(session_pid, pid_to_ppid)
128
+
129
+ # Check IDE ancestry first.
130
+ for ancestor_pid in ancestors:
131
+ if ancestor_pid in ide_pid_map:
132
+ return ide_pid_map[ancestor_pid]
133
+
134
+ # Fallback: detect terminal emulator from ancestor command names.
135
+ for ancestor_pid in ancestors:
136
+ comm = pid_to_comm.get(ancestor_pid, "")
137
+ if not comm:
138
+ continue
139
+ comm_lower = comm.lower()
140
+ for key, name in _KNOWN_TERMINALS.items():
141
+ if key in comm_lower:
142
+ return name
143
+
144
+ return ""