clicodelog 0.1.0__py3-none-any.whl → 0.2.1__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.
- clicodelog/__init__.py +1 -1
- clicodelog/app.py +171 -96
- clicodelog/cli.py +2 -2
- clicodelog/templates/index.html +333 -4
- {clicodelog-0.1.0.dist-info → clicodelog-0.2.1.dist-info}/METADATA +105 -66
- clicodelog-0.2.1.dist-info/RECORD +11 -0
- {clicodelog-0.1.0.dist-info → clicodelog-0.2.1.dist-info}/WHEEL +1 -1
- clicodelog-0.1.0.dist-info/RECORD +0 -11
- {clicodelog-0.1.0.dist-info → clicodelog-0.2.1.dist-info}/entry_points.txt +0 -0
- {clicodelog-0.1.0.dist-info → clicodelog-0.2.1.dist-info}/licenses/LICENSE +0 -0
- {clicodelog-0.1.0.dist-info → clicodelog-0.2.1.dist-info}/top_level.txt +0 -0
clicodelog/__init__.py
CHANGED
clicodelog/app.py
CHANGED
|
@@ -1,21 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
1
2
|
"""
|
|
2
|
-
|
|
3
|
-
A web app to browse,
|
|
4
|
-
Data is copied from source directories to
|
|
3
|
+
AI Conversation History Viewer
|
|
4
|
+
A web app to browse and view Claude Code, OpenAI Codex, and Google Gemini conversation history.
|
|
5
|
+
Data is copied from source directories to ./data/ for backup and local use.
|
|
5
6
|
Background sync runs every hour to keep data updated.
|
|
6
7
|
"""
|
|
7
8
|
|
|
8
|
-
import base64
|
|
9
9
|
import json
|
|
10
10
|
import os
|
|
11
11
|
import shutil
|
|
12
|
+
import signal
|
|
13
|
+
import subprocess
|
|
12
14
|
import threading
|
|
13
15
|
import time
|
|
16
|
+
import webbrowser
|
|
14
17
|
from datetime import datetime
|
|
15
18
|
from pathlib import Path
|
|
19
|
+
from typing import Optional
|
|
16
20
|
|
|
17
|
-
|
|
18
|
-
from
|
|
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
|
|
19
27
|
|
|
20
28
|
# Package directory for templates
|
|
21
29
|
PACKAGE_DIR = Path(__file__).parent
|
|
@@ -51,13 +59,9 @@ sync_lock = threading.Lock()
|
|
|
51
59
|
last_sync_time = {} # Track per-source sync times
|
|
52
60
|
current_source = "claude-code" # Default source
|
|
53
61
|
|
|
54
|
-
# Create Flask app with template folder from package
|
|
55
|
-
app = Flask(__name__, template_folder=str(PACKAGE_DIR / "templates"))
|
|
56
|
-
CORS(app)
|
|
57
|
-
|
|
58
62
|
|
|
59
63
|
def sync_data(source_id=None, silent=False):
|
|
60
|
-
"""Copy data from source directory to data
|
|
64
|
+
"""Copy data from source directory to ./data/{source}/ for backup."""
|
|
61
65
|
global last_sync_time
|
|
62
66
|
|
|
63
67
|
if source_id is None:
|
|
@@ -79,7 +83,7 @@ def sync_data(source_id=None, silent=False):
|
|
|
79
83
|
return False
|
|
80
84
|
|
|
81
85
|
# Create data directory if it doesn't exist
|
|
82
|
-
DATA_DIR.mkdir(
|
|
86
|
+
DATA_DIR.mkdir(exist_ok=True)
|
|
83
87
|
|
|
84
88
|
# Copy source directory
|
|
85
89
|
if not silent:
|
|
@@ -156,11 +160,13 @@ def background_sync():
|
|
|
156
160
|
|
|
157
161
|
def encode_path_id(path):
|
|
158
162
|
"""Encode a path as a safe ID using base64."""
|
|
163
|
+
import base64
|
|
159
164
|
return base64.urlsafe_b64encode(path.encode()).decode().rstrip('=')
|
|
160
165
|
|
|
161
166
|
|
|
162
167
|
def decode_path_id(encoded_id):
|
|
163
168
|
"""Decode a base64-encoded path ID."""
|
|
169
|
+
import base64
|
|
164
170
|
# Add back padding if needed
|
|
165
171
|
padding = 4 - len(encoded_id) % 4
|
|
166
172
|
if padding != 4:
|
|
@@ -577,11 +583,18 @@ def parse_codex_conversation(session_file, session_id):
|
|
|
577
583
|
|
|
578
584
|
elif payload_type == "reasoning":
|
|
579
585
|
# Reasoning/thinking block
|
|
580
|
-
summary_parts = payload.get("summary", [])
|
|
581
586
|
thinking_text = ""
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
587
|
+
|
|
588
|
+
# Check if content is encrypted
|
|
589
|
+
if payload.get("encrypted_content"):
|
|
590
|
+
thinking_text = "[Reasoning content is encrypted and cannot be displayed]\n\nOpenAI Codex encrypts extended thinking for privacy. The model used reasoning here, but the content is not accessible in the logs."
|
|
591
|
+
else:
|
|
592
|
+
# Try to extract from summary (unencrypted format)
|
|
593
|
+
summary_parts = payload.get("summary", [])
|
|
594
|
+
for part in summary_parts:
|
|
595
|
+
if isinstance(part, dict) and part.get("type") == "summary_text":
|
|
596
|
+
thinking_text += part.get("text", "") + "\n"
|
|
597
|
+
|
|
585
598
|
if thinking_text:
|
|
586
599
|
messages.append({
|
|
587
600
|
"role": "assistant",
|
|
@@ -711,13 +724,29 @@ def parse_gemini_conversation(session_file, session_id):
|
|
|
711
724
|
}
|
|
712
725
|
|
|
713
726
|
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
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
|
+
)
|
|
738
|
+
|
|
739
|
+
# Templates
|
|
740
|
+
templates = Jinja2Templates(directory=str(PACKAGE_DIR / "templates"))
|
|
717
741
|
|
|
718
742
|
|
|
719
|
-
@app.
|
|
720
|
-
def
|
|
743
|
+
@app.get("/", response_class=HTMLResponse)
|
|
744
|
+
async def index(request: Request):
|
|
745
|
+
return templates.TemplateResponse("index.html", {"request": request})
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
@app.get("/api/sources")
|
|
749
|
+
async def api_sources():
|
|
721
750
|
"""Get available sources."""
|
|
722
751
|
sources = []
|
|
723
752
|
for source_id, config in SOURCES.items():
|
|
@@ -726,55 +755,55 @@ def api_sources():
|
|
|
726
755
|
"name": config["name"],
|
|
727
756
|
"available": config["source_dir"].exists()
|
|
728
757
|
})
|
|
729
|
-
return
|
|
758
|
+
return {
|
|
730
759
|
"sources": sources,
|
|
731
760
|
"current": current_source
|
|
732
|
-
}
|
|
761
|
+
}
|
|
733
762
|
|
|
734
763
|
|
|
735
|
-
@app.
|
|
736
|
-
def api_set_source(source_id):
|
|
764
|
+
@app.post("/api/sources/{source_id}")
|
|
765
|
+
async def api_set_source(source_id: str):
|
|
737
766
|
"""Set the current source."""
|
|
738
767
|
global current_source
|
|
739
768
|
if source_id not in SOURCES:
|
|
740
|
-
return
|
|
769
|
+
return JSONResponse({"error": "Unknown source"}, status_code=400)
|
|
741
770
|
current_source = source_id
|
|
742
|
-
return
|
|
771
|
+
return {"status": "success", "current": current_source}
|
|
743
772
|
|
|
744
773
|
|
|
745
|
-
@app.
|
|
746
|
-
def api_projects():
|
|
747
|
-
source_id =
|
|
748
|
-
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)
|
|
749
778
|
|
|
750
779
|
|
|
751
|
-
@app.
|
|
752
|
-
def api_sessions(project_id):
|
|
753
|
-
source_id =
|
|
754
|
-
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)
|
|
755
784
|
|
|
756
785
|
|
|
757
|
-
@app.
|
|
758
|
-
def api_conversation(project_id, session_id):
|
|
759
|
-
source_id =
|
|
760
|
-
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)
|
|
761
790
|
|
|
762
791
|
|
|
763
|
-
@app.
|
|
764
|
-
def api_search():
|
|
792
|
+
@app.get("/api/search")
|
|
793
|
+
async def api_search(q: Optional[str] = None, source: Optional[str] = None):
|
|
765
794
|
"""Search across all conversations."""
|
|
766
|
-
query =
|
|
767
|
-
source_id =
|
|
795
|
+
query = (q or '').lower()
|
|
796
|
+
source_id = source or current_source
|
|
768
797
|
|
|
769
798
|
if not query:
|
|
770
|
-
return
|
|
799
|
+
return []
|
|
771
800
|
|
|
772
801
|
if source_id not in SOURCES:
|
|
773
|
-
return
|
|
802
|
+
return []
|
|
774
803
|
|
|
775
804
|
data_dir = DATA_DIR / SOURCES[source_id]["data_subdir"]
|
|
776
805
|
if not data_dir.exists():
|
|
777
|
-
return
|
|
806
|
+
return []
|
|
778
807
|
|
|
779
808
|
results = []
|
|
780
809
|
|
|
@@ -827,17 +856,17 @@ def api_search():
|
|
|
827
856
|
except Exception:
|
|
828
857
|
continue
|
|
829
858
|
|
|
830
|
-
return
|
|
859
|
+
return results[:50] # Limit results
|
|
831
860
|
|
|
832
861
|
|
|
833
|
-
@app.
|
|
834
|
-
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):
|
|
835
864
|
"""Export conversation as text file."""
|
|
836
|
-
source_id =
|
|
865
|
+
source_id = source or current_source
|
|
837
866
|
conversation = get_conversation(project_id, session_id, source_id)
|
|
838
867
|
|
|
839
868
|
if "error" in conversation:
|
|
840
|
-
return
|
|
869
|
+
return JSONResponse(conversation, status_code=404)
|
|
841
870
|
|
|
842
871
|
# Build text content
|
|
843
872
|
lines = []
|
|
@@ -851,7 +880,7 @@ def api_export(project_id, session_id):
|
|
|
851
880
|
if conversation.get("summaries"):
|
|
852
881
|
lines.append("SUMMARIES:")
|
|
853
882
|
for s in conversation["summaries"]:
|
|
854
|
-
lines.append(f"
|
|
883
|
+
lines.append(f" • {s}")
|
|
855
884
|
lines.append("")
|
|
856
885
|
lines.append("-" * 60)
|
|
857
886
|
lines.append("")
|
|
@@ -902,63 +931,100 @@ def api_export(project_id, session_id):
|
|
|
902
931
|
text_content = "\n".join(lines)
|
|
903
932
|
|
|
904
933
|
return Response(
|
|
905
|
-
text_content,
|
|
906
|
-
|
|
934
|
+
content=text_content,
|
|
935
|
+
media_type="text/plain",
|
|
907
936
|
headers={"Content-Disposition": f"attachment; filename={session_id}.txt"}
|
|
908
937
|
)
|
|
909
938
|
|
|
910
939
|
|
|
911
|
-
@app.
|
|
912
|
-
def api_sync():
|
|
940
|
+
@app.post("/api/sync")
|
|
941
|
+
async def api_sync(source: Optional[str] = None):
|
|
913
942
|
"""Manually trigger a data sync."""
|
|
914
|
-
source_id =
|
|
943
|
+
source_id = source or current_source
|
|
915
944
|
try:
|
|
916
945
|
sync_data(source_id=source_id, silent=True)
|
|
917
|
-
return
|
|
946
|
+
return {
|
|
918
947
|
"status": "success",
|
|
919
948
|
"source": source_id,
|
|
920
949
|
"last_sync": last_sync_time.get(source_id).isoformat() if last_sync_time.get(source_id) else None
|
|
921
|
-
}
|
|
950
|
+
}
|
|
922
951
|
except Exception as e:
|
|
923
|
-
return
|
|
952
|
+
return JSONResponse({"status": "error", "message": str(e)}, status_code=500)
|
|
924
953
|
|
|
925
954
|
|
|
926
|
-
@app.
|
|
927
|
-
def api_status():
|
|
955
|
+
@app.get("/api/status")
|
|
956
|
+
async def api_status(source: Optional[str] = None):
|
|
928
957
|
"""Get sync status."""
|
|
929
|
-
source_id =
|
|
958
|
+
source_id = source or current_source
|
|
930
959
|
source_config = SOURCES.get(source_id, {})
|
|
931
960
|
data_dir = DATA_DIR / source_config.get("data_subdir", "")
|
|
932
961
|
|
|
933
|
-
return
|
|
962
|
+
return {
|
|
934
963
|
"source": source_id,
|
|
935
964
|
"last_sync": last_sync_time.get(source_id).isoformat() if last_sync_time.get(source_id) else None,
|
|
936
965
|
"sync_interval_hours": SYNC_INTERVAL / 3600,
|
|
937
966
|
"data_dir": str(data_dir)
|
|
938
|
-
}
|
|
939
|
-
|
|
967
|
+
}
|
|
940
968
|
|
|
941
|
-
def find_available_port(host, start_port, max_attempts=100):
|
|
942
|
-
"""Find an available port starting from start_port."""
|
|
943
|
-
import socket
|
|
944
969
|
|
|
945
|
-
|
|
970
|
+
def kill_process_on_port(port, max_retries=3):
|
|
971
|
+
"""Kill any process running on the specified port."""
|
|
972
|
+
for attempt in range(max_retries):
|
|
946
973
|
try:
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
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
|
|
954
1015
|
|
|
955
|
-
def run_server(host="127.0.0.1", port=5050, skip_sync=False, debug=False):
|
|
956
|
-
"""Run the Flask server."""
|
|
957
|
-
from clicodelog import __version__
|
|
958
1016
|
|
|
1017
|
+
def run_server(host="127.0.0.1", port=6126, skip_sync=False, debug=False):
|
|
1018
|
+
"""Run the FastAPI server with uvicorn."""
|
|
959
1019
|
print("=" * 60)
|
|
960
|
-
print(
|
|
1020
|
+
print("CLI Code Log - AI Conversation History Viewer")
|
|
961
1021
|
print("=" * 60)
|
|
1022
|
+
|
|
1023
|
+
# Kill any process on the 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
|
|
962
1028
|
|
|
963
1029
|
if not skip_sync:
|
|
964
1030
|
# Sync data from all sources
|
|
@@ -969,28 +1035,37 @@ def run_server(host="127.0.0.1", port=5050, skip_sync=False, debug=False):
|
|
|
969
1035
|
print(f" Backup: {DATA_DIR / config['data_subdir']}")
|
|
970
1036
|
|
|
971
1037
|
if sync_data(source_id=source_id):
|
|
972
|
-
print(" Sync completed!")
|
|
1038
|
+
print(f" ✓ Sync completed!")
|
|
973
1039
|
else:
|
|
974
|
-
print(" Warning: Could not sync. Using existing local data if available.")
|
|
975
|
-
|
|
976
|
-
# Start background sync thread
|
|
977
|
-
print(f"\nBackground sync: Every {SYNC_INTERVAL // 3600} hour(s)")
|
|
978
|
-
sync_thread = threading.Thread(target=background_sync, daemon=True)
|
|
979
|
-
sync_thread.start()
|
|
980
|
-
print("Background sync thread started.")
|
|
1040
|
+
print(f" ⚠ Warning: Could not sync. Using existing local data if available.")
|
|
981
1041
|
else:
|
|
982
1042
|
print("\nSkipping initial sync (--no-sync flag)")
|
|
983
1043
|
|
|
984
|
-
#
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
1044
|
+
# Start background sync thread
|
|
1045
|
+
print(f"\nBackground sync: Every {SYNC_INTERVAL // 3600} hour(s)")
|
|
1046
|
+
sync_thread = threading.Thread(target=background_sync, daemon=True)
|
|
1047
|
+
sync_thread.start()
|
|
1048
|
+
print("Background sync thread started.")
|
|
988
1049
|
|
|
1050
|
+
url = f"http://{host}:{port}"
|
|
989
1051
|
print(f"\nStarting server...")
|
|
990
|
-
print(f"
|
|
1052
|
+
print(f"🌐 Opening {url} in your browser...")
|
|
991
1053
|
print("=" * 60)
|
|
992
|
-
|
|
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")
|
|
993
1067
|
|
|
994
1068
|
|
|
995
1069
|
if __name__ == '__main__':
|
|
996
|
-
|
|
1070
|
+
# For direct execution
|
|
1071
|
+
run_server(debug=True)
|
clicodelog/cli.py
CHANGED
clicodelog/templates/index.html
CHANGED
|
@@ -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.1
|
|
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"
|
|
@@ -34,7 +35,10 @@ Requires-Dist: twine; extra == "dev"
|
|
|
34
35
|
Dynamic: license-file
|
|
35
36
|
|
|
36
37
|
<div align="center">
|
|
37
|
-
<
|
|
38
|
+
<div align="center">
|
|
39
|
+
<img width="220px" src="https://raw.githubusercontent.com/monk1337/clicodelog/refs/heads/main/screenshots/logo.png">
|
|
40
|
+
</div>
|
|
41
|
+
|
|
38
42
|
<p>
|
|
39
43
|
A lightweight, local-first web app to browse, inspect, and export logs from
|
|
40
44
|
CLI-based AI coding agents — Claude Code, OpenAI Codex, and Gemini CLI.
|
|
@@ -50,7 +54,7 @@ CLI-based AI coding agents — Claude Code, OpenAI Codex, and Gemini CLI.
|
|
|
50
54
|
|
|
51
55
|
<p>
|
|
52
56
|
<img src="https://img.shields.io/badge/Python-3.7+-blue.svg" alt="Python 3.7+" />
|
|
53
|
-
<img src="https://img.shields.io/badge/
|
|
57
|
+
<img src="https://img.shields.io/badge/FastAPI-0.104+-00c7b7.svg" alt="FastAPI" />
|
|
54
58
|
<img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License MIT" />
|
|
55
59
|
<a href="http://makeapullrequest.com">
|
|
56
60
|
<img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square" alt="PRs Welcome" />
|
|
@@ -58,10 +62,97 @@ CLI-based AI coding agents — Claude Code, OpenAI Codex, and Gemini CLI.
|
|
|
58
62
|
</p>
|
|
59
63
|
</div>
|
|
60
64
|
|
|
61
|
-
|
|
65
|
+
<!-- <div align="center">
|
|
66
|
+
<table>
|
|
67
|
+
<tr>
|
|
68
|
+
<td align="center">
|
|
69
|
+
<img src="screenshots/claude.png" width="80" alt="Claude Code"><br>
|
|
70
|
+
<sub><b>Claude Code</b></sub>
|
|
71
|
+
</td>
|
|
72
|
+
<td align="center">
|
|
73
|
+
<img src="screenshots/codex.png" width="80" alt="OpenAI Codex"><br>
|
|
74
|
+
<sub><b>OpenAI Codex</b></sub>
|
|
75
|
+
</td>
|
|
76
|
+
<td align="center">
|
|
77
|
+
<img src="screenshots/gemini.png" width="80" alt="Gemini CLI"><br>
|
|
78
|
+
<sub><b>Gemini CLI</b></sub>
|
|
79
|
+
</td>
|
|
80
|
+
</tr>
|
|
81
|
+
</table>
|
|
82
|
+
</div> -->
|
|
62
83
|
|
|
63
84
|
---
|
|
64
85
|
|
|
86
|
+
## Installation
|
|
87
|
+
|
|
88
|
+
### Via uv tool (Recommended)
|
|
89
|
+
|
|
90
|
+
[uv](https://github.com/astral-sh/uv) is a fast Python package installer. Install it first if you haven't:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Then install clicodelog as an isolated tool:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
uv tool install clicodelog
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
To upgrade:
|
|
103
|
+
```bash
|
|
104
|
+
uv tool upgrade clicodelog
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Via pip
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
pip install clicodelog
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### From source
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
git clone https://github.com/monk1337/clicodelog.git
|
|
117
|
+
cd clicodelog
|
|
118
|
+
uv tool install -e .
|
|
119
|
+
# or with pip:
|
|
120
|
+
pip install -e .
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Usage
|
|
126
|
+
|
|
127
|
+
After installation, simply run:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
clicodelog
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
The app will:
|
|
134
|
+
- Auto-kill any process on port **6126** (if occupied)
|
|
135
|
+
- Sync data from all AI coding agent sources
|
|
136
|
+
- Start a web server at **http://localhost:6126**
|
|
137
|
+
|
|
138
|
+
### Command Options
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
clicodelog --help # Show all options
|
|
142
|
+
clicodelog --port 8080 # Use custom port
|
|
143
|
+
clicodelog --host 0.0.0.0 # Bind to all interfaces
|
|
144
|
+
clicodelog --no-sync # Skip initial data sync
|
|
145
|
+
clicodelog --debug # Run in debug mode
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Alternative: Run from source
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
git clone https://github.com/monk1337/clicodelog.git
|
|
152
|
+
cd clicodelog
|
|
153
|
+
python -m clicodelog.cli
|
|
154
|
+
```
|
|
155
|
+
|
|
65
156
|
## Features
|
|
66
157
|
|
|
67
158
|
- **Multi-source support** — View logs from Claude Code, OpenAI Codex, and Gemini CLI
|
|
@@ -104,56 +195,18 @@ CLI-based AI coding agents — Claude Code, OpenAI Codex, and Gemini CLI.
|
|
|
104
195
|
|
|
105
196
|
---
|
|
106
197
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
### Via pip (Recommended)
|
|
110
|
-
|
|
111
|
-
```bash
|
|
112
|
-
pip install clicodelog
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
### From source
|
|
116
|
-
|
|
117
|
-
```bash
|
|
118
|
-
git clone https://github.com/monk1337/clicodelog.git
|
|
119
|
-
cd clicodelog
|
|
120
|
-
pip install -e .
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
---
|
|
124
|
-
|
|
125
|
-
## Usage
|
|
126
|
-
|
|
127
|
-
If installed via pip:
|
|
128
|
-
|
|
129
|
-
```bash
|
|
130
|
-
clicodelog
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
Or run directly from source:
|
|
134
|
-
|
|
135
|
-
```bash
|
|
136
|
-
./run.sh
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
Or manually:
|
|
198
|
+
### CLI Options
|
|
140
199
|
|
|
141
200
|
```bash
|
|
142
|
-
|
|
143
|
-
|
|
201
|
+
clicodelog --help # Show help message
|
|
202
|
+
clicodelog --version # Show version
|
|
203
|
+
clicodelog --port 8080 # Run on custom port (default: 6126)
|
|
204
|
+
clicodelog --host 0.0.0.0 # Bind to all interfaces (default: 127.0.0.1)
|
|
205
|
+
clicodelog --no-sync # Skip initial data sync
|
|
206
|
+
clicodelog --debug # Run in debug mode
|
|
144
207
|
```
|
|
145
208
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
### CLI Options
|
|
149
|
-
|
|
150
|
-
```
|
|
151
|
-
clicodelog --help
|
|
152
|
-
clicodelog --port 8080 # Run on custom port
|
|
153
|
-
clicodelog --host 0.0.0.0 # Bind to all interfaces
|
|
154
|
-
clicodelog --no-sync # Skip initial data sync
|
|
155
|
-
clicodelog --debug # Run in debug mode
|
|
156
|
-
```
|
|
209
|
+
**Note:** The app automatically kills any process running on the specified port before starting.
|
|
157
210
|
|
|
158
211
|
---
|
|
159
212
|
|
|
@@ -289,17 +342,3 @@ MIT
|
|
|
289
342
|
</div>
|
|
290
343
|
|
|
291
344
|
```
|
|
292
|
-
|
|
293
|
-
@misc{clicodelog2026,
|
|
294
|
-
title = {clicodelog: Browse, inspect CLI-based AI coding agents},
|
|
295
|
-
author = {Pal, Ankit},
|
|
296
|
-
year = {2026},
|
|
297
|
-
howpublished = {\url{https://github.com/monk1337/clicodelog}},
|
|
298
|
-
note = {A lightweight, local-first web app to browse, inspect, and export logs from CLI-based AI coding agents — Claude Code, OpenAI Codex, and Gemini CLI.}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
```
|
|
302
|
-
|
|
303
|
-
## 💁 Contributing
|
|
304
|
-
|
|
305
|
-
Welcome any contributions to open source project, including new features, improvements to infrastructure, and more comprehensive documentation.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
clicodelog/__init__.py,sha256=ZRRWF0lVHMqhbZi5O9reskm45hUqG911bWa21dRMvvA,136
|
|
2
|
+
clicodelog/__main__.py,sha256=JDb49rUInCUlT5Ya0aGfusfJm8e7wGqA447mrnVqDkA,119
|
|
3
|
+
clicodelog/app.py,sha256=Wfefm-ie0qDdZU9p0CpPELfy_d9b-bgydrG50PghiGw,40860
|
|
4
|
+
clicodelog/cli.py,sha256=c34P5fOpE5Hy_DJog52BGO4ab4cUBRZZTsW5nxaFUL4,1297
|
|
5
|
+
clicodelog/templates/index.html,sha256=2F6iCVq50nzkY_381QeuAGYJW9KJWJk2Taav1UmcfKk,46501
|
|
6
|
+
clicodelog-0.2.1.dist-info/licenses/LICENSE,sha256=enO5ZtMm5_HoQnB9jA_d3CCajd9I6Kzg-m1FmNCsAvk,1065
|
|
7
|
+
clicodelog-0.2.1.dist-info/METADATA,sha256=kaqtFBPFdK-cvJ3G0-NGbtrvZdXBdI7BNjDzay10yKE,9124
|
|
8
|
+
clicodelog-0.2.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
9
|
+
clicodelog-0.2.1.dist-info/entry_points.txt,sha256=6Zodty_o0mVbDoIcNPQ31aI-P-KLZ0Xr_qsfrQkDYck,51
|
|
10
|
+
clicodelog-0.2.1.dist-info/top_level.txt,sha256=YvkoZJtBm4QSRL2YlLr5TeVILsiptIF7D43i29RG5oc,11
|
|
11
|
+
clicodelog-0.2.1.dist-info/RECORD,,
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
clicodelog/__init__.py,sha256=HYMXrxX39PPodu0qTmWqbgTr09RLzUtl3QNFDQCz_r4,136
|
|
2
|
-
clicodelog/__main__.py,sha256=JDb49rUInCUlT5Ya0aGfusfJm8e7wGqA447mrnVqDkA,119
|
|
3
|
-
clicodelog/app.py,sha256=W-7mQSwcB5N69ZEYhdOkg-fs93wxR4XccPAQ2H1GKO8,37823
|
|
4
|
-
clicodelog/cli.py,sha256=mH_fjEVg2RovP2sPpZPOyIzejnLcvaqi0hf41Xnxnao,1297
|
|
5
|
-
clicodelog/templates/index.html,sha256=kUYQJivjWOLonREjinUsIHvgiGUHbhuJvlLWyjIst9Y,34392
|
|
6
|
-
clicodelog-0.1.0.dist-info/licenses/LICENSE,sha256=enO5ZtMm5_HoQnB9jA_d3CCajd9I6Kzg-m1FmNCsAvk,1065
|
|
7
|
-
clicodelog-0.1.0.dist-info/METADATA,sha256=fVvrn6ehZcS2z3CD_5E64vNVZwR6O6cSaYi1jNWkxNM,8058
|
|
8
|
-
clicodelog-0.1.0.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
|
|
9
|
-
clicodelog-0.1.0.dist-info/entry_points.txt,sha256=6Zodty_o0mVbDoIcNPQ31aI-P-KLZ0Xr_qsfrQkDYck,51
|
|
10
|
-
clicodelog-0.1.0.dist-info/top_level.txt,sha256=YvkoZJtBm4QSRL2YlLr5TeVILsiptIF7D43i29RG5oc,11
|
|
11
|
-
clicodelog-0.1.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|