doclogs-cli 0.1.1__tar.gz → 0.1.2__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.
- {doclogs_cli-0.1.1/doclogs_cli.egg-info → doclogs_cli-0.1.2}/PKG-INFO +1 -1
- {doclogs_cli-0.1.1 → doclogs_cli-0.1.2}/commands/capture.py +17 -3
- {doclogs_cli-0.1.1 → doclogs_cli-0.1.2/doclogs_cli.egg-info}/PKG-INFO +1 -1
- {doclogs_cli-0.1.1 → doclogs_cli-0.1.2}/helper/capture_prompts.py +14 -4
- {doclogs_cli-0.1.1 → doclogs_cli-0.1.2}/helper/story.py +26 -9
- {doclogs_cli-0.1.1 → doclogs_cli-0.1.2}/helper/syntax.py +3 -0
- {doclogs_cli-0.1.1 → doclogs_cli-0.1.2}/helper/task_notes.py +27 -6
- {doclogs_cli-0.1.1 → doclogs_cli-0.1.2}/helper/weekly.py +2 -2
- {doclogs_cli-0.1.1 → doclogs_cli-0.1.2}/pyproject.toml +1 -1
- {doclogs_cli-0.1.1 → doclogs_cli-0.1.2}/LICENSE +0 -0
- {doclogs_cli-0.1.1 → doclogs_cli-0.1.2}/README.md +0 -0
- {doclogs_cli-0.1.1 → doclogs_cli-0.1.2}/commands/__init__.py +0 -0
- {doclogs_cli-0.1.1 → doclogs_cli-0.1.2}/commands/config.py +0 -0
- {doclogs_cli-0.1.1 → doclogs_cli-0.1.2}/commands/generate.py +0 -0
- {doclogs_cli-0.1.1 → doclogs_cli-0.1.2}/commands/publish.py +0 -0
- {doclogs_cli-0.1.1 → doclogs_cli-0.1.2}/commands/sanitize.py +0 -0
- {doclogs_cli-0.1.1 → doclogs_cli-0.1.2}/commands/weekly.py +0 -0
- {doclogs_cli-0.1.1 → doclogs_cli-0.1.2}/doclogs_cli.egg-info/SOURCES.txt +0 -0
- {doclogs_cli-0.1.1 → doclogs_cli-0.1.2}/doclogs_cli.egg-info/dependency_links.txt +0 -0
- {doclogs_cli-0.1.1 → doclogs_cli-0.1.2}/doclogs_cli.egg-info/entry_points.txt +0 -0
- {doclogs_cli-0.1.1 → doclogs_cli-0.1.2}/doclogs_cli.egg-info/requires.txt +0 -0
- {doclogs_cli-0.1.1 → doclogs_cli-0.1.2}/doclogs_cli.egg-info/top_level.txt +0 -0
- {doclogs_cli-0.1.1 → doclogs_cli-0.1.2}/helper/__init__.py +0 -0
- {doclogs_cli-0.1.1 → doclogs_cli-0.1.2}/helper/capture.py +0 -0
- {doclogs_cli-0.1.1 → doclogs_cli-0.1.2}/helper/entry.py +0 -0
- {doclogs_cli-0.1.1 → doclogs_cli-0.1.2}/helper/generate.py +0 -0
- {doclogs_cli-0.1.1 → doclogs_cli-0.1.2}/helper/git_collector.py +0 -0
- {doclogs_cli-0.1.1 → doclogs_cli-0.1.2}/helper/llm/__init__.py +0 -0
- {doclogs_cli-0.1.1 → doclogs_cli-0.1.2}/helper/llm/base.py +0 -0
- {doclogs_cli-0.1.1 → doclogs_cli-0.1.2}/helper/llm/config.py +0 -0
- {doclogs_cli-0.1.1 → doclogs_cli-0.1.2}/helper/llm/copilot_cli.py +0 -0
- {doclogs_cli-0.1.1 → doclogs_cli-0.1.2}/helper/llm/cursor_cli.py +0 -0
- {doclogs_cli-0.1.1 → doclogs_cli-0.1.2}/helper/llm/prompt_only.py +0 -0
- {doclogs_cli-0.1.1 → doclogs_cli-0.1.2}/helper/llm/registry.py +0 -0
- {doclogs_cli-0.1.1 → doclogs_cli-0.1.2}/helper/paths.py +0 -0
- {doclogs_cli-0.1.1 → doclogs_cli-0.1.2}/helper/prompts.py +0 -0
- {doclogs_cli-0.1.1 → doclogs_cli-0.1.2}/helper/publish_git.py +0 -0
- {doclogs_cli-0.1.1 → doclogs_cli-0.1.2}/helper/sanitize.py +0 -0
- {doclogs_cli-0.1.1 → doclogs_cli-0.1.2}/helper/templates/blog.md +0 -0
- {doclogs_cli-0.1.1 → doclogs_cli-0.1.2}/helper/templates/changelog.md +0 -0
- {doclogs_cli-0.1.1 → doclogs_cli-0.1.2}/helper/templates/config.yaml +0 -0
- {doclogs_cli-0.1.1 → doclogs_cli-0.1.2}/helper/templates/interview.md +0 -0
- {doclogs_cli-0.1.1 → doclogs_cli-0.1.2}/helper/templates/linkedin.md +0 -0
- {doclogs_cli-0.1.1 → doclogs_cli-0.1.2}/helper/templates/resume.md +0 -0
- {doclogs_cli-0.1.1 → doclogs_cli-0.1.2}/main.py +0 -0
- {doclogs_cli-0.1.1 → doclogs_cli-0.1.2}/setup.cfg +0 -0
|
@@ -14,11 +14,25 @@ def _print_summary(entry, path) -> None:
|
|
|
14
14
|
typer.echo(f" commits today: {len(entry.commits)}")
|
|
15
15
|
|
|
16
16
|
|
|
17
|
+
def _notes_with_topic(notes: str, topic: str | None) -> str:
|
|
18
|
+
if not topic or not topic.strip():
|
|
19
|
+
return notes
|
|
20
|
+
topic_line = f"topic: {topic.strip()}"
|
|
21
|
+
if notes.lstrip().startswith("topic:"):
|
|
22
|
+
return notes
|
|
23
|
+
return f"{topic_line}\n{notes}"
|
|
24
|
+
|
|
25
|
+
|
|
17
26
|
def register(app: typer.Typer):
|
|
18
27
|
|
|
19
28
|
@app.command("capture", help="Capture today's engineering work into local storage.")
|
|
20
29
|
def capture(
|
|
21
30
|
notes: Optional[str] = typer.Option(None, "-n", "--notes", help="Optional notes (skips interactive prompts)."),
|
|
31
|
+
topic: Optional[str] = typer.Option(
|
|
32
|
+
None,
|
|
33
|
+
"--topic",
|
|
34
|
+
help="Short task name (used in weekly review and doclog generate -t).",
|
|
35
|
+
),
|
|
22
36
|
no_interactive: bool = typer.Option(False, "--no-interactive", help="Skip questions; git-only capture."),
|
|
23
37
|
include_terminal: bool = typer.Option(False, help="Include optional terminal history evidence."),
|
|
24
38
|
include_tickets: bool = typer.Option(False, help="Include optional ticket IDs or issue references."),
|
|
@@ -34,7 +48,7 @@ def register(app: typer.Typer):
|
|
|
34
48
|
entry = None
|
|
35
49
|
saved_any = False
|
|
36
50
|
|
|
37
|
-
for batch, replace in iter_interactive_tasks():
|
|
51
|
+
for batch, replace in iter_interactive_tasks(topic=topic):
|
|
38
52
|
entry = build_capture_entry(notes=batch, replace_notes=replace)
|
|
39
53
|
path = save_entry(entry)
|
|
40
54
|
saved_any = True
|
|
@@ -48,7 +62,7 @@ def register(app: typer.Typer):
|
|
|
48
62
|
return
|
|
49
63
|
|
|
50
64
|
# Non-interactive: git only or single --notes
|
|
51
|
-
final_notes = merge_notes(notes)
|
|
65
|
+
final_notes = merge_notes(_notes_with_topic(notes, topic) if notes else None)
|
|
52
66
|
entry = build_capture_entry(notes=final_notes)
|
|
53
67
|
path = save_entry(entry)
|
|
54
|
-
_print_summary(entry, path)
|
|
68
|
+
_print_summary(entry, path)
|
|
@@ -59,28 +59,38 @@ def _next_task_number() -> int:
|
|
|
59
59
|
return max(nums, default=0) + 1
|
|
60
60
|
|
|
61
61
|
|
|
62
|
-
def iter_interactive_tasks():
|
|
62
|
+
def iter_interactive_tasks(topic: str | None = None):
|
|
63
63
|
updated = maybe_complete_stub()
|
|
64
64
|
if updated:
|
|
65
65
|
yield updated, True # ← replace entire notes
|
|
66
66
|
task_num = _next_task_number()
|
|
67
|
+
first_task = True
|
|
67
68
|
while True:
|
|
68
69
|
typer.echo(f"\n--- Task {task_num} ---\n")
|
|
69
|
-
|
|
70
|
+
default_topic = topic if first_task else None
|
|
71
|
+
batch = collect_interactive_notes(default_topic=default_topic)
|
|
72
|
+
first_task = False
|
|
70
73
|
if batch:
|
|
71
74
|
yield f"### Task {task_num}\n{batch}", False # ← append new task
|
|
72
75
|
if not typer.confirm("Add another task?", default=False):
|
|
73
76
|
break
|
|
74
77
|
task_num += 1
|
|
75
78
|
|
|
76
|
-
def collect_interactive_notes() -> str | None:
|
|
79
|
+
def collect_interactive_notes(default_topic: str | None = None) -> str | None:
|
|
77
80
|
typer.echo("\n📋 Daily capture — answer briefly (Enter to skip optional questions)\n")
|
|
78
81
|
|
|
82
|
+
topic_answer = typer.prompt(
|
|
83
|
+
"Task topic (short name for weekly & generate)",
|
|
84
|
+
default=default_topic or "",
|
|
85
|
+
).strip()
|
|
79
86
|
worked_on = typer.prompt("What did you work on today?", default="").strip()
|
|
80
87
|
if not worked_on:
|
|
81
88
|
return None
|
|
82
89
|
|
|
83
|
-
lines = [
|
|
90
|
+
lines: list[str] = []
|
|
91
|
+
if topic_answer:
|
|
92
|
+
lines.append(f"topic: {topic_answer}")
|
|
93
|
+
lines.append(f"worked_on: {worked_on}")
|
|
84
94
|
|
|
85
95
|
if typer.confirm("Add more details later?", default=False):
|
|
86
96
|
lines.append("status: details_later")
|
|
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import re
|
|
4
4
|
|
|
5
|
-
from helper.task_notes import extract_worked_on, normalize_title, parse_task_blocks
|
|
5
|
+
from helper.task_notes import extract_topic, extract_worked_on, normalize_title, parse_task_blocks, story_title
|
|
6
6
|
from helper.weekly import build_story_candidates, load_entries_for_days
|
|
7
7
|
|
|
8
8
|
_WEEKLY_DATE_SUFFIX = re.compile(r"\s+\(\d{4}-\d{2}-\d{2}\)\s*$")
|
|
@@ -53,14 +53,16 @@ def find_story_text(title: str, days: int = 7) -> str | None:
|
|
|
53
53
|
)
|
|
54
54
|
|
|
55
55
|
for block in parse_task_blocks(entry.notes):
|
|
56
|
-
|
|
57
|
-
if not
|
|
56
|
+
titles = _task_titles(block)
|
|
57
|
+
if not titles:
|
|
58
58
|
continue
|
|
59
|
-
candidate
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
59
|
+
for candidate in titles:
|
|
60
|
+
normalized = normalize_title(candidate)
|
|
61
|
+
if needle == normalized:
|
|
62
|
+
return block
|
|
63
|
+
if _is_partial_match(needle, normalized):
|
|
64
|
+
partial_matches.append(block)
|
|
65
|
+
break
|
|
64
66
|
|
|
65
67
|
if len(partial_matches) == 1:
|
|
66
68
|
return partial_matches[0]
|
|
@@ -68,6 +70,21 @@ def find_story_text(title: str, days: int = 7) -> str | None:
|
|
|
68
70
|
|
|
69
71
|
|
|
70
72
|
def _is_partial_match(needle: str, candidate: str) -> bool:
|
|
71
|
-
if len(needle) <
|
|
73
|
+
if len(needle) < 3:
|
|
72
74
|
return False
|
|
73
75
|
return needle in candidate or candidate in needle
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _task_titles(block: str) -> list[str]:
|
|
79
|
+
titles: list[str] = []
|
|
80
|
+
topic = extract_topic(block)
|
|
81
|
+
worked_on = extract_worked_on(block)
|
|
82
|
+
if topic:
|
|
83
|
+
titles.append(topic)
|
|
84
|
+
if worked_on and worked_on not in titles:
|
|
85
|
+
titles.append(worked_on)
|
|
86
|
+
if not titles:
|
|
87
|
+
fallback = story_title(block)
|
|
88
|
+
if fallback:
|
|
89
|
+
titles.append(fallback)
|
|
90
|
+
return titles
|
|
@@ -37,6 +37,7 @@ SYNTAX: dict[str, CommandSyntax] = {
|
|
|
37
37
|
usage="doclog capture [OPTIONS]",
|
|
38
38
|
options=(
|
|
39
39
|
"-n, --notes TEXT Notes text (skips interactive prompts)",
|
|
40
|
+
"--topic TEXT Short task name (weekly & generate -t)",
|
|
40
41
|
"--no-interactive Git-only capture; skip questions",
|
|
41
42
|
"--include-terminal Include terminal history (planned)",
|
|
42
43
|
"--include-tickets Include ticket IDs (planned)",
|
|
@@ -44,6 +45,8 @@ SYNTAX: dict[str, CommandSyntax] = {
|
|
|
44
45
|
),
|
|
45
46
|
examples=(
|
|
46
47
|
"doclog capture",
|
|
48
|
+
"doclog capture --topic \"nginx akamai TLS\"",
|
|
49
|
+
'doclog capture --topic "DocLogs" -n "worked_on: shipped publish command"',
|
|
47
50
|
"doclog capture --no-interactive",
|
|
48
51
|
'doclog capture -n "Fixed nginx TLS handshake with Akamai"',
|
|
49
52
|
"doclog capture --syntax",
|
|
@@ -17,6 +17,19 @@ def extract_worked_on(block: str) -> str | None:
|
|
|
17
17
|
return _extract_worked_on(block)
|
|
18
18
|
|
|
19
19
|
|
|
20
|
+
def extract_topic(block: str) -> str | None:
|
|
21
|
+
return _extract_field(
|
|
22
|
+
block,
|
|
23
|
+
"topic",
|
|
24
|
+
stop_fields=("worked_on", "impact", "blockers", "remember", "status", "### Task"),
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def story_title(block: str) -> str | None:
|
|
29
|
+
"""Short label for weekly/generate; prefers topic over worked_on."""
|
|
30
|
+
return extract_topic(block) or extract_worked_on(block)
|
|
31
|
+
|
|
32
|
+
|
|
20
33
|
def normalize_title(text: str) -> str:
|
|
21
34
|
"""Collapse whitespace and lowercase for fuzzy title matching."""
|
|
22
35
|
return " ".join(text.strip().lower().split())
|
|
@@ -33,21 +46,27 @@ def list_incomplete_tasks(notes: str | None) -> list[tuple[int, str]]:
|
|
|
33
46
|
continue
|
|
34
47
|
match = re.search(r"### Task (\d+)", block)
|
|
35
48
|
task_num = int(match.group(1)) if match else 0
|
|
36
|
-
summary =
|
|
49
|
+
summary = story_title(block) or (f"Task {task_num}" if task_num else "Incomplete entry")
|
|
37
50
|
results.append((task_num, summary))
|
|
38
51
|
|
|
39
52
|
return results
|
|
40
53
|
|
|
41
54
|
|
|
42
55
|
def _extract_worked_on(block: str) -> str | None:
|
|
43
|
-
|
|
44
|
-
r"worked_on:\s*(.+?)(?=\n(?:impact|blockers|remember|status:|### Task|\Z))",
|
|
56
|
+
return _extract_field(
|
|
45
57
|
block,
|
|
46
|
-
|
|
58
|
+
"worked_on",
|
|
59
|
+
stop_fields=("topic", "impact", "blockers", "remember", "status", "### Task"),
|
|
47
60
|
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _extract_field(block: str, field: str, *, stop_fields: tuple[str, ...]) -> str | None:
|
|
64
|
+
stops = "|".join(re.escape(name) for name in stop_fields)
|
|
65
|
+
pattern = rf"{field}:\s*(.+?)(?=\n(?:{stops}|\Z))"
|
|
66
|
+
match = re.search(pattern, block, re.DOTALL)
|
|
48
67
|
if not match:
|
|
49
68
|
return None
|
|
50
|
-
return " ".join(match.group(1).split())
|
|
69
|
+
return " ".join(match.group(1).split())
|
|
51
70
|
|
|
52
71
|
|
|
53
72
|
def complete_task(notes: str, task_num: int, followup: str) -> str:
|
|
@@ -61,7 +80,9 @@ def complete_task(notes: str, task_num: int, followup: str) -> str:
|
|
|
61
80
|
if not match:
|
|
62
81
|
raise ValueError(f"Task {task_num} not found")
|
|
63
82
|
worked_on = _extract_worked_on(match.group(0)) or ""
|
|
64
|
-
|
|
83
|
+
topic = extract_topic(match.group(0))
|
|
84
|
+
topic_line = f"topic: {topic}\n" if topic else ""
|
|
85
|
+
new_block = f"### Task {task_num}\n{topic_line}worked_on: {worked_on}\n{followup}"
|
|
65
86
|
|
|
66
87
|
match = re.search(pattern, notes, re.DOTALL)
|
|
67
88
|
if not match:
|
|
@@ -5,7 +5,7 @@ from datetime import date, timedelta
|
|
|
5
5
|
|
|
6
6
|
from helper.entry import DailyEntry, load_entry
|
|
7
7
|
from helper.paths import entries_dir
|
|
8
|
-
from helper.task_notes import DETAILS_LATER,
|
|
8
|
+
from helper.task_notes import DETAILS_LATER, parse_task_blocks, story_title
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
@dataclass
|
|
@@ -45,7 +45,7 @@ def build_story_candidates(
|
|
|
45
45
|
)
|
|
46
46
|
|
|
47
47
|
for block in parse_task_blocks(entry.notes):
|
|
48
|
-
title =
|
|
48
|
+
title = story_title(block)
|
|
49
49
|
if not title:
|
|
50
50
|
continue
|
|
51
51
|
key = title.lower()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|