chronicle-log 0.1.0__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.
@@ -0,0 +1,40 @@
1
+ Metadata-Version: 2.4
2
+ Name: chronicle-log
3
+ Version: 0.1.0
4
+ Summary: A CLI logbook for founders, builders, and creators.
5
+ Author-email: Your Name <you@example.com>
6
+ License: MIT
7
+ Requires-Python: >=3.9
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: typer>=0.9
10
+ Requires-Dist: rich>=13
11
+
12
+ # Chronicle 📖
13
+
14
+ > Log the journey. Tell the story.
15
+
16
+ Chronicle is a minimal CLI logbook built for founders, builders, and solo makers.
17
+ Log your ideas, wins, failures, and progress — with accurate timestamps — as you build.
18
+
19
+ ## Install
20
+
21
+ pip install chronicle-log
22
+
23
+ ## Usage
24
+
25
+ chronicle log "Had a breakthrough on the auth flow" -c build
26
+ chronicle idea "What if we added voice logging via whisper?"
27
+ chronicle win "First real user signed up"
28
+ chronicle fail "Shipped a bug that deleted sessions. Lesson: always backup."
29
+ chronicle show --limit 20
30
+ chronicle stats
31
+ chronicle export --format markdown
32
+
33
+ ## Why?
34
+
35
+ Built by a founder, for founders. Every entry you make today
36
+ becomes the raw material of the story you'll tell tomorrow.
37
+
38
+ ## License
39
+
40
+ MIT — free forever.
@@ -0,0 +1,29 @@
1
+ # Chronicle 📖
2
+
3
+ > Log the journey. Tell the story.
4
+
5
+ Chronicle is a minimal CLI logbook built for founders, builders, and solo makers.
6
+ Log your ideas, wins, failures, and progress — with accurate timestamps — as you build.
7
+
8
+ ## Install
9
+
10
+ pip install chronicle-log
11
+
12
+ ## Usage
13
+
14
+ chronicle log "Had a breakthrough on the auth flow" -c build
15
+ chronicle idea "What if we added voice logging via whisper?"
16
+ chronicle win "First real user signed up"
17
+ chronicle fail "Shipped a bug that deleted sessions. Lesson: always backup."
18
+ chronicle show --limit 20
19
+ chronicle stats
20
+ chronicle export --format markdown
21
+
22
+ ## Why?
23
+
24
+ Built by a founder, for founders. Every entry you make today
25
+ becomes the raw material of the story you'll tell tomorrow.
26
+
27
+ ## License
28
+
29
+ MIT — free forever.
@@ -0,0 +1,3 @@
1
+ # chronicle/__init__.py
2
+ __version__ = "0.1.0"
3
+ __author__ = "Pratyay Mukherjee"
@@ -0,0 +1,78 @@
1
+ # chronicle/db.py
2
+ import sqlite3
3
+ import os
4
+ from pathlib import Path
5
+ from datetime import datetime
6
+
7
+ DB_DIR = Path.home() / ".chronicle"
8
+ DB_PATH = DB_DIR / "logbook.db"
9
+
10
+ def get_connection():
11
+ DB_DIR.mkdir(exist_ok=True)
12
+ conn = sqlite3.connect(DB_PATH)
13
+ conn.row_factory = sqlite3.Row
14
+ return conn
15
+
16
+ def init_db():
17
+ conn = get_connection()
18
+ conn.executescript("""
19
+ CREATE TABLE IF NOT EXISTS entries (
20
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
21
+ timestamp TEXT NOT NULL,
22
+ category TEXT NOT NULL DEFAULT 'general',
23
+ title TEXT,
24
+ content TEXT NOT NULL,
25
+ mood TEXT,
26
+ tags TEXT
27
+ );
28
+ CREATE INDEX IF NOT EXISTS idx_timestamp ON entries(timestamp);
29
+ CREATE INDEX IF NOT EXISTS idx_category ON entries(category);
30
+ """)
31
+ conn.commit()
32
+ conn.close()
33
+
34
+ def add_entry(content: str, category: str = "general",
35
+ title: str = None, mood: str = None, tags: list = None):
36
+ conn = get_connection()
37
+ timestamp = datetime.now().isoformat(sep=" ", timespec="seconds")
38
+ tags_str = ",".join(tags) if tags else None
39
+ conn.execute(
40
+ "INSERT INTO entries (timestamp, category, title, content, mood, tags) "
41
+ "VALUES (?, ?, ?, ?, ?, ?)",
42
+ (timestamp, category, title, content, mood, tags_str)
43
+ )
44
+ conn.commit()
45
+ conn.close()
46
+
47
+ def get_entries(limit: int = 20, category: str = None,
48
+ search: str = None, date: str = None):
49
+ conn = get_connection()
50
+ query = "SELECT * FROM entries WHERE 1=1"
51
+ params = []
52
+ if category:
53
+ query += " AND category = ?"
54
+ params.append(category)
55
+ if search:
56
+ query += " AND (content LIKE ? OR title LIKE ? OR tags LIKE ?)"
57
+ params += [f"%{search}%", f"%{search}%", f"%{search}%"]
58
+ if date:
59
+ query += " AND timestamp LIKE ?"
60
+ params.append(f"{date}%")
61
+ query += " ORDER BY timestamp DESC LIMIT ?"
62
+ params.append(limit)
63
+ rows = conn.execute(query, params).fetchall()
64
+ conn.close()
65
+ return rows
66
+
67
+ def get_stats():
68
+ conn = get_connection()
69
+ stats = {}
70
+ stats["total"] = conn.execute("SELECT COUNT(*) FROM entries").fetchone()[0]
71
+ stats["by_category"] = conn.execute(
72
+ "SELECT category, COUNT(*) as n FROM entries GROUP BY category"
73
+ ).fetchall()
74
+ stats["first_entry"] = conn.execute(
75
+ "SELECT timestamp FROM entries ORDER BY timestamp ASC LIMIT 1"
76
+ ).fetchone()
77
+ conn.close()
78
+ return stats
@@ -0,0 +1,38 @@
1
+ # chronicle/export.py
2
+ from datetime import datetime
3
+ import json
4
+ from pathlib import Path
5
+
6
+ def export_markdown(entries, filename: str = "chronicle_export") -> str:
7
+ """Export all entries as a Markdown document — book-ready format."""
8
+ lines = [
9
+ "# Chronicle — Founder's Logbook\n",
10
+ f"*Exported: {datetime.now().strftime('%B %d, %Y at %H:%M')}*\n",
11
+ "---\n"
12
+ ]
13
+ current_date = None
14
+ for e in reversed(list(entries)): # Chronological order for export
15
+ entry_date = e["timestamp"][:10]
16
+ if entry_date != current_date:
17
+ current_date = entry_date
18
+ lines.append(f"\n## {entry_date}\n")
19
+ if e["title"]:
20
+ lines.append(f"### {e['title']}\n")
21
+ lines.append(f"**[{e['category'].upper()}]** — *{e['timestamp'][11:]}*\n")
22
+ if e["mood"]:
23
+ lines.append(f"*Mood: {e['mood']}*\n")
24
+ lines.append(f"\n{e['content']}\n")
25
+ if e["tags"]:
26
+ lines.append(f"\n*Tags: {e['tags']}*\n")
27
+ lines.append("\n---\n")
28
+
29
+ path = Path(f"{filename}.md")
30
+ path.write_text("\n".join(lines), encoding="utf-8")
31
+ return str(path)
32
+
33
+ def export_json(entries, filename: str = "chronicle_export") -> str:
34
+ """Export as structured JSON."""
35
+ data = [dict(e) for e in entries]
36
+ path = Path(f"{filename}.json")
37
+ path.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8")
38
+ return str(path)
@@ -0,0 +1,140 @@
1
+ # chronicle/main.py
2
+ import typer
3
+ from rich.console import Console
4
+ from rich.table import Table
5
+ from rich.panel import Panel
6
+ from rich.text import Text
7
+ from typing import Optional, List
8
+ from chronicle.db import init_db, add_entry, get_entries, get_stats
9
+ from chronicle.export import export_markdown, export_json
10
+
11
+ app = typer.Typer(
12
+ name="chronicle",
13
+ help="📖 Chronicle — Log your founder journey, one entry at a time.",
14
+ add_completion=False
15
+ )
16
+ console = Console()
17
+
18
+ CATEGORIES = ["idea", "build", "learn", "fail", "win", "research", "general"]
19
+ MOODS = ["🔥 fire", "😤 grind", "😌 calm", "😩 stuck", "🎯 focused", "🤔 thinking"]
20
+
21
+ @app.callback(invoke_without_command=True)
22
+ def startup(ctx: typer.Context):
23
+ init_db()
24
+ if ctx.invoked_subcommand is None:
25
+ console.print(Panel(
26
+ "[bold cyan]Chronicle[/bold cyan] — Your founder's logbook\n"
27
+ "[dim]Run [bold]chronicle --help[/bold] to see all commands[/dim]",
28
+ border_style="cyan"
29
+ ))
30
+
31
+ @app.command("log")
32
+ def log_entry(
33
+ content: str = typer.Argument(..., help="Your log entry"),
34
+ category: str = typer.Option("general", "-c", "--category",
35
+ help=f"Category: {', '.join(CATEGORIES)}"),
36
+ title: Optional[str] = typer.Option(None, "-t", "--title", help="Optional title"),
37
+ mood: Optional[str] = typer.Option(None, "-m", "--mood", help="Your mood/energy"),
38
+ tags: Optional[List[str]] = typer.Option(None, "--tag", help="Add tags (repeat for multiple)")
39
+ ):
40
+ """📝 Log a new entry."""
41
+ add_entry(content, category=category, title=title, mood=mood, tags=tags)
42
+ console.print(f"[green]✓[/green] Entry logged under [bold]{category}[/bold]")
43
+
44
+ @app.command("idea")
45
+ def log_idea(content: str = typer.Argument(..., help="Your idea")):
46
+ """💡 Quick-log an idea."""
47
+ add_entry(content, category="idea")
48
+ console.print("[yellow]💡[/yellow] Idea captured.")
49
+
50
+ @app.command("win")
51
+ def log_win(content: str = typer.Argument(..., help="What did you achieve?")):
52
+ """🏆 Log a win, no matter how small."""
53
+ add_entry(content, category="win")
54
+ console.print("[bold green]🏆 WIN logged. Keep going.[/bold green]")
55
+
56
+ @app.command("fail")
57
+ def log_fail(content: str = typer.Argument(..., help="What went wrong?")):
58
+ """📉 Log a failure or lesson learned."""
59
+ add_entry(content, category="fail")
60
+ console.print("[dim red]📉 Failure logged. This is data, not defeat.[/dim red]")
61
+
62
+ @app.command("show")
63
+ def show_entries(
64
+ limit: int = typer.Option(10, "-n", "--limit", help="Number of entries to show"),
65
+ category: Optional[str] = typer.Option(None, "-c", "--category"),
66
+ search: Optional[str] = typer.Option(None, "-s", "--search"),
67
+ date: Optional[str] = typer.Option(None, "-d", "--date", help="Filter by date: YYYY-MM-DD")
68
+ ):
69
+ """📋 Show your log entries."""
70
+ entries = get_entries(limit=limit, category=category, search=search, date=date)
71
+ if not entries:
72
+ console.print("[dim]No entries found.[/dim]")
73
+ return
74
+
75
+ table = Table(show_header=True, header_style="bold cyan",
76
+ border_style="dim", expand=True)
77
+ table.add_column("ID", style="dim", width=4)
78
+ table.add_column("Timestamp", style="dim", width=20)
79
+ table.add_column("Category", width=10)
80
+ table.add_column("Title / Content")
81
+ table.add_column("Tags", style="dim", width=20)
82
+
83
+ CATEGORY_COLORS = {
84
+ "idea": "yellow", "build": "cyan", "learn": "blue",
85
+ "fail": "red", "win": "green", "research": "magenta", "general": "white"
86
+ }
87
+
88
+ for e in entries:
89
+ color = CATEGORY_COLORS.get(e["category"], "white")
90
+ display = e["title"] if e["title"] else (
91
+ e["content"][:70] + "..." if len(e["content"]) > 70 else e["content"]
92
+ )
93
+ table.add_row(
94
+ str(e["id"]),
95
+ e["timestamp"],
96
+ f"[{color}]{e['category']}[/{color}]",
97
+ display,
98
+ e["tags"] or ""
99
+ )
100
+ console.print(table)
101
+
102
+ @app.command("stats")
103
+ def show_stats():
104
+ """📊 Show your journey statistics."""
105
+ stats = get_stats()
106
+ first = stats["first_entry"]["timestamp"] if stats["first_entry"] else "N/A"
107
+ console.print(Panel(
108
+ f"[bold]Total Entries:[/bold] {stats['total']}\n"
109
+ f"[bold]Journey Started:[/bold] {first}",
110
+ title="[bold cyan]Chronicle Stats[/bold cyan]",
111
+ border_style="cyan"
112
+ ))
113
+ if stats["by_category"]:
114
+ table = Table(show_header=True, header_style="bold", border_style="dim")
115
+ table.add_column("Category")
116
+ table.add_column("Entries", justify="right")
117
+ for row in stats["by_category"]:
118
+ table.add_row(row["category"], str(row["n"]))
119
+ console.print(table)
120
+
121
+ @app.command("export")
122
+ def export_entries(
123
+ format: str = typer.Option("markdown", "-f", "--format",
124
+ help="Export format: markdown | json"),
125
+ output: str = typer.Option("chronicle_export", "-o", "--output",
126
+ help="Output filename (no extension)")
127
+ ):
128
+ """📤 Export your log to Markdown or JSON."""
129
+ entries = get_entries(limit=99999)
130
+ if format == "markdown":
131
+ path = export_markdown(entries, output)
132
+ elif format == "json":
133
+ path = export_json(entries, output)
134
+ else:
135
+ console.print("[red]Unknown format. Use 'markdown' or 'json'[/red]")
136
+ return
137
+ console.print(f"[green]✓[/green] Exported to [bold]{path}[/bold]")
138
+
139
+ if __name__ == "__main__":
140
+ app()
@@ -0,0 +1,40 @@
1
+ Metadata-Version: 2.4
2
+ Name: chronicle-log
3
+ Version: 0.1.0
4
+ Summary: A CLI logbook for founders, builders, and creators.
5
+ Author-email: Your Name <you@example.com>
6
+ License: MIT
7
+ Requires-Python: >=3.9
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: typer>=0.9
10
+ Requires-Dist: rich>=13
11
+
12
+ # Chronicle 📖
13
+
14
+ > Log the journey. Tell the story.
15
+
16
+ Chronicle is a minimal CLI logbook built for founders, builders, and solo makers.
17
+ Log your ideas, wins, failures, and progress — with accurate timestamps — as you build.
18
+
19
+ ## Install
20
+
21
+ pip install chronicle-log
22
+
23
+ ## Usage
24
+
25
+ chronicle log "Had a breakthrough on the auth flow" -c build
26
+ chronicle idea "What if we added voice logging via whisper?"
27
+ chronicle win "First real user signed up"
28
+ chronicle fail "Shipped a bug that deleted sessions. Lesson: always backup."
29
+ chronicle show --limit 20
30
+ chronicle stats
31
+ chronicle export --format markdown
32
+
33
+ ## Why?
34
+
35
+ Built by a founder, for founders. Every entry you make today
36
+ becomes the raw material of the story you'll tell tomorrow.
37
+
38
+ ## License
39
+
40
+ MIT — free forever.
@@ -0,0 +1,13 @@
1
+ README.md
2
+ pyproject.toml
3
+ chronicle/__init__.py
4
+ chronicle/db.py
5
+ chronicle/export.py
6
+ chronicle/main.py
7
+ chronicle_log.egg-info/PKG-INFO
8
+ chronicle_log.egg-info/SOURCES.txt
9
+ chronicle_log.egg-info/dependency_links.txt
10
+ chronicle_log.egg-info/entry_points.txt
11
+ chronicle_log.egg-info/requires.txt
12
+ chronicle_log.egg-info/top_level.txt
13
+ tests/test_db.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ chronicle = chronicle.main:app
@@ -0,0 +1,2 @@
1
+ typer>=0.9
2
+ rich>=13
@@ -0,0 +1 @@
1
+ chronicle
@@ -0,0 +1,21 @@
1
+ # pyproject.toml
2
+ [build-system]
3
+ requires = ["setuptools>=68", "wheel"]
4
+ build-backend = "setuptools.build_meta"
5
+
6
+ [project]
7
+ name = "chronicle-log"
8
+ version = "0.1.0"
9
+ description = "A CLI logbook for founders, builders, and creators."
10
+ authors = [{ name = "Your Name", email = "you@example.com" }]
11
+ license = { text = "MIT" }
12
+ requires-python = ">=3.9"
13
+ dependencies = ["typer>=0.9", "rich>=13"]
14
+ readme = "README.md"
15
+
16
+ [project.scripts]
17
+ chronicle = "chronicle.main:app"
18
+
19
+ [tool.setuptools.packages.find]
20
+ where = ["."]
21
+ include = ["chronicle*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,26 @@
1
+ # tests/test_db.py
2
+ import os, pytest
3
+ from pathlib import Path
4
+
5
+ # Override DB path for tests
6
+ import chronicle.db as db
7
+ db.DB_PATH = Path("/tmp/test_chronicle.db")
8
+
9
+ from chronicle.db import init_db, add_entry, get_entries
10
+
11
+ def setup_function():
12
+ if db.DB_PATH.exists():
13
+ db.DB_PATH.unlink()
14
+ init_db()
15
+
16
+ def test_add_and_retrieve():
17
+ add_entry("Test entry", category="build")
18
+ entries = get_entries(limit=1)
19
+ assert len(entries) == 1
20
+ assert entries[0]["content"] == "Test entry"
21
+ assert entries[0]["category"] == "build"
22
+
23
+ def test_search():
24
+ add_entry("Implemented OAuth", category="build", tags=["auth", "security"])
25
+ entries = get_entries(search="OAuth")
26
+ assert any("OAuth" in e["content"] for e in entries)