claude-code-tools 1.0.6__py3-none-any.whl → 1.4.6__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.
- claude_code_tools/__init__.py +1 -1
- claude_code_tools/action_rpc.py +16 -10
- claude_code_tools/aichat.py +793 -51
- claude_code_tools/claude_continue.py +4 -0
- claude_code_tools/codex_continue.py +48 -0
- claude_code_tools/export_session.py +94 -11
- claude_code_tools/find_claude_session.py +36 -12
- claude_code_tools/find_codex_session.py +33 -18
- claude_code_tools/find_session.py +30 -16
- claude_code_tools/gdoc2md.py +220 -0
- claude_code_tools/md2gdoc.py +549 -0
- claude_code_tools/search_index.py +119 -15
- claude_code_tools/session_menu_cli.py +1 -1
- claude_code_tools/session_utils.py +3 -3
- claude_code_tools/smart_trim.py +18 -8
- claude_code_tools/smart_trim_core.py +4 -2
- claude_code_tools/tmux_cli_controller.py +35 -25
- claude_code_tools/trim_session.py +28 -2
- claude_code_tools-1.4.6.dist-info/METADATA +1112 -0
- {claude_code_tools-1.0.6.dist-info → claude_code_tools-1.4.6.dist-info}/RECORD +31 -24
- {claude_code_tools-1.0.6.dist-info → claude_code_tools-1.4.6.dist-info}/entry_points.txt +2 -0
- docs/linked-in-20260102.md +32 -0
- docs/local-llm-setup.md +286 -0
- docs/reddit-aichat-resume-v2.md +80 -0
- docs/reddit-aichat-resume.md +29 -0
- docs/reddit-aichat.md +79 -0
- docs/rollover-details.md +67 -0
- node_ui/action_config.js +3 -3
- node_ui/menu.js +67 -113
- claude_code_tools/session_tui.py +0 -516
- claude_code_tools-1.0.6.dist-info/METADATA +0 -685
- {claude_code_tools-1.0.6.dist-info → claude_code_tools-1.4.6.dist-info}/WHEEL +0 -0
- {claude_code_tools-1.0.6.dist-info → claude_code_tools-1.4.6.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
gdoc2md: Download Google Docs as Markdown files.
|
|
4
|
+
|
|
5
|
+
This tool uses the Google Drive API to export Google Docs as Markdown,
|
|
6
|
+
using Google's native markdown export (same as File → Download → Markdown).
|
|
7
|
+
|
|
8
|
+
Prerequisites:
|
|
9
|
+
- First run: Will open browser for OAuth authentication (one-time setup)
|
|
10
|
+
- Credentials stored in .gdoc-credentials.json (local) or ~/.config/md2gdoc/
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Optional
|
|
17
|
+
|
|
18
|
+
from rich.console import Console
|
|
19
|
+
from rich.panel import Panel
|
|
20
|
+
|
|
21
|
+
console = Console()
|
|
22
|
+
|
|
23
|
+
# Import shared utilities from md2gdoc
|
|
24
|
+
from claude_code_tools.md2gdoc import (
|
|
25
|
+
SCOPES,
|
|
26
|
+
check_dependencies,
|
|
27
|
+
get_credentials,
|
|
28
|
+
get_drive_service,
|
|
29
|
+
find_folder_id,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def find_doc_by_name(
|
|
34
|
+
service, folder_id: Optional[str], doc_name: str
|
|
35
|
+
) -> Optional[dict]:
|
|
36
|
+
"""Find a Google Doc by name in a folder. Returns file metadata or None."""
|
|
37
|
+
parent = folder_id if folder_id else "root"
|
|
38
|
+
query = (
|
|
39
|
+
f"name = '{doc_name}' and "
|
|
40
|
+
f"'{parent}' in parents and "
|
|
41
|
+
f"mimeType = 'application/vnd.google-apps.document' and "
|
|
42
|
+
f"trashed = false"
|
|
43
|
+
)
|
|
44
|
+
results = (
|
|
45
|
+
service.files()
|
|
46
|
+
.list(q=query, fields="files(id, name)", pageSize=1)
|
|
47
|
+
.execute()
|
|
48
|
+
)
|
|
49
|
+
files = results.get("files", [])
|
|
50
|
+
return files[0] if files else None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def list_docs_in_folder(service, folder_id: Optional[str]) -> list[dict]:
|
|
54
|
+
"""List all Google Docs in a folder."""
|
|
55
|
+
parent = folder_id if folder_id else "root"
|
|
56
|
+
query = (
|
|
57
|
+
f"'{parent}' in parents and "
|
|
58
|
+
f"mimeType = 'application/vnd.google-apps.document' and "
|
|
59
|
+
f"trashed = false"
|
|
60
|
+
)
|
|
61
|
+
results = (
|
|
62
|
+
service.files()
|
|
63
|
+
.list(q=query, fields="files(id, name)", pageSize=100, orderBy="name")
|
|
64
|
+
.execute()
|
|
65
|
+
)
|
|
66
|
+
return results.get("files", [])
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def download_doc_as_markdown(service, file_id: str) -> Optional[str]:
|
|
70
|
+
"""Download a Google Doc as Markdown content."""
|
|
71
|
+
try:
|
|
72
|
+
# Export as markdown
|
|
73
|
+
content = (
|
|
74
|
+
service.files()
|
|
75
|
+
.export(fileId=file_id, mimeType="text/markdown")
|
|
76
|
+
.execute()
|
|
77
|
+
)
|
|
78
|
+
# Content is returned as bytes
|
|
79
|
+
if isinstance(content, bytes):
|
|
80
|
+
return content.decode("utf-8")
|
|
81
|
+
return content
|
|
82
|
+
except Exception as e:
|
|
83
|
+
console.print(f"[red]Export error:[/red] {e}")
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def main() -> None:
|
|
88
|
+
parser = argparse.ArgumentParser(
|
|
89
|
+
description="Download Google Docs as Markdown files.",
|
|
90
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
91
|
+
epilog="""
|
|
92
|
+
Examples:
|
|
93
|
+
gdoc2md "My Document" # Download from root
|
|
94
|
+
gdoc2md "My Document" --folder "OTA/Reports" # Download from folder
|
|
95
|
+
gdoc2md "My Document" -o report.md # Save with custom name
|
|
96
|
+
gdoc2md --list --folder OTA # List docs in folder
|
|
97
|
+
|
|
98
|
+
Credentials (in order of precedence):
|
|
99
|
+
1. .gdoc-token.json in current directory (project-specific)
|
|
100
|
+
2. ~/.config/md2gdoc/token.json (global)
|
|
101
|
+
3. Application Default Credentials (gcloud)
|
|
102
|
+
""",
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
parser.add_argument(
|
|
106
|
+
"doc_name",
|
|
107
|
+
type=str,
|
|
108
|
+
nargs="?",
|
|
109
|
+
help="Name of the Google Doc to download",
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
parser.add_argument(
|
|
113
|
+
"--folder",
|
|
114
|
+
"-f",
|
|
115
|
+
type=str,
|
|
116
|
+
default="",
|
|
117
|
+
help="Folder in Google Drive (e.g., 'OTA/Reports')",
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
parser.add_argument(
|
|
121
|
+
"--output",
|
|
122
|
+
"-o",
|
|
123
|
+
type=str,
|
|
124
|
+
default="",
|
|
125
|
+
help="Output filename (default: <doc_name>.md)",
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
parser.add_argument(
|
|
129
|
+
"--list",
|
|
130
|
+
"-l",
|
|
131
|
+
action="store_true",
|
|
132
|
+
help="List Google Docs in the folder instead of downloading",
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
args = parser.parse_args()
|
|
136
|
+
|
|
137
|
+
# Check dependencies
|
|
138
|
+
if not check_dependencies():
|
|
139
|
+
sys.exit(1)
|
|
140
|
+
|
|
141
|
+
# Need either doc_name or --list
|
|
142
|
+
if not args.doc_name and not args.list:
|
|
143
|
+
parser.print_help()
|
|
144
|
+
sys.exit(1)
|
|
145
|
+
|
|
146
|
+
# Get Drive service
|
|
147
|
+
service = get_drive_service()
|
|
148
|
+
if not service:
|
|
149
|
+
sys.exit(1)
|
|
150
|
+
|
|
151
|
+
# Find folder if specified
|
|
152
|
+
folder_id = None
|
|
153
|
+
if args.folder:
|
|
154
|
+
console.print(f"[dim]Finding folder: {args.folder}[/dim]")
|
|
155
|
+
folder_id = find_folder_id(service, args.folder, create_if_missing=False)
|
|
156
|
+
if folder_id is None:
|
|
157
|
+
console.print(f"[red]Error:[/red] Folder not found: {args.folder}")
|
|
158
|
+
sys.exit(1)
|
|
159
|
+
|
|
160
|
+
# List mode
|
|
161
|
+
if args.list:
|
|
162
|
+
docs = list_docs_in_folder(service, folder_id)
|
|
163
|
+
if not docs:
|
|
164
|
+
console.print("[yellow]No Google Docs found in this folder.[/yellow]")
|
|
165
|
+
sys.exit(0)
|
|
166
|
+
|
|
167
|
+
console.print(f"\n[bold]Google Docs in {args.folder or 'My Drive'}:[/bold]\n")
|
|
168
|
+
for doc in docs:
|
|
169
|
+
console.print(f" • {doc['name']}")
|
|
170
|
+
console.print(f"\n[dim]Total: {len(docs)} document(s)[/dim]")
|
|
171
|
+
sys.exit(0)
|
|
172
|
+
|
|
173
|
+
# Download mode
|
|
174
|
+
console.print(f"[dim]Looking for: {args.doc_name}[/dim]")
|
|
175
|
+
doc = find_doc_by_name(service, folder_id, args.doc_name)
|
|
176
|
+
|
|
177
|
+
if not doc:
|
|
178
|
+
console.print(f"[red]Error:[/red] Document not found: {args.doc_name}")
|
|
179
|
+
# Suggest listing
|
|
180
|
+
console.print(f"[dim]Use --list to see available documents[/dim]")
|
|
181
|
+
sys.exit(1)
|
|
182
|
+
|
|
183
|
+
# Download as markdown
|
|
184
|
+
console.print(f"[cyan]Downloading[/cyan] {doc['name']} → Markdown...")
|
|
185
|
+
content = download_doc_as_markdown(service, doc["id"])
|
|
186
|
+
|
|
187
|
+
if content is None:
|
|
188
|
+
sys.exit(1)
|
|
189
|
+
|
|
190
|
+
# Determine output filename
|
|
191
|
+
if args.output:
|
|
192
|
+
output_path = Path(args.output)
|
|
193
|
+
else:
|
|
194
|
+
# Use doc name, sanitize for filesystem
|
|
195
|
+
safe_name = "".join(c if c.isalnum() or c in "._- " else "_" for c in doc["name"])
|
|
196
|
+
output_path = Path(f"{safe_name}.md")
|
|
197
|
+
|
|
198
|
+
# Check if file exists
|
|
199
|
+
if output_path.exists():
|
|
200
|
+
console.print(
|
|
201
|
+
f"[yellow]Warning:[/yellow] {output_path} already exists, overwriting"
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
# Write file
|
|
205
|
+
output_path.write_text(content, encoding="utf-8")
|
|
206
|
+
|
|
207
|
+
console.print()
|
|
208
|
+
console.print(
|
|
209
|
+
Panel(
|
|
210
|
+
f"[green]Successfully downloaded![/green]\n\n"
|
|
211
|
+
f"[dim]Document:[/dim] {doc['name']}\n"
|
|
212
|
+
f"[dim]Saved to:[/dim] {output_path}",
|
|
213
|
+
title="Done",
|
|
214
|
+
border_style="green",
|
|
215
|
+
)
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
if __name__ == "__main__":
|
|
220
|
+
main()
|