clicodelog 0.2.0__tar.gz → 0.2.1__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.
- {clicodelog-0.2.0 → clicodelog-0.2.1}/PKG-INFO +14 -8
- {clicodelog-0.2.0 → clicodelog-0.2.1}/README.md +10 -5
- {clicodelog-0.2.0 → clicodelog-0.2.1}/clicodelog/__init__.py +1 -1
- {clicodelog-0.2.0 → clicodelog-0.2.1}/clicodelog/app.py +135 -83
- {clicodelog-0.2.0 → clicodelog-0.2.1}/clicodelog/templates/index.html +333 -4
- {clicodelog-0.2.0 → clicodelog-0.2.1}/clicodelog.egg-info/PKG-INFO +14 -8
- clicodelog-0.2.1/clicodelog.egg-info/requires.txt +8 -0
- {clicodelog-0.2.0 → clicodelog-0.2.1}/pyproject.toml +4 -3
- clicodelog-0.2.0/clicodelog.egg-info/requires.txt +0 -7
- {clicodelog-0.2.0 → clicodelog-0.2.1}/LICENSE +0 -0
- {clicodelog-0.2.0 → clicodelog-0.2.1}/clicodelog/__main__.py +0 -0
- {clicodelog-0.2.0 → clicodelog-0.2.1}/clicodelog/cli.py +0 -0
- {clicodelog-0.2.0 → clicodelog-0.2.1}/clicodelog.egg-info/SOURCES.txt +0 -0
- {clicodelog-0.2.0 → clicodelog-0.2.1}/clicodelog.egg-info/dependency_links.txt +0 -0
- {clicodelog-0.2.0 → clicodelog-0.2.1}/clicodelog.egg-info/entry_points.txt +0 -0
- {clicodelog-0.2.0 → clicodelog-0.2.1}/clicodelog.egg-info/top_level.txt +0 -0
- {clicodelog-0.2.0 → clicodelog-0.2.1}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: clicodelog
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: A lightweight, local-first web app to browse, inspect, and export logs from CLI-based AI coding agents
|
|
5
5
|
Author: monk1337
|
|
6
6
|
License-Expression: MIT
|
|
@@ -25,8 +25,9 @@ Classifier: Topic :: Utilities
|
|
|
25
25
|
Requires-Python: >=3.7
|
|
26
26
|
Description-Content-Type: text/markdown
|
|
27
27
|
License-File: LICENSE
|
|
28
|
-
Requires-Dist:
|
|
29
|
-
Requires-Dist:
|
|
28
|
+
Requires-Dist: fastapi>=0.104.0
|
|
29
|
+
Requires-Dist: uvicorn[standard]>=0.24.0
|
|
30
|
+
Requires-Dist: jinja2>=3.1.0
|
|
30
31
|
Provides-Extra: dev
|
|
31
32
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
32
33
|
Requires-Dist: build; extra == "dev"
|
|
@@ -53,7 +54,7 @@ CLI-based AI coding agents — Claude Code, OpenAI Codex, and Gemini CLI.
|
|
|
53
54
|
|
|
54
55
|
<p>
|
|
55
56
|
<img src="https://img.shields.io/badge/Python-3.7+-blue.svg" alt="Python 3.7+" />
|
|
56
|
-
<img src="https://img.shields.io/badge/
|
|
57
|
+
<img src="https://img.shields.io/badge/FastAPI-0.104+-00c7b7.svg" alt="FastAPI" />
|
|
57
58
|
<img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License MIT" />
|
|
58
59
|
<a href="http://makeapullrequest.com">
|
|
59
60
|
<img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square" alt="PRs Welcome" />
|
|
@@ -84,7 +85,7 @@ CLI-based AI coding agents — Claude Code, OpenAI Codex, and Gemini CLI.
|
|
|
84
85
|
|
|
85
86
|
## Installation
|
|
86
87
|
|
|
87
|
-
### Via uv (Recommended)
|
|
88
|
+
### Via uv tool (Recommended)
|
|
88
89
|
|
|
89
90
|
[uv](https://github.com/astral-sh/uv) is a fast Python package installer. Install it first if you haven't:
|
|
90
91
|
|
|
@@ -92,10 +93,15 @@ CLI-based AI coding agents — Claude Code, OpenAI Codex, and Gemini CLI.
|
|
|
92
93
|
curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
93
94
|
```
|
|
94
95
|
|
|
95
|
-
Then install clicodelog:
|
|
96
|
+
Then install clicodelog as an isolated tool:
|
|
96
97
|
|
|
97
98
|
```bash
|
|
98
|
-
uv
|
|
99
|
+
uv tool install clicodelog
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
To upgrade:
|
|
103
|
+
```bash
|
|
104
|
+
uv tool upgrade clicodelog
|
|
99
105
|
```
|
|
100
106
|
|
|
101
107
|
### Via pip
|
|
@@ -109,7 +115,7 @@ pip install clicodelog
|
|
|
109
115
|
```bash
|
|
110
116
|
git clone https://github.com/monk1337/clicodelog.git
|
|
111
117
|
cd clicodelog
|
|
112
|
-
uv
|
|
118
|
+
uv tool install -e .
|
|
113
119
|
# or with pip:
|
|
114
120
|
pip install -e .
|
|
115
121
|
```
|
|
@@ -18,7 +18,7 @@ CLI-based AI coding agents — Claude Code, OpenAI Codex, and Gemini CLI.
|
|
|
18
18
|
|
|
19
19
|
<p>
|
|
20
20
|
<img src="https://img.shields.io/badge/Python-3.7+-blue.svg" alt="Python 3.7+" />
|
|
21
|
-
<img src="https://img.shields.io/badge/
|
|
21
|
+
<img src="https://img.shields.io/badge/FastAPI-0.104+-00c7b7.svg" alt="FastAPI" />
|
|
22
22
|
<img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License MIT" />
|
|
23
23
|
<a href="http://makeapullrequest.com">
|
|
24
24
|
<img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square" alt="PRs Welcome" />
|
|
@@ -49,7 +49,7 @@ CLI-based AI coding agents — Claude Code, OpenAI Codex, and Gemini CLI.
|
|
|
49
49
|
|
|
50
50
|
## Installation
|
|
51
51
|
|
|
52
|
-
### Via uv (Recommended)
|
|
52
|
+
### Via uv tool (Recommended)
|
|
53
53
|
|
|
54
54
|
[uv](https://github.com/astral-sh/uv) is a fast Python package installer. Install it first if you haven't:
|
|
55
55
|
|
|
@@ -57,10 +57,15 @@ CLI-based AI coding agents — Claude Code, OpenAI Codex, and Gemini CLI.
|
|
|
57
57
|
curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
-
Then install clicodelog:
|
|
60
|
+
Then install clicodelog as an isolated tool:
|
|
61
61
|
|
|
62
62
|
```bash
|
|
63
|
-
uv
|
|
63
|
+
uv tool install clicodelog
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
To upgrade:
|
|
67
|
+
```bash
|
|
68
|
+
uv tool upgrade clicodelog
|
|
64
69
|
```
|
|
65
70
|
|
|
66
71
|
### Via pip
|
|
@@ -74,7 +79,7 @@ pip install clicodelog
|
|
|
74
79
|
```bash
|
|
75
80
|
git clone https://github.com/monk1337/clicodelog.git
|
|
76
81
|
cd clicodelog
|
|
77
|
-
uv
|
|
82
|
+
uv tool install -e .
|
|
78
83
|
# or with pip:
|
|
79
84
|
pip install -e .
|
|
80
85
|
```
|
|
@@ -13,10 +13,17 @@ import signal
|
|
|
13
13
|
import subprocess
|
|
14
14
|
import threading
|
|
15
15
|
import time
|
|
16
|
+
import webbrowser
|
|
16
17
|
from datetime import datetime
|
|
17
18
|
from pathlib import Path
|
|
18
|
-
from
|
|
19
|
-
|
|
19
|
+
from typing import Optional
|
|
20
|
+
|
|
21
|
+
import uvicorn
|
|
22
|
+
from fastapi import FastAPI, Request
|
|
23
|
+
from fastapi.responses import HTMLResponse, JSONResponse, Response
|
|
24
|
+
from fastapi.staticfiles import StaticFiles
|
|
25
|
+
from fastapi.templating import Jinja2Templates
|
|
26
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
20
27
|
|
|
21
28
|
# Package directory for templates
|
|
22
29
|
PACKAGE_DIR = Path(__file__).parent
|
|
@@ -717,18 +724,29 @@ def parse_gemini_conversation(session_file, session_id):
|
|
|
717
724
|
}
|
|
718
725
|
|
|
719
726
|
|
|
720
|
-
# Create
|
|
721
|
-
app =
|
|
722
|
-
|
|
727
|
+
# Create FastAPI app
|
|
728
|
+
app = FastAPI(title="CLI Code Log", version="0.2.0")
|
|
729
|
+
|
|
730
|
+
# Add CORS middleware
|
|
731
|
+
app.add_middleware(
|
|
732
|
+
CORSMiddleware,
|
|
733
|
+
allow_origins=["*"],
|
|
734
|
+
allow_credentials=True,
|
|
735
|
+
allow_methods=["*"],
|
|
736
|
+
allow_headers=["*"],
|
|
737
|
+
)
|
|
723
738
|
|
|
739
|
+
# Templates
|
|
740
|
+
templates = Jinja2Templates(directory=str(PACKAGE_DIR / "templates"))
|
|
724
741
|
|
|
725
|
-
@app.route('/')
|
|
726
|
-
def index():
|
|
727
|
-
return render_template('index.html')
|
|
728
742
|
|
|
743
|
+
@app.get("/", response_class=HTMLResponse)
|
|
744
|
+
async def index(request: Request):
|
|
745
|
+
return templates.TemplateResponse("index.html", {"request": request})
|
|
729
746
|
|
|
730
|
-
|
|
731
|
-
|
|
747
|
+
|
|
748
|
+
@app.get("/api/sources")
|
|
749
|
+
async def api_sources():
|
|
732
750
|
"""Get available sources."""
|
|
733
751
|
sources = []
|
|
734
752
|
for source_id, config in SOURCES.items():
|
|
@@ -737,55 +755,55 @@ def api_sources():
|
|
|
737
755
|
"name": config["name"],
|
|
738
756
|
"available": config["source_dir"].exists()
|
|
739
757
|
})
|
|
740
|
-
return
|
|
758
|
+
return {
|
|
741
759
|
"sources": sources,
|
|
742
760
|
"current": current_source
|
|
743
|
-
}
|
|
761
|
+
}
|
|
744
762
|
|
|
745
763
|
|
|
746
|
-
@app.
|
|
747
|
-
def api_set_source(source_id):
|
|
764
|
+
@app.post("/api/sources/{source_id}")
|
|
765
|
+
async def api_set_source(source_id: str):
|
|
748
766
|
"""Set the current source."""
|
|
749
767
|
global current_source
|
|
750
768
|
if source_id not in SOURCES:
|
|
751
|
-
return
|
|
769
|
+
return JSONResponse({"error": "Unknown source"}, status_code=400)
|
|
752
770
|
current_source = source_id
|
|
753
|
-
return
|
|
771
|
+
return {"status": "success", "current": current_source}
|
|
754
772
|
|
|
755
773
|
|
|
756
|
-
@app.
|
|
757
|
-
def api_projects():
|
|
758
|
-
source_id =
|
|
759
|
-
return
|
|
774
|
+
@app.get("/api/projects")
|
|
775
|
+
async def api_projects(source: Optional[str] = None):
|
|
776
|
+
source_id = source or current_source
|
|
777
|
+
return get_projects(source_id)
|
|
760
778
|
|
|
761
779
|
|
|
762
|
-
@app.
|
|
763
|
-
def api_sessions(project_id):
|
|
764
|
-
source_id =
|
|
765
|
-
return
|
|
780
|
+
@app.get("/api/projects/{project_id}/sessions")
|
|
781
|
+
async def api_sessions(project_id: str, source: Optional[str] = None):
|
|
782
|
+
source_id = source or current_source
|
|
783
|
+
return get_sessions(project_id, source_id)
|
|
766
784
|
|
|
767
785
|
|
|
768
|
-
@app.
|
|
769
|
-
def api_conversation(project_id, session_id):
|
|
770
|
-
source_id =
|
|
771
|
-
return
|
|
786
|
+
@app.get("/api/projects/{project_id}/sessions/{session_id}")
|
|
787
|
+
async def api_conversation(project_id: str, session_id: str, source: Optional[str] = None):
|
|
788
|
+
source_id = source or current_source
|
|
789
|
+
return get_conversation(project_id, session_id, source_id)
|
|
772
790
|
|
|
773
791
|
|
|
774
|
-
@app.
|
|
775
|
-
def api_search():
|
|
792
|
+
@app.get("/api/search")
|
|
793
|
+
async def api_search(q: Optional[str] = None, source: Optional[str] = None):
|
|
776
794
|
"""Search across all conversations."""
|
|
777
|
-
query =
|
|
778
|
-
source_id =
|
|
795
|
+
query = (q or '').lower()
|
|
796
|
+
source_id = source or current_source
|
|
779
797
|
|
|
780
798
|
if not query:
|
|
781
|
-
return
|
|
799
|
+
return []
|
|
782
800
|
|
|
783
801
|
if source_id not in SOURCES:
|
|
784
|
-
return
|
|
802
|
+
return []
|
|
785
803
|
|
|
786
804
|
data_dir = DATA_DIR / SOURCES[source_id]["data_subdir"]
|
|
787
805
|
if not data_dir.exists():
|
|
788
|
-
return
|
|
806
|
+
return []
|
|
789
807
|
|
|
790
808
|
results = []
|
|
791
809
|
|
|
@@ -838,17 +856,17 @@ def api_search():
|
|
|
838
856
|
except Exception:
|
|
839
857
|
continue
|
|
840
858
|
|
|
841
|
-
return
|
|
859
|
+
return results[:50] # Limit results
|
|
842
860
|
|
|
843
861
|
|
|
844
|
-
@app.
|
|
845
|
-
def api_export(project_id, session_id):
|
|
862
|
+
@app.get("/api/projects/{project_id}/sessions/{session_id}/export")
|
|
863
|
+
async def api_export(project_id: str, session_id: str, source: Optional[str] = None):
|
|
846
864
|
"""Export conversation as text file."""
|
|
847
|
-
source_id =
|
|
865
|
+
source_id = source or current_source
|
|
848
866
|
conversation = get_conversation(project_id, session_id, source_id)
|
|
849
867
|
|
|
850
868
|
if "error" in conversation:
|
|
851
|
-
return
|
|
869
|
+
return JSONResponse(conversation, status_code=404)
|
|
852
870
|
|
|
853
871
|
# Build text content
|
|
854
872
|
lines = []
|
|
@@ -912,80 +930,101 @@ def api_export(project_id, session_id):
|
|
|
912
930
|
|
|
913
931
|
text_content = "\n".join(lines)
|
|
914
932
|
|
|
915
|
-
from flask import Response
|
|
916
933
|
return Response(
|
|
917
|
-
text_content,
|
|
918
|
-
|
|
934
|
+
content=text_content,
|
|
935
|
+
media_type="text/plain",
|
|
919
936
|
headers={"Content-Disposition": f"attachment; filename={session_id}.txt"}
|
|
920
937
|
)
|
|
921
938
|
|
|
922
939
|
|
|
923
|
-
@app.
|
|
924
|
-
def api_sync():
|
|
940
|
+
@app.post("/api/sync")
|
|
941
|
+
async def api_sync(source: Optional[str] = None):
|
|
925
942
|
"""Manually trigger a data sync."""
|
|
926
|
-
source_id =
|
|
943
|
+
source_id = source or current_source
|
|
927
944
|
try:
|
|
928
945
|
sync_data(source_id=source_id, silent=True)
|
|
929
|
-
return
|
|
946
|
+
return {
|
|
930
947
|
"status": "success",
|
|
931
948
|
"source": source_id,
|
|
932
949
|
"last_sync": last_sync_time.get(source_id).isoformat() if last_sync_time.get(source_id) else None
|
|
933
|
-
}
|
|
950
|
+
}
|
|
934
951
|
except Exception as e:
|
|
935
|
-
return
|
|
952
|
+
return JSONResponse({"status": "error", "message": str(e)}, status_code=500)
|
|
936
953
|
|
|
937
954
|
|
|
938
|
-
@app.
|
|
939
|
-
def api_status():
|
|
955
|
+
@app.get("/api/status")
|
|
956
|
+
async def api_status(source: Optional[str] = None):
|
|
940
957
|
"""Get sync status."""
|
|
941
|
-
source_id =
|
|
958
|
+
source_id = source or current_source
|
|
942
959
|
source_config = SOURCES.get(source_id, {})
|
|
943
960
|
data_dir = DATA_DIR / source_config.get("data_subdir", "")
|
|
944
961
|
|
|
945
|
-
return
|
|
962
|
+
return {
|
|
946
963
|
"source": source_id,
|
|
947
964
|
"last_sync": last_sync_time.get(source_id).isoformat() if last_sync_time.get(source_id) else None,
|
|
948
965
|
"sync_interval_hours": SYNC_INTERVAL / 3600,
|
|
949
966
|
"data_dir": str(data_dir)
|
|
950
|
-
}
|
|
967
|
+
}
|
|
951
968
|
|
|
952
969
|
|
|
953
|
-
def kill_process_on_port(port):
|
|
970
|
+
def kill_process_on_port(port, max_retries=3):
|
|
954
971
|
"""Kill any process running on the specified port."""
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
972
|
+
for attempt in range(max_retries):
|
|
973
|
+
try:
|
|
974
|
+
# Find process ID on the port
|
|
975
|
+
result = subprocess.run(
|
|
976
|
+
["lsof", "-ti", f":{port}"],
|
|
977
|
+
capture_output=True,
|
|
978
|
+
text=True
|
|
979
|
+
)
|
|
980
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
981
|
+
pids = result.stdout.strip().split('\n')
|
|
982
|
+
for pid in pids:
|
|
983
|
+
print(f"⚠️ Port {port} is in use by process {pid}")
|
|
984
|
+
print(f"🔄 Killing process {pid}...")
|
|
985
|
+
try:
|
|
986
|
+
os.kill(int(pid), signal.SIGKILL) # Use SIGKILL for immediate termination
|
|
987
|
+
except ProcessLookupError:
|
|
988
|
+
pass # Process already gone
|
|
989
|
+
except Exception as e:
|
|
990
|
+
print(f"Warning: Could not kill process: {e}")
|
|
991
|
+
|
|
992
|
+
# Wait longer and verify
|
|
993
|
+
time.sleep(1.5)
|
|
994
|
+
|
|
995
|
+
# Check if port is now free
|
|
996
|
+
check = subprocess.run(
|
|
997
|
+
["lsof", "-ti", f":{port}"],
|
|
998
|
+
capture_output=True,
|
|
999
|
+
text=True
|
|
1000
|
+
)
|
|
1001
|
+
if check.returncode != 0 or not check.stdout.strip():
|
|
1002
|
+
print(f"✓ Port {port} is now free")
|
|
1003
|
+
return True
|
|
1004
|
+
else:
|
|
1005
|
+
return True # Port is already free
|
|
1006
|
+
except FileNotFoundError:
|
|
1007
|
+
# lsof not available (not on Unix-like system)
|
|
1008
|
+
return True
|
|
1009
|
+
except Exception as e:
|
|
1010
|
+
print(f"Warning: Could not check port: {e}")
|
|
1011
|
+
return False
|
|
1012
|
+
|
|
1013
|
+
print(f"❌ Failed to free port {port} after {max_retries} attempts")
|
|
1014
|
+
return False
|
|
979
1015
|
|
|
980
1016
|
|
|
981
1017
|
def run_server(host="127.0.0.1", port=6126, skip_sync=False, debug=False):
|
|
982
|
-
"""Run the
|
|
1018
|
+
"""Run the FastAPI server with uvicorn."""
|
|
983
1019
|
print("=" * 60)
|
|
984
1020
|
print("CLI Code Log - AI Conversation History Viewer")
|
|
985
1021
|
print("=" * 60)
|
|
986
1022
|
|
|
987
1023
|
# Kill any process on the port
|
|
988
|
-
kill_process_on_port(port)
|
|
1024
|
+
if not kill_process_on_port(port):
|
|
1025
|
+
print(f"\n❌ Could not free port {port}. Please manually stop the process or use a different port.")
|
|
1026
|
+
print(f" Try: lsof -ti:{port} | xargs kill -9")
|
|
1027
|
+
return
|
|
989
1028
|
|
|
990
1029
|
if not skip_sync:
|
|
991
1030
|
# Sync data from all sources
|
|
@@ -1008,10 +1047,23 @@ def run_server(host="127.0.0.1", port=6126, skip_sync=False, debug=False):
|
|
|
1008
1047
|
sync_thread.start()
|
|
1009
1048
|
print("Background sync thread started.")
|
|
1010
1049
|
|
|
1050
|
+
url = f"http://{host}:{port}"
|
|
1011
1051
|
print(f"\nStarting server...")
|
|
1012
|
-
print(f"
|
|
1052
|
+
print(f"🌐 Opening {url} in your browser...")
|
|
1013
1053
|
print("=" * 60)
|
|
1014
|
-
|
|
1054
|
+
|
|
1055
|
+
# Open browser after a short delay
|
|
1056
|
+
def open_browser():
|
|
1057
|
+
time.sleep(1.5)
|
|
1058
|
+
try:
|
|
1059
|
+
webbrowser.open(url)
|
|
1060
|
+
except Exception as e:
|
|
1061
|
+
print(f"Could not open browser automatically: {e}")
|
|
1062
|
+
|
|
1063
|
+
browser_thread = threading.Thread(target=open_browser, daemon=True)
|
|
1064
|
+
browser_thread.start()
|
|
1065
|
+
|
|
1066
|
+
uvicorn.run(app, host=host, port=port, log_level="warning" if not debug else "info")
|
|
1015
1067
|
|
|
1016
1068
|
|
|
1017
1069
|
if __name__ == '__main__':
|
|
@@ -62,7 +62,8 @@
|
|
|
62
62
|
.app {
|
|
63
63
|
display: grid;
|
|
64
64
|
grid-template-columns: 280px 320px 1fr;
|
|
65
|
-
height: 100vh;
|
|
65
|
+
height: calc(100vh - 60px);
|
|
66
|
+
margin-top: 60px;
|
|
66
67
|
}
|
|
67
68
|
|
|
68
69
|
.panel {
|
|
@@ -576,6 +577,118 @@
|
|
|
576
577
|
[data-theme="light"] .source-selector select {
|
|
577
578
|
background: var(--bg-secondary);
|
|
578
579
|
}
|
|
580
|
+
|
|
581
|
+
/* Search Results Modal */
|
|
582
|
+
.search-modal {
|
|
583
|
+
display: none;
|
|
584
|
+
position: fixed;
|
|
585
|
+
top: 70px;
|
|
586
|
+
right: 16px;
|
|
587
|
+
width: 400px;
|
|
588
|
+
max-height: 500px;
|
|
589
|
+
background: var(--bg-secondary);
|
|
590
|
+
border: 1px solid var(--border-color);
|
|
591
|
+
border-radius: 8px;
|
|
592
|
+
box-shadow: 0 4px 16px var(--shadow-color);
|
|
593
|
+
z-index: 99;
|
|
594
|
+
overflow: hidden;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
.search-modal.active {
|
|
598
|
+
display: flex;
|
|
599
|
+
flex-direction: column;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
.search-modal-header {
|
|
603
|
+
padding: 12px 16px;
|
|
604
|
+
border-bottom: 1px solid var(--border-color);
|
|
605
|
+
display: flex;
|
|
606
|
+
align-items: center;
|
|
607
|
+
justify-content: space-between;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
.search-modal-header h3 {
|
|
611
|
+
font-size: 14px;
|
|
612
|
+
font-weight: 600;
|
|
613
|
+
color: var(--text-primary);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
.search-modal-close {
|
|
617
|
+
cursor: pointer;
|
|
618
|
+
font-size: 18px;
|
|
619
|
+
color: var(--text-muted);
|
|
620
|
+
padding: 4px;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
.search-modal-close:hover {
|
|
624
|
+
color: var(--text-primary);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
.search-modal-content {
|
|
628
|
+
flex: 1;
|
|
629
|
+
overflow-y: auto;
|
|
630
|
+
padding: 8px;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
.search-result-item {
|
|
634
|
+
padding: 12px;
|
|
635
|
+
margin-bottom: 8px;
|
|
636
|
+
background: var(--bg-tertiary);
|
|
637
|
+
border-radius: 6px;
|
|
638
|
+
cursor: pointer;
|
|
639
|
+
transition: all 0.2s;
|
|
640
|
+
border-left: 3px solid var(--accent-blue);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
.search-result-item:hover {
|
|
644
|
+
background: var(--bg-primary);
|
|
645
|
+
transform: translateX(2px);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
.search-result-title {
|
|
649
|
+
font-size: 13px;
|
|
650
|
+
font-weight: 500;
|
|
651
|
+
margin-bottom: 4px;
|
|
652
|
+
color: var(--text-primary);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
.search-result-meta {
|
|
656
|
+
font-size: 11px;
|
|
657
|
+
color: var(--text-muted);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
.search-input-container {
|
|
661
|
+
padding: 12px 16px;
|
|
662
|
+
border-bottom: 1px solid var(--border-color);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
.search-input-container input {
|
|
666
|
+
width: 100%;
|
|
667
|
+
padding: 8px 12px;
|
|
668
|
+
background: var(--bg-tertiary);
|
|
669
|
+
border: 1px solid var(--border-color);
|
|
670
|
+
border-radius: 6px;
|
|
671
|
+
color: var(--text-primary);
|
|
672
|
+
font-size: 13px;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
.search-input-container input:focus {
|
|
676
|
+
outline: none;
|
|
677
|
+
border-color: var(--accent-blue);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
.no-search-results {
|
|
681
|
+
padding: 40px 20px;
|
|
682
|
+
text-align: center;
|
|
683
|
+
color: var(--text-muted);
|
|
684
|
+
font-size: 13px;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
.control-btn.active {
|
|
688
|
+
background: var(--accent-blue);
|
|
689
|
+
color: white;
|
|
690
|
+
border-color: var(--accent-blue);
|
|
691
|
+
}
|
|
579
692
|
</style>
|
|
580
693
|
</head>
|
|
581
694
|
<body>
|
|
@@ -590,6 +703,18 @@
|
|
|
590
703
|
</select>
|
|
591
704
|
</div>
|
|
592
705
|
<span class="sync-status" id="sync-status" title="Last sync time">Syncing...</span>
|
|
706
|
+
<button class="control-btn" id="thinking-toggle-btn" onclick="toggleAllThinking()" title="Toggle all thinking blocks">
|
|
707
|
+
<span class="icon">🧠</span>
|
|
708
|
+
<span>Show Thinking</span>
|
|
709
|
+
</button>
|
|
710
|
+
<button class="control-btn" id="search-btn" onclick="toggleSearch()" title="Search conversations">
|
|
711
|
+
<span class="icon">🔍</span>
|
|
712
|
+
<span>Search</span>
|
|
713
|
+
</button>
|
|
714
|
+
<button class="control-btn" id="copy-btn" onclick="copyConversation()" title="Copy conversation to clipboard" disabled>
|
|
715
|
+
<span class="icon">📋</span>
|
|
716
|
+
<span>Copy</span>
|
|
717
|
+
</button>
|
|
593
718
|
<button class="control-btn" id="export-btn" onclick="exportConversation()" title="Export conversation as TXT" disabled>
|
|
594
719
|
<span class="icon">📥</span>
|
|
595
720
|
<span>Export</span>
|
|
@@ -604,6 +729,20 @@
|
|
|
604
729
|
</button>
|
|
605
730
|
</div>
|
|
606
731
|
|
|
732
|
+
<!-- Search Modal -->
|
|
733
|
+
<div class="search-modal" id="search-modal">
|
|
734
|
+
<div class="search-modal-header">
|
|
735
|
+
<h3>Search Conversations</h3>
|
|
736
|
+
<span class="search-modal-close" onclick="toggleSearch()">✕</span>
|
|
737
|
+
</div>
|
|
738
|
+
<div class="search-input-container">
|
|
739
|
+
<input type="text" id="global-search-input" placeholder="Search across all conversations...">
|
|
740
|
+
</div>
|
|
741
|
+
<div class="search-modal-content" id="search-results">
|
|
742
|
+
<div class="no-search-results">Type to search across conversations</div>
|
|
743
|
+
</div>
|
|
744
|
+
</div>
|
|
745
|
+
|
|
607
746
|
<div class="app">
|
|
608
747
|
<!-- Projects Panel -->
|
|
609
748
|
<div class="panel" id="projects-panel">
|
|
@@ -652,6 +791,8 @@
|
|
|
652
791
|
let currentSource = 'claude-code';
|
|
653
792
|
let projects = [];
|
|
654
793
|
let availableSources = [];
|
|
794
|
+
let allThinkingExpanded = false;
|
|
795
|
+
let currentConversation = null;
|
|
655
796
|
|
|
656
797
|
// Theme Management
|
|
657
798
|
function getPreferredTheme() {
|
|
@@ -719,6 +860,7 @@
|
|
|
719
860
|
|
|
720
861
|
// Update UI
|
|
721
862
|
document.getElementById('export-btn').disabled = true;
|
|
863
|
+
document.getElementById('copy-btn').disabled = true;
|
|
722
864
|
|
|
723
865
|
// Reset panels
|
|
724
866
|
document.getElementById('sessions-list').innerHTML = `
|
|
@@ -811,6 +953,182 @@
|
|
|
811
953
|
document.body.removeChild(link);
|
|
812
954
|
}
|
|
813
955
|
|
|
956
|
+
// Copy conversation to clipboard
|
|
957
|
+
async function copyConversation() {
|
|
958
|
+
if (!currentConversation) return;
|
|
959
|
+
|
|
960
|
+
try {
|
|
961
|
+
const text = buildConversationText(currentConversation);
|
|
962
|
+
await navigator.clipboard.writeText(text);
|
|
963
|
+
|
|
964
|
+
// Show feedback
|
|
965
|
+
const btn = document.getElementById('copy-btn');
|
|
966
|
+
const originalText = btn.querySelector('span:last-child').textContent;
|
|
967
|
+
btn.querySelector('span:last-child').textContent = 'Copied!';
|
|
968
|
+
setTimeout(() => {
|
|
969
|
+
btn.querySelector('span:last-child').textContent = originalText;
|
|
970
|
+
}, 2000);
|
|
971
|
+
} catch (error) {
|
|
972
|
+
console.error('Failed to copy:', error);
|
|
973
|
+
alert('Failed to copy to clipboard');
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
// Build conversation text (same format as export)
|
|
978
|
+
function buildConversationText(conversation) {
|
|
979
|
+
const lines = [];
|
|
980
|
+
lines.push('='.repeat(60));
|
|
981
|
+
lines.push(`Session: ${conversation.session_id}`);
|
|
982
|
+
lines.push(`Source: ${currentSource}`);
|
|
983
|
+
lines.push('='.repeat(60));
|
|
984
|
+
lines.push('');
|
|
985
|
+
|
|
986
|
+
// Add summaries if present
|
|
987
|
+
if (conversation.summaries && conversation.summaries.length > 0) {
|
|
988
|
+
lines.push('SUMMARIES:');
|
|
989
|
+
conversation.summaries.forEach(s => lines.push(` • ${s}`));
|
|
990
|
+
lines.push('');
|
|
991
|
+
lines.push('-'.repeat(60));
|
|
992
|
+
lines.push('');
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// Add messages
|
|
996
|
+
conversation.messages.forEach(msg => {
|
|
997
|
+
const role = msg.role.toUpperCase();
|
|
998
|
+
const timestamp = msg.timestamp || '';
|
|
999
|
+
|
|
1000
|
+
lines.push(`[${role}] ${timestamp}`);
|
|
1001
|
+
if (msg.model) {
|
|
1002
|
+
lines.push(`Model: ${msg.model}`);
|
|
1003
|
+
}
|
|
1004
|
+
lines.push('-'.repeat(40));
|
|
1005
|
+
|
|
1006
|
+
// Content
|
|
1007
|
+
if (msg.content) {
|
|
1008
|
+
lines.push(msg.content);
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
// Thinking
|
|
1012
|
+
if (msg.thinking) {
|
|
1013
|
+
lines.push('');
|
|
1014
|
+
lines.push('--- THINKING ---');
|
|
1015
|
+
lines.push(msg.thinking);
|
|
1016
|
+
lines.push('--- END THINKING ---');
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
// Tool uses
|
|
1020
|
+
if (msg.tool_uses && msg.tool_uses.length > 0) {
|
|
1021
|
+
lines.push('');
|
|
1022
|
+
msg.tool_uses.forEach(tool => {
|
|
1023
|
+
lines.push(`[TOOL: ${tool.name}]`);
|
|
1024
|
+
if (typeof tool.input === 'object') {
|
|
1025
|
+
for (const [k, v] of Object.entries(tool.input)) {
|
|
1026
|
+
const val = String(v).length > 200 ? String(v).substring(0, 200) + '...' : String(v);
|
|
1027
|
+
lines.push(` ${k}: ${val}`);
|
|
1028
|
+
}
|
|
1029
|
+
} else {
|
|
1030
|
+
lines.push(` ${tool.input || ''}`);
|
|
1031
|
+
}
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
// Usage stats
|
|
1036
|
+
if (msg.usage) {
|
|
1037
|
+
const tokens = (msg.usage.input_tokens || 0) + (msg.usage.output_tokens || 0);
|
|
1038
|
+
lines.push(`\n[Tokens: ${tokens}]`);
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
lines.push('');
|
|
1042
|
+
lines.push('='.repeat(60));
|
|
1043
|
+
lines.push('');
|
|
1044
|
+
});
|
|
1045
|
+
|
|
1046
|
+
return lines.join('\n');
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
// Toggle all thinking blocks
|
|
1050
|
+
function toggleAllThinking() {
|
|
1051
|
+
allThinkingExpanded = !allThinkingExpanded;
|
|
1052
|
+
const thinkingBlocks = document.querySelectorAll('.thinking-block');
|
|
1053
|
+
const btn = document.getElementById('thinking-toggle-btn');
|
|
1054
|
+
|
|
1055
|
+
thinkingBlocks.forEach(block => {
|
|
1056
|
+
if (allThinkingExpanded) {
|
|
1057
|
+
block.classList.add('expanded');
|
|
1058
|
+
} else {
|
|
1059
|
+
block.classList.remove('expanded');
|
|
1060
|
+
}
|
|
1061
|
+
});
|
|
1062
|
+
|
|
1063
|
+
// Update button state
|
|
1064
|
+
if (allThinkingExpanded) {
|
|
1065
|
+
btn.classList.add('active');
|
|
1066
|
+
btn.querySelector('span:last-child').textContent = 'Hide Thinking';
|
|
1067
|
+
} else {
|
|
1068
|
+
btn.classList.remove('active');
|
|
1069
|
+
btn.querySelector('span:last-child').textContent = 'Show Thinking';
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// Toggle search modal
|
|
1074
|
+
function toggleSearch() {
|
|
1075
|
+
const modal = document.getElementById('search-modal');
|
|
1076
|
+
const btn = document.getElementById('search-btn');
|
|
1077
|
+
|
|
1078
|
+
if (modal.classList.contains('active')) {
|
|
1079
|
+
modal.classList.remove('active');
|
|
1080
|
+
btn.classList.remove('active');
|
|
1081
|
+
} else {
|
|
1082
|
+
modal.classList.add('active');
|
|
1083
|
+
btn.classList.add('active');
|
|
1084
|
+
document.getElementById('global-search-input').focus();
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
// Search across conversations
|
|
1089
|
+
let searchTimeout = null;
|
|
1090
|
+
async function searchConversations(query) {
|
|
1091
|
+
if (!query || query.length < 2) {
|
|
1092
|
+
document.getElementById('search-results').innerHTML = '<div class="no-search-results">Type at least 2 characters to search</div>';
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
document.getElementById('search-results').innerHTML = '<div class="loading"><div class="spinner"></div>Searching...</div>';
|
|
1097
|
+
|
|
1098
|
+
try {
|
|
1099
|
+
const response = await fetch(`/api/search?q=${encodeURIComponent(query)}&source=${currentSource}`);
|
|
1100
|
+
const results = await response.json();
|
|
1101
|
+
|
|
1102
|
+
if (results.length === 0) {
|
|
1103
|
+
document.getElementById('search-results').innerHTML = '<div class="no-search-results">No results found</div>';
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
const html = results.map(result => `
|
|
1108
|
+
<div class="search-result-item" onclick="selectSearchResult('${result.project_id}', '${result.session_id}')">
|
|
1109
|
+
<div class="search-result-title">${escapeHtml(result.session_id)}</div>
|
|
1110
|
+
<div class="search-result-meta">${escapeHtml(result.project_name)}</div>
|
|
1111
|
+
</div>
|
|
1112
|
+
`).join('');
|
|
1113
|
+
|
|
1114
|
+
document.getElementById('search-results').innerHTML = html;
|
|
1115
|
+
} catch (error) {
|
|
1116
|
+
console.error('Search error:', error);
|
|
1117
|
+
document.getElementById('search-results').innerHTML = '<div class="no-search-results">Search failed</div>';
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
// Select a search result
|
|
1122
|
+
async function selectSearchResult(projectId, sessionId) {
|
|
1123
|
+
toggleSearch(); // Close search modal
|
|
1124
|
+
currentProjectId = projectId;
|
|
1125
|
+
currentSessionId = sessionId;
|
|
1126
|
+
|
|
1127
|
+
// Load the conversation
|
|
1128
|
+
await selectProject(projectId);
|
|
1129
|
+
await selectSession(sessionId);
|
|
1130
|
+
}
|
|
1131
|
+
|
|
814
1132
|
// Format timestamp
|
|
815
1133
|
function formatTime(timestamp) {
|
|
816
1134
|
if (!timestamp) return '';
|
|
@@ -886,8 +1204,9 @@
|
|
|
886
1204
|
currentSessionId = null;
|
|
887
1205
|
renderProjects(projects);
|
|
888
1206
|
|
|
889
|
-
// Disable export
|
|
1207
|
+
// Disable export and copy buttons when switching projects
|
|
890
1208
|
document.getElementById('export-btn').disabled = true;
|
|
1209
|
+
document.getElementById('copy-btn').disabled = true;
|
|
891
1210
|
|
|
892
1211
|
const sessionsContainer = document.getElementById('sessions-list');
|
|
893
1212
|
sessionsContainer.innerHTML = `
|
|
@@ -967,6 +1286,7 @@
|
|
|
967
1286
|
|
|
968
1287
|
// Render conversation
|
|
969
1288
|
function renderConversation(conversation) {
|
|
1289
|
+
currentConversation = conversation; // Store for copy function
|
|
970
1290
|
const container = document.getElementById('conversation-content');
|
|
971
1291
|
|
|
972
1292
|
let summariesHtml = '';
|
|
@@ -982,8 +1302,9 @@
|
|
|
982
1302
|
const messagesHtml = conversation.messages.map(msg => {
|
|
983
1303
|
let thinkingHtml = '';
|
|
984
1304
|
if (msg.thinking) {
|
|
1305
|
+
const expandedClass = allThinkingExpanded ? ' expanded' : '';
|
|
985
1306
|
thinkingHtml = `
|
|
986
|
-
<div class="thinking-block" onclick="this.classList.toggle('expanded')">
|
|
1307
|
+
<div class="thinking-block${expandedClass}" onclick="this.classList.toggle('expanded')">
|
|
987
1308
|
<div class="thinking-header">Thinking</div>
|
|
988
1309
|
<div class="thinking-content">${escapeHtml(msg.thinking)}</div>
|
|
989
1310
|
</div>
|
|
@@ -1043,8 +1364,9 @@
|
|
|
1043
1364
|
</div>
|
|
1044
1365
|
`;
|
|
1045
1366
|
|
|
1046
|
-
// Enable export
|
|
1367
|
+
// Enable export and copy buttons
|
|
1047
1368
|
document.getElementById('export-btn').disabled = false;
|
|
1369
|
+
document.getElementById('copy-btn').disabled = false;
|
|
1048
1370
|
}
|
|
1049
1371
|
|
|
1050
1372
|
// Search projects
|
|
@@ -1056,6 +1378,13 @@
|
|
|
1056
1378
|
renderProjects(filtered);
|
|
1057
1379
|
});
|
|
1058
1380
|
|
|
1381
|
+
// Global search input handler with debounce
|
|
1382
|
+
document.getElementById('global-search-input').addEventListener('input', (e) => {
|
|
1383
|
+
clearTimeout(searchTimeout);
|
|
1384
|
+
const query = e.target.value;
|
|
1385
|
+
searchTimeout = setTimeout(() => searchConversations(query), 300);
|
|
1386
|
+
});
|
|
1387
|
+
|
|
1059
1388
|
// Initialize
|
|
1060
1389
|
async function init() {
|
|
1061
1390
|
await loadSources();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: clicodelog
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: A lightweight, local-first web app to browse, inspect, and export logs from CLI-based AI coding agents
|
|
5
5
|
Author: monk1337
|
|
6
6
|
License-Expression: MIT
|
|
@@ -25,8 +25,9 @@ Classifier: Topic :: Utilities
|
|
|
25
25
|
Requires-Python: >=3.7
|
|
26
26
|
Description-Content-Type: text/markdown
|
|
27
27
|
License-File: LICENSE
|
|
28
|
-
Requires-Dist:
|
|
29
|
-
Requires-Dist:
|
|
28
|
+
Requires-Dist: fastapi>=0.104.0
|
|
29
|
+
Requires-Dist: uvicorn[standard]>=0.24.0
|
|
30
|
+
Requires-Dist: jinja2>=3.1.0
|
|
30
31
|
Provides-Extra: dev
|
|
31
32
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
32
33
|
Requires-Dist: build; extra == "dev"
|
|
@@ -53,7 +54,7 @@ CLI-based AI coding agents — Claude Code, OpenAI Codex, and Gemini CLI.
|
|
|
53
54
|
|
|
54
55
|
<p>
|
|
55
56
|
<img src="https://img.shields.io/badge/Python-3.7+-blue.svg" alt="Python 3.7+" />
|
|
56
|
-
<img src="https://img.shields.io/badge/
|
|
57
|
+
<img src="https://img.shields.io/badge/FastAPI-0.104+-00c7b7.svg" alt="FastAPI" />
|
|
57
58
|
<img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License MIT" />
|
|
58
59
|
<a href="http://makeapullrequest.com">
|
|
59
60
|
<img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square" alt="PRs Welcome" />
|
|
@@ -84,7 +85,7 @@ CLI-based AI coding agents — Claude Code, OpenAI Codex, and Gemini CLI.
|
|
|
84
85
|
|
|
85
86
|
## Installation
|
|
86
87
|
|
|
87
|
-
### Via uv (Recommended)
|
|
88
|
+
### Via uv tool (Recommended)
|
|
88
89
|
|
|
89
90
|
[uv](https://github.com/astral-sh/uv) is a fast Python package installer. Install it first if you haven't:
|
|
90
91
|
|
|
@@ -92,10 +93,15 @@ CLI-based AI coding agents — Claude Code, OpenAI Codex, and Gemini CLI.
|
|
|
92
93
|
curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
93
94
|
```
|
|
94
95
|
|
|
95
|
-
Then install clicodelog:
|
|
96
|
+
Then install clicodelog as an isolated tool:
|
|
96
97
|
|
|
97
98
|
```bash
|
|
98
|
-
uv
|
|
99
|
+
uv tool install clicodelog
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
To upgrade:
|
|
103
|
+
```bash
|
|
104
|
+
uv tool upgrade clicodelog
|
|
99
105
|
```
|
|
100
106
|
|
|
101
107
|
### Via pip
|
|
@@ -109,7 +115,7 @@ pip install clicodelog
|
|
|
109
115
|
```bash
|
|
110
116
|
git clone https://github.com/monk1337/clicodelog.git
|
|
111
117
|
cd clicodelog
|
|
112
|
-
uv
|
|
118
|
+
uv tool install -e .
|
|
113
119
|
# or with pip:
|
|
114
120
|
pip install -e .
|
|
115
121
|
```
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "clicodelog"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.1"
|
|
8
8
|
description = "A lightweight, local-first web app to browse, inspect, and export logs from CLI-based AI coding agents"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -42,8 +42,9 @@ classifiers = [
|
|
|
42
42
|
]
|
|
43
43
|
|
|
44
44
|
dependencies = [
|
|
45
|
-
"
|
|
46
|
-
"
|
|
45
|
+
"fastapi>=0.104.0",
|
|
46
|
+
"uvicorn[standard]>=0.24.0",
|
|
47
|
+
"jinja2>=3.1.0",
|
|
47
48
|
]
|
|
48
49
|
|
|
49
50
|
[project.optional-dependencies]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|