doclogs-cli 0.1.0__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.0/doclogs_cli.egg-info → doclogs_cli-0.1.2}/PKG-INFO +50 -3
- {doclogs_cli-0.1.0 → doclogs_cli-0.1.2}/README.md +49 -2
- {doclogs_cli-0.1.0 → doclogs_cli-0.1.2}/commands/capture.py +20 -3
- doclogs_cli-0.1.2/commands/config.py +88 -0
- doclogs_cli-0.1.2/commands/generate.py +133 -0
- doclogs_cli-0.1.2/commands/publish.py +89 -0
- {doclogs_cli-0.1.0 → doclogs_cli-0.1.2}/commands/sanitize.py +3 -0
- {doclogs_cli-0.1.0 → doclogs_cli-0.1.2}/commands/weekly.py +6 -1
- {doclogs_cli-0.1.0 → doclogs_cli-0.1.2/doclogs_cli.egg-info}/PKG-INFO +50 -3
- {doclogs_cli-0.1.0 → doclogs_cli-0.1.2}/doclogs_cli.egg-info/SOURCES.txt +10 -0
- {doclogs_cli-0.1.0 → doclogs_cli-0.1.2}/helper/capture_prompts.py +14 -4
- doclogs_cli-0.1.2/helper/generate.py +67 -0
- doclogs_cli-0.1.2/helper/llm/__init__.py +10 -0
- doclogs_cli-0.1.2/helper/llm/base.py +23 -0
- doclogs_cli-0.1.2/helper/llm/config.py +99 -0
- doclogs_cli-0.1.2/helper/llm/copilot_cli.py +79 -0
- doclogs_cli-0.1.2/helper/llm/cursor_cli.py +68 -0
- doclogs_cli-0.1.2/helper/llm/prompt_only.py +16 -0
- doclogs_cli-0.1.2/helper/llm/registry.py +39 -0
- {doclogs_cli-0.1.0 → doclogs_cli-0.1.2}/helper/paths.py +1 -1
- doclogs_cli-0.1.2/helper/publish_git.py +160 -0
- doclogs_cli-0.1.2/helper/story.py +90 -0
- doclogs_cli-0.1.2/helper/syntax.py +230 -0
- {doclogs_cli-0.1.0 → doclogs_cli-0.1.2}/helper/task_notes.py +32 -6
- {doclogs_cli-0.1.0 → doclogs_cli-0.1.2}/helper/templates/config.yaml +19 -1
- {doclogs_cli-0.1.0 → doclogs_cli-0.1.2}/helper/weekly.py +2 -2
- doclogs_cli-0.1.2/main.py +41 -0
- {doclogs_cli-0.1.0 → doclogs_cli-0.1.2}/pyproject.toml +1 -1
- doclogs_cli-0.1.0/commands/config.py +0 -28
- doclogs_cli-0.1.0/commands/generate.py +0 -49
- doclogs_cli-0.1.0/helper/generate.py +0 -17
- doclogs_cli-0.1.0/helper/story.py +0 -31
- doclogs_cli-0.1.0/main.py +0 -22
- {doclogs_cli-0.1.0 → doclogs_cli-0.1.2}/LICENSE +0 -0
- {doclogs_cli-0.1.0 → doclogs_cli-0.1.2}/commands/__init__.py +0 -0
- {doclogs_cli-0.1.0 → doclogs_cli-0.1.2}/doclogs_cli.egg-info/dependency_links.txt +0 -0
- {doclogs_cli-0.1.0 → doclogs_cli-0.1.2}/doclogs_cli.egg-info/entry_points.txt +0 -0
- {doclogs_cli-0.1.0 → doclogs_cli-0.1.2}/doclogs_cli.egg-info/requires.txt +0 -0
- {doclogs_cli-0.1.0 → doclogs_cli-0.1.2}/doclogs_cli.egg-info/top_level.txt +0 -0
- {doclogs_cli-0.1.0 → doclogs_cli-0.1.2}/helper/__init__.py +0 -0
- {doclogs_cli-0.1.0 → doclogs_cli-0.1.2}/helper/capture.py +0 -0
- {doclogs_cli-0.1.0 → doclogs_cli-0.1.2}/helper/entry.py +0 -0
- {doclogs_cli-0.1.0 → doclogs_cli-0.1.2}/helper/git_collector.py +0 -0
- {doclogs_cli-0.1.0 → doclogs_cli-0.1.2}/helper/prompts.py +0 -0
- {doclogs_cli-0.1.0 → doclogs_cli-0.1.2}/helper/sanitize.py +0 -0
- {doclogs_cli-0.1.0 → doclogs_cli-0.1.2}/helper/templates/blog.md +0 -0
- {doclogs_cli-0.1.0 → doclogs_cli-0.1.2}/helper/templates/changelog.md +0 -0
- {doclogs_cli-0.1.0 → doclogs_cli-0.1.2}/helper/templates/interview.md +0 -0
- {doclogs_cli-0.1.0 → doclogs_cli-0.1.2}/helper/templates/linkedin.md +0 -0
- {doclogs_cli-0.1.0 → doclogs_cli-0.1.2}/helper/templates/resume.md +0 -0
- {doclogs_cli-0.1.0 → doclogs_cli-0.1.2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: doclogs-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: CLI for capturing engineering work and turning it into career artifacts.
|
|
5
5
|
Author: Mridul Tiwari
|
|
6
6
|
License-Expression: MIT
|
|
@@ -39,6 +39,8 @@ DocLogs helps you turn day-to-day engineering activity into durable artifacts by
|
|
|
39
39
|
- generating markdown artifacts for blog posts, LinkedIn, resumes, and interview prep
|
|
40
40
|
- keeping model usage provider-agnostic and safe with sanitization
|
|
41
41
|
|
|
42
|
+
Use `--syntax` on any command for usage, options, and examples (e.g. `doclog generate --syntax`). Standard `--help` is also available.
|
|
43
|
+
|
|
42
44
|
## Core commands
|
|
43
45
|
|
|
44
46
|
- `doclog capture`
|
|
@@ -49,9 +51,14 @@ DocLogs helps you turn day-to-day engineering activity into durable artifacts by
|
|
|
49
51
|
- surface candidate stories worth expanding
|
|
50
52
|
- `doclog generate <type>`
|
|
51
53
|
- create reusable artifacts such as `blog`, `linkedin`, `resume`, `interview`, or `changelog`
|
|
54
|
+
- default: saves a sanitized prompt file for manual use in Cursor or Copilot
|
|
55
|
+
- optional: auto-generate via logged-in Cursor CLI or GitHub Copilot CLI
|
|
52
56
|
- `doclog sanitize`
|
|
53
57
|
- sanitize captured content before any LLM request
|
|
54
58
|
- redact internal URLs, tokens, IP addresses, and other sensitive details
|
|
59
|
+
- `doclog publish`
|
|
60
|
+
- configure a git repo for publishing generated posts
|
|
61
|
+
- commit and push a post to that repo
|
|
55
62
|
|
|
56
63
|
## Recommended repository structure
|
|
57
64
|
|
|
@@ -85,7 +92,7 @@ For implementation, start by focusing on:
|
|
|
85
92
|
1. evidence collection and local storage
|
|
86
93
|
2. weekly summaries and story selection
|
|
87
94
|
3. safe prompt generation
|
|
88
|
-
4. provider adapters for
|
|
95
|
+
4. provider adapters for IDE CLIs (Cursor, Copilot) and optional API providers
|
|
89
96
|
|
|
90
97
|
## Getting started
|
|
91
98
|
|
|
@@ -120,10 +127,50 @@ doclog capture
|
|
|
120
127
|
|
|
121
128
|
### Daily use
|
|
122
129
|
|
|
123
|
-
1. Configure `~/.doclog/config.yaml`
|
|
130
|
+
1. Configure `~/.doclog/config.yaml` (created automatically on first run).
|
|
124
131
|
2. Add a scheduler entry outside the CLI to invoke `doclog capture` at your preferred check-in time.
|
|
125
132
|
3. Capture daily progress and generate reusable career artifacts from the same captured story.
|
|
126
133
|
|
|
134
|
+
### Generate posts
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
# Default: prompt file only (paste into Cursor/Copilot chat manually)
|
|
138
|
+
doclog weekly
|
|
139
|
+
doclog generate blog -t "nginx fix"
|
|
140
|
+
|
|
141
|
+
# Auto-generate via logged-in Cursor CLI
|
|
142
|
+
doclog config set provider cursor
|
|
143
|
+
doclog generate blog -t "nginx fix"
|
|
144
|
+
|
|
145
|
+
# Or Copilot
|
|
146
|
+
doclog config set provider copilot
|
|
147
|
+
|
|
148
|
+
# Back to prompt-only
|
|
149
|
+
doclog config set provider prompt_only
|
|
150
|
+
|
|
151
|
+
# Check provider availability
|
|
152
|
+
doclog config
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Ensure Cursor/Copilot CLI is installed and authenticated when using those providers (`agent login` or `copilot`).
|
|
156
|
+
|
|
157
|
+
### Publish a post to git
|
|
158
|
+
|
|
159
|
+
Point DocLogs at a local clone of your blog/docs repo, then commit and push generated posts:
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
doclog publish set repo ~/projects/my-blog
|
|
163
|
+
doclog publish set branch main
|
|
164
|
+
doclog generate blog -t 1
|
|
165
|
+
doclog publish push --latest
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Or push a specific file:
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
doclog publish push ~/.doclog/posts/nginx-fix-blog.md -m "Add nginx TLS post"
|
|
172
|
+
```
|
|
173
|
+
|
|
127
174
|
## Publish to PyPI (GitHub Actions)
|
|
128
175
|
|
|
129
176
|
Publishing runs via GitHub Actions — no local `twine upload` needed.
|
|
@@ -13,6 +13,8 @@ DocLogs helps you turn day-to-day engineering activity into durable artifacts by
|
|
|
13
13
|
- generating markdown artifacts for blog posts, LinkedIn, resumes, and interview prep
|
|
14
14
|
- keeping model usage provider-agnostic and safe with sanitization
|
|
15
15
|
|
|
16
|
+
Use `--syntax` on any command for usage, options, and examples (e.g. `doclog generate --syntax`). Standard `--help` is also available.
|
|
17
|
+
|
|
16
18
|
## Core commands
|
|
17
19
|
|
|
18
20
|
- `doclog capture`
|
|
@@ -23,9 +25,14 @@ DocLogs helps you turn day-to-day engineering activity into durable artifacts by
|
|
|
23
25
|
- surface candidate stories worth expanding
|
|
24
26
|
- `doclog generate <type>`
|
|
25
27
|
- create reusable artifacts such as `blog`, `linkedin`, `resume`, `interview`, or `changelog`
|
|
28
|
+
- default: saves a sanitized prompt file for manual use in Cursor or Copilot
|
|
29
|
+
- optional: auto-generate via logged-in Cursor CLI or GitHub Copilot CLI
|
|
26
30
|
- `doclog sanitize`
|
|
27
31
|
- sanitize captured content before any LLM request
|
|
28
32
|
- redact internal URLs, tokens, IP addresses, and other sensitive details
|
|
33
|
+
- `doclog publish`
|
|
34
|
+
- configure a git repo for publishing generated posts
|
|
35
|
+
- commit and push a post to that repo
|
|
29
36
|
|
|
30
37
|
## Recommended repository structure
|
|
31
38
|
|
|
@@ -59,7 +66,7 @@ For implementation, start by focusing on:
|
|
|
59
66
|
1. evidence collection and local storage
|
|
60
67
|
2. weekly summaries and story selection
|
|
61
68
|
3. safe prompt generation
|
|
62
|
-
4. provider adapters for
|
|
69
|
+
4. provider adapters for IDE CLIs (Cursor, Copilot) and optional API providers
|
|
63
70
|
|
|
64
71
|
## Getting started
|
|
65
72
|
|
|
@@ -94,10 +101,50 @@ doclog capture
|
|
|
94
101
|
|
|
95
102
|
### Daily use
|
|
96
103
|
|
|
97
|
-
1. Configure `~/.doclog/config.yaml`
|
|
104
|
+
1. Configure `~/.doclog/config.yaml` (created automatically on first run).
|
|
98
105
|
2. Add a scheduler entry outside the CLI to invoke `doclog capture` at your preferred check-in time.
|
|
99
106
|
3. Capture daily progress and generate reusable career artifacts from the same captured story.
|
|
100
107
|
|
|
108
|
+
### Generate posts
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
# Default: prompt file only (paste into Cursor/Copilot chat manually)
|
|
112
|
+
doclog weekly
|
|
113
|
+
doclog generate blog -t "nginx fix"
|
|
114
|
+
|
|
115
|
+
# Auto-generate via logged-in Cursor CLI
|
|
116
|
+
doclog config set provider cursor
|
|
117
|
+
doclog generate blog -t "nginx fix"
|
|
118
|
+
|
|
119
|
+
# Or Copilot
|
|
120
|
+
doclog config set provider copilot
|
|
121
|
+
|
|
122
|
+
# Back to prompt-only
|
|
123
|
+
doclog config set provider prompt_only
|
|
124
|
+
|
|
125
|
+
# Check provider availability
|
|
126
|
+
doclog config
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Ensure Cursor/Copilot CLI is installed and authenticated when using those providers (`agent login` or `copilot`).
|
|
130
|
+
|
|
131
|
+
### Publish a post to git
|
|
132
|
+
|
|
133
|
+
Point DocLogs at a local clone of your blog/docs repo, then commit and push generated posts:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
doclog publish set repo ~/projects/my-blog
|
|
137
|
+
doclog publish set branch main
|
|
138
|
+
doclog generate blog -t 1
|
|
139
|
+
doclog publish push --latest
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Or push a specific file:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
doclog publish push ~/.doclog/posts/nginx-fix-blog.md -m "Add nginx TLS post"
|
|
146
|
+
```
|
|
147
|
+
|
|
101
148
|
## Publish to PyPI (GitHub Actions)
|
|
102
149
|
|
|
103
150
|
Publishing runs via GitHub Actions — no local `twine upload` needed.
|
|
@@ -5,6 +5,7 @@ from helper.capture import build_capture_entry, merge_notes
|
|
|
5
5
|
from helper.capture_prompts import iter_interactive_tasks
|
|
6
6
|
from helper.entry import save_entry
|
|
7
7
|
from helper.git_collector import find_git_root
|
|
8
|
+
from helper.syntax import maybe_show_syntax
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
def _print_summary(entry, path) -> None:
|
|
@@ -13,15 +14,31 @@ def _print_summary(entry, path) -> None:
|
|
|
13
14
|
typer.echo(f" commits today: {len(entry.commits)}")
|
|
14
15
|
|
|
15
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
|
+
|
|
16
26
|
def register(app: typer.Typer):
|
|
17
27
|
|
|
18
28
|
@app.command("capture", help="Capture today's engineering work into local storage.")
|
|
19
29
|
def capture(
|
|
20
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
|
+
),
|
|
21
36
|
no_interactive: bool = typer.Option(False, "--no-interactive", help="Skip questions; git-only capture."),
|
|
22
37
|
include_terminal: bool = typer.Option(False, help="Include optional terminal history evidence."),
|
|
23
38
|
include_tickets: bool = typer.Option(False, help="Include optional ticket IDs or issue references."),
|
|
39
|
+
syntax: bool = typer.Option(False, "--syntax", help="Show command syntax, options, and examples."),
|
|
24
40
|
) -> None:
|
|
41
|
+
maybe_show_syntax("capture", syntax)
|
|
25
42
|
if find_git_root() is None:
|
|
26
43
|
typer.echo("⚠️ Not inside a git repo — git evidence will be empty.")
|
|
27
44
|
|
|
@@ -31,7 +48,7 @@ def register(app: typer.Typer):
|
|
|
31
48
|
entry = None
|
|
32
49
|
saved_any = False
|
|
33
50
|
|
|
34
|
-
for batch, replace in iter_interactive_tasks():
|
|
51
|
+
for batch, replace in iter_interactive_tasks(topic=topic):
|
|
35
52
|
entry = build_capture_entry(notes=batch, replace_notes=replace)
|
|
36
53
|
path = save_entry(entry)
|
|
37
54
|
saved_any = True
|
|
@@ -45,7 +62,7 @@ def register(app: typer.Typer):
|
|
|
45
62
|
return
|
|
46
63
|
|
|
47
64
|
# Non-interactive: git only or single --notes
|
|
48
|
-
final_notes = merge_notes(notes)
|
|
65
|
+
final_notes = merge_notes(_notes_with_topic(notes, topic) if notes else None)
|
|
49
66
|
entry = build_capture_entry(notes=final_notes)
|
|
50
67
|
path = save_entry(entry)
|
|
51
|
-
_print_summary(entry, path)
|
|
68
|
+
_print_summary(entry, path)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import yaml
|
|
6
|
+
|
|
7
|
+
from helper.llm import PROVIDER_NAMES, get_provider
|
|
8
|
+
from helper.llm.config import (
|
|
9
|
+
load_llm_config,
|
|
10
|
+
load_raw_config,
|
|
11
|
+
provider_name_from_config,
|
|
12
|
+
set_llm_provider,
|
|
13
|
+
)
|
|
14
|
+
from helper.paths import ensure_config
|
|
15
|
+
from helper.syntax import maybe_show_syntax
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def load_config(path: Path | None = None) -> dict[str, object]:
|
|
19
|
+
config_file = path or ensure_config()
|
|
20
|
+
if not config_file.exists():
|
|
21
|
+
raise typer.Exit(code=1, message=f"Config file not found: {config_file}")
|
|
22
|
+
with config_file.open("r", encoding="utf-8") as stream:
|
|
23
|
+
return yaml.safe_load(stream) or {}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _show_config(path: Path | None = None) -> None:
|
|
27
|
+
active = path or ensure_config()
|
|
28
|
+
settings = load_config(active)
|
|
29
|
+
provider_name = provider_name_from_config(settings)
|
|
30
|
+
|
|
31
|
+
typer.echo(f"Active DocLogs configuration ({active}):")
|
|
32
|
+
typer.echo(settings)
|
|
33
|
+
typer.echo("")
|
|
34
|
+
typer.echo(f"Active provider: {provider_name}")
|
|
35
|
+
typer.echo(f"Cursor CLI: {_provider_status('cursor')}")
|
|
36
|
+
typer.echo(f"Copilot CLI: {_provider_status('copilot')}")
|
|
37
|
+
typer.echo("")
|
|
38
|
+
typer.echo("Change provider: doclog config set provider cursor|copilot|prompt_only")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _provider_status(name: str) -> str:
|
|
42
|
+
provider = get_provider(name)
|
|
43
|
+
if provider.is_available():
|
|
44
|
+
return "available"
|
|
45
|
+
return f"not available — {provider.availability_hint()}"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def register(app: typer.Typer):
|
|
49
|
+
config_app = typer.Typer(help="Show or update DocLogs configuration.")
|
|
50
|
+
|
|
51
|
+
@config_app.callback(invoke_without_command=True)
|
|
52
|
+
def config_root(
|
|
53
|
+
ctx: typer.Context,
|
|
54
|
+
path: Optional[Path] = typer.Option(None, "-c", "--config", help="Path to the config file."),
|
|
55
|
+
syntax: bool = typer.Option(False, "--syntax", help="Show command syntax, options, and examples."),
|
|
56
|
+
) -> None:
|
|
57
|
+
if syntax and ctx.invoked_subcommand is None:
|
|
58
|
+
maybe_show_syntax("config", True)
|
|
59
|
+
if ctx.invoked_subcommand is not None:
|
|
60
|
+
return
|
|
61
|
+
_show_config(path)
|
|
62
|
+
|
|
63
|
+
@config_app.command("set", help="Update a configuration value.")
|
|
64
|
+
def config_set(
|
|
65
|
+
key: str = typer.Argument(..., help="Setting name (currently: provider)."),
|
|
66
|
+
value: str = typer.Argument(..., help="New value."),
|
|
67
|
+
path: Optional[Path] = typer.Option(None, "-c", "--config", help="Path to the config file."),
|
|
68
|
+
syntax: bool = typer.Option(False, "--syntax", help="Show command syntax, options, and examples."),
|
|
69
|
+
) -> None:
|
|
70
|
+
maybe_show_syntax("config set", syntax)
|
|
71
|
+
normalized_key = key.strip().lower().replace(".", "_")
|
|
72
|
+
if normalized_key not in {"provider", "llm_provider"}:
|
|
73
|
+
typer.echo(f"Unknown setting {key!r}. Supported: provider")
|
|
74
|
+
raise typer.Exit(code=1)
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
config_file = set_llm_provider(value, path)
|
|
78
|
+
except ValueError as exc:
|
|
79
|
+
typer.echo(str(exc))
|
|
80
|
+
raise typer.Exit(code=1) from exc
|
|
81
|
+
|
|
82
|
+
typer.echo(f"Set llm.provider to {value.strip().lower()} in {config_file}")
|
|
83
|
+
if value.strip().lower() != "prompt_only":
|
|
84
|
+
selected = get_provider(value.strip().lower(), load_llm_config(config_file))
|
|
85
|
+
if not selected.is_available():
|
|
86
|
+
typer.echo(f"Warning: {selected.availability_hint()}")
|
|
87
|
+
|
|
88
|
+
app.add_typer(config_app, name="config")
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from helper.generate import build_prompt, save_and_generate
|
|
7
|
+
from helper.llm import PROVIDER_NAMES, ProviderError, get_provider, resolve_provider_name
|
|
8
|
+
from helper.llm.config import load_llm_config, provider_settings
|
|
9
|
+
from helper.paths import posts_dir
|
|
10
|
+
from helper.story import find_story_text
|
|
11
|
+
from helper.syntax import maybe_show_syntax
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def slugify(text: str) -> str:
|
|
15
|
+
text = text.strip().lower()
|
|
16
|
+
text = re.sub(r"[^\w\s-]", "", text)
|
|
17
|
+
return re.sub(r"[-\s]+", "-", text).strip("-")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def register(app: typer.Typer):
|
|
21
|
+
|
|
22
|
+
@app.command("generate", help="Generate a reusable artifact from a captured story.")
|
|
23
|
+
def generate(
|
|
24
|
+
artifact_type: str = typer.Argument(..., help="blog, linkedin, resume, interview, changelog"),
|
|
25
|
+
title: Optional[str] = typer.Option(None, "-t", "--title", help="Story title from doclog weekly."),
|
|
26
|
+
days: int = typer.Option(7, help="Days back to search for the story."),
|
|
27
|
+
provider: Optional[str] = typer.Option(
|
|
28
|
+
None,
|
|
29
|
+
"--provider",
|
|
30
|
+
help=f"Override LLM provider: {', '.join(PROVIDER_NAMES)}",
|
|
31
|
+
),
|
|
32
|
+
prompt_only: bool = typer.Option(
|
|
33
|
+
False,
|
|
34
|
+
"--prompt-only",
|
|
35
|
+
help="Write the sanitized prompt file only.",
|
|
36
|
+
),
|
|
37
|
+
force: bool = typer.Option(
|
|
38
|
+
False,
|
|
39
|
+
"--force",
|
|
40
|
+
help="Send to an external provider even when sanitize flags are present.",
|
|
41
|
+
),
|
|
42
|
+
trust: bool = typer.Option(
|
|
43
|
+
False,
|
|
44
|
+
"--trust",
|
|
45
|
+
help="Pass --trust to Cursor CLI (required for non-interactive agent runs).",
|
|
46
|
+
),
|
|
47
|
+
syntax: bool = typer.Option(False, "--syntax", help="Show command syntax, options, and examples."),
|
|
48
|
+
) -> None:
|
|
49
|
+
maybe_show_syntax("generate", syntax)
|
|
50
|
+
if not title:
|
|
51
|
+
typer.echo('Pass a story title: doclog generate blog -t "Phase 0 completed"')
|
|
52
|
+
raise typer.Exit(code=1)
|
|
53
|
+
|
|
54
|
+
story = find_story_text(title, days=days)
|
|
55
|
+
if not story:
|
|
56
|
+
typer.echo(f"Story not found: {title!r}")
|
|
57
|
+
typer.echo("Run doclog weekly to see available titles.")
|
|
58
|
+
typer.echo('Tip: use the list number, e.g. doclog generate blog -t 1')
|
|
59
|
+
typer.echo(' or a short unique phrase (do not include the date in parentheses)')
|
|
60
|
+
raise typer.Exit(code=1)
|
|
61
|
+
|
|
62
|
+
config = load_llm_config()
|
|
63
|
+
if trust:
|
|
64
|
+
config = dict(config)
|
|
65
|
+
cursor_cfg = dict(provider_settings(config, "cursor"))
|
|
66
|
+
cursor_cfg["trust_workspace"] = True
|
|
67
|
+
config["cursor"] = cursor_cfg
|
|
68
|
+
try:
|
|
69
|
+
provider_name = resolve_provider_name(
|
|
70
|
+
provider,
|
|
71
|
+
prompt_only=prompt_only,
|
|
72
|
+
config=config,
|
|
73
|
+
)
|
|
74
|
+
selected_provider = get_provider(provider_name, config)
|
|
75
|
+
except ValueError as exc:
|
|
76
|
+
typer.echo(str(exc))
|
|
77
|
+
raise typer.Exit(code=1) from exc
|
|
78
|
+
|
|
79
|
+
if provider_name != "prompt_only" and not selected_provider.is_available():
|
|
80
|
+
typer.echo(f"Provider {provider_name!r} is not available.")
|
|
81
|
+
typer.echo(selected_provider.availability_hint())
|
|
82
|
+
raise typer.Exit(code=1)
|
|
83
|
+
|
|
84
|
+
prompt, findings, flags = build_prompt(artifact_type, story)
|
|
85
|
+
|
|
86
|
+
if flags:
|
|
87
|
+
typer.echo("Sensitive patterns flagged — review before sending to an external provider:")
|
|
88
|
+
for flag in flags:
|
|
89
|
+
typer.echo(f" - [{flag.kind}] {flag.matched[:60]}")
|
|
90
|
+
if provider_name != "prompt_only" and not force:
|
|
91
|
+
slug = slugify(title)
|
|
92
|
+
output_dir = posts_dir()
|
|
93
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
94
|
+
prompt_path = output_dir / f"{slug}-{artifact_type}-prompt.md"
|
|
95
|
+
prompt_path.write_text(prompt, encoding="utf-8")
|
|
96
|
+
typer.echo("Re-run with --force to send this prompt to the selected provider.")
|
|
97
|
+
typer.echo(f"Prompt saved to {prompt_path}")
|
|
98
|
+
raise typer.Exit(code=1)
|
|
99
|
+
|
|
100
|
+
slug = slugify(title)
|
|
101
|
+
output_dir = posts_dir()
|
|
102
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
103
|
+
|
|
104
|
+
if provider_name == "prompt_only":
|
|
105
|
+
prompt_path = output_dir / f"{slug}-{artifact_type}.md"
|
|
106
|
+
artifact_path = None
|
|
107
|
+
else:
|
|
108
|
+
prompt_path = output_dir / f"{slug}-{artifact_type}-prompt.md"
|
|
109
|
+
artifact_path = output_dir / f"{slug}-{artifact_type}.md"
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
result = save_and_generate(
|
|
113
|
+
prompt,
|
|
114
|
+
findings,
|
|
115
|
+
flags,
|
|
116
|
+
selected_provider,
|
|
117
|
+
prompt_path=prompt_path,
|
|
118
|
+
artifact_path=artifact_path,
|
|
119
|
+
)
|
|
120
|
+
except ProviderError as exc:
|
|
121
|
+
prompt_path.write_text(prompt, encoding="utf-8")
|
|
122
|
+
typer.echo(str(exc))
|
|
123
|
+
typer.echo(f"Prompt saved to {prompt_path}")
|
|
124
|
+
raise typer.Exit(code=1) from exc
|
|
125
|
+
|
|
126
|
+
if provider_name == "prompt_only":
|
|
127
|
+
typer.echo(f"Prompt saved to {result.prompt_path}")
|
|
128
|
+
else:
|
|
129
|
+
typer.echo(f"Prompt saved to {result.prompt_path}")
|
|
130
|
+
typer.echo(f"Artifact saved to {result.artifact_path} (provider: {provider_name})")
|
|
131
|
+
|
|
132
|
+
if findings:
|
|
133
|
+
typer.echo(f" ({len(findings)} item(s) redacted in evidence)")
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from helper.publish_git import (
|
|
6
|
+
PUBLISH_KEYS,
|
|
7
|
+
PublishError,
|
|
8
|
+
load_publish_config,
|
|
9
|
+
push_post,
|
|
10
|
+
resolve_post_file,
|
|
11
|
+
set_publish_value,
|
|
12
|
+
)
|
|
13
|
+
from helper.syntax import maybe_show_syntax
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _show_publish_config() -> None:
|
|
17
|
+
config = load_publish_config()
|
|
18
|
+
typer.echo("Publish configuration:")
|
|
19
|
+
typer.echo(f" repo: {config.repo_path or '(not set)'}")
|
|
20
|
+
typer.echo(f" branch: {config.branch}")
|
|
21
|
+
typer.echo(f" remote: {config.remote}")
|
|
22
|
+
typer.echo(f" subdir: {config.subdir}")
|
|
23
|
+
typer.echo("")
|
|
24
|
+
typer.echo("Configure: doclog publish set repo /path/to/clone")
|
|
25
|
+
typer.echo("Push post: doclog publish push --latest")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def register(app: typer.Typer):
|
|
29
|
+
publish_app = typer.Typer(help="Configure and push generated posts to a git repo.")
|
|
30
|
+
|
|
31
|
+
@publish_app.callback(invoke_without_command=True)
|
|
32
|
+
def publish_root(
|
|
33
|
+
ctx: typer.Context,
|
|
34
|
+
syntax: bool = typer.Option(False, "--syntax", help="Show command syntax, options, and examples."),
|
|
35
|
+
) -> None:
|
|
36
|
+
if syntax and ctx.invoked_subcommand is None:
|
|
37
|
+
maybe_show_syntax("publish", True)
|
|
38
|
+
if ctx.invoked_subcommand is not None:
|
|
39
|
+
return
|
|
40
|
+
_show_publish_config()
|
|
41
|
+
|
|
42
|
+
@publish_app.command("set", help="Configure the git repo used to publish posts.")
|
|
43
|
+
def publish_set(
|
|
44
|
+
key: str = typer.Argument(..., help=f"One of: {', '.join(sorted(PUBLISH_KEYS))}"),
|
|
45
|
+
value: str = typer.Argument(..., help="Setting value."),
|
|
46
|
+
syntax: bool = typer.Option(False, "--syntax", help="Show command syntax, options, and examples."),
|
|
47
|
+
) -> None:
|
|
48
|
+
maybe_show_syntax("publish set", syntax)
|
|
49
|
+
try:
|
|
50
|
+
config_file = set_publish_value(key, value)
|
|
51
|
+
except ValueError as exc:
|
|
52
|
+
typer.echo(str(exc))
|
|
53
|
+
raise typer.Exit(code=1) from exc
|
|
54
|
+
|
|
55
|
+
typer.echo(f"Set publish.{key.strip().lower()} in {config_file}")
|
|
56
|
+
|
|
57
|
+
@publish_app.command("push", help="Copy a post into the publish repo, commit, and push.")
|
|
58
|
+
def publish_push(
|
|
59
|
+
post: Optional[Path] = typer.Argument(
|
|
60
|
+
None,
|
|
61
|
+
help="Path to a generated post under ~/.doclog/posts/",
|
|
62
|
+
),
|
|
63
|
+
latest: bool = typer.Option(
|
|
64
|
+
False,
|
|
65
|
+
"--latest",
|
|
66
|
+
"-l",
|
|
67
|
+
help="Publish the most recently generated post.",
|
|
68
|
+
),
|
|
69
|
+
message: Optional[str] = typer.Option(
|
|
70
|
+
None,
|
|
71
|
+
"-m",
|
|
72
|
+
"--message",
|
|
73
|
+
help="Custom git commit message.",
|
|
74
|
+
),
|
|
75
|
+
syntax: bool = typer.Option(False, "--syntax", help="Show command syntax, options, and examples."),
|
|
76
|
+
) -> None:
|
|
77
|
+
maybe_show_syntax("publish push", syntax)
|
|
78
|
+
try:
|
|
79
|
+
post_file = resolve_post_file(post, latest=latest)
|
|
80
|
+
dest = push_post(post_file, load_publish_config(), message=message)
|
|
81
|
+
except PublishError as exc:
|
|
82
|
+
typer.echo(str(exc))
|
|
83
|
+
raise typer.Exit(code=1) from exc
|
|
84
|
+
|
|
85
|
+
typer.echo(f"Published {post_file.name}")
|
|
86
|
+
typer.echo(f" copied to {dest}")
|
|
87
|
+
typer.echo(f" pushed to remote")
|
|
88
|
+
|
|
89
|
+
app.add_typer(publish_app, name="publish")
|
|
@@ -3,6 +3,7 @@ from typing import *
|
|
|
3
3
|
|
|
4
4
|
from helper.entry import entry_path_for, load_entry
|
|
5
5
|
from helper.sanitize import redact, sanitize_with_review, text_from_entry
|
|
6
|
+
from helper.syntax import maybe_show_syntax
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
def register(app: typer.Typer):
|
|
@@ -10,7 +11,9 @@ def register(app: typer.Typer):
|
|
|
10
11
|
@app.command("sanitize", help="Sanitize content before sending to an LLM.")
|
|
11
12
|
def sanitize(
|
|
12
13
|
source: Optional[str] = typer.Argument(None, help="Text to sanitize. If omitted, uses today's capture."),
|
|
14
|
+
syntax: bool = typer.Option(False, "--syntax", help="Show command syntax, options, and examples."),
|
|
13
15
|
) -> None:
|
|
16
|
+
maybe_show_syntax("sanitize", syntax)
|
|
14
17
|
if source:
|
|
15
18
|
text = source
|
|
16
19
|
else:
|
|
@@ -2,6 +2,7 @@ from ast import Return
|
|
|
2
2
|
import typer
|
|
3
3
|
from typing import *
|
|
4
4
|
from helper.weekly import build_story_candidates, load_entries_for_days
|
|
5
|
+
from helper.syntax import maybe_show_syntax
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
def register(app: typer.Typer):
|
|
@@ -9,9 +10,11 @@ def register(app: typer.Typer):
|
|
|
9
10
|
@app.command("weekly", help="run workflows")
|
|
10
11
|
def weekly(
|
|
11
12
|
limit: int = typer.Option(5, help="Maximum number of story candidates to display."),
|
|
12
|
-
days: int = typer.Option(7, help="How many days back to review.")
|
|
13
|
+
days: int = typer.Option(7, help="How many days back to review."),
|
|
14
|
+
syntax: bool = typer.Option(False, "--syntax", help="Show command syntax, options, and examples."),
|
|
13
15
|
) -> None:
|
|
14
16
|
"""Summarize the week and surface candidate stories for expansion."""
|
|
17
|
+
maybe_show_syntax("weekly", syntax)
|
|
15
18
|
entries = load_entries_for_days(days)
|
|
16
19
|
if not entries:
|
|
17
20
|
typer.echo(f"📅 No captures found in the last {days} days.")
|
|
@@ -25,3 +28,5 @@ def register(app: typer.Typer):
|
|
|
25
28
|
return
|
|
26
29
|
for i, story in enumerate(candidates, start=1):
|
|
27
30
|
typer.echo(f"{i}. [{story.source}] {story.title} ({story.date})")
|
|
31
|
+
typer.echo("")
|
|
32
|
+
typer.echo('Generate: doclog generate blog -t 1 (or a short unique phrase from the title)')
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: doclogs-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: CLI for capturing engineering work and turning it into career artifacts.
|
|
5
5
|
Author: Mridul Tiwari
|
|
6
6
|
License-Expression: MIT
|
|
@@ -39,6 +39,8 @@ DocLogs helps you turn day-to-day engineering activity into durable artifacts by
|
|
|
39
39
|
- generating markdown artifacts for blog posts, LinkedIn, resumes, and interview prep
|
|
40
40
|
- keeping model usage provider-agnostic and safe with sanitization
|
|
41
41
|
|
|
42
|
+
Use `--syntax` on any command for usage, options, and examples (e.g. `doclog generate --syntax`). Standard `--help` is also available.
|
|
43
|
+
|
|
42
44
|
## Core commands
|
|
43
45
|
|
|
44
46
|
- `doclog capture`
|
|
@@ -49,9 +51,14 @@ DocLogs helps you turn day-to-day engineering activity into durable artifacts by
|
|
|
49
51
|
- surface candidate stories worth expanding
|
|
50
52
|
- `doclog generate <type>`
|
|
51
53
|
- create reusable artifacts such as `blog`, `linkedin`, `resume`, `interview`, or `changelog`
|
|
54
|
+
- default: saves a sanitized prompt file for manual use in Cursor or Copilot
|
|
55
|
+
- optional: auto-generate via logged-in Cursor CLI or GitHub Copilot CLI
|
|
52
56
|
- `doclog sanitize`
|
|
53
57
|
- sanitize captured content before any LLM request
|
|
54
58
|
- redact internal URLs, tokens, IP addresses, and other sensitive details
|
|
59
|
+
- `doclog publish`
|
|
60
|
+
- configure a git repo for publishing generated posts
|
|
61
|
+
- commit and push a post to that repo
|
|
55
62
|
|
|
56
63
|
## Recommended repository structure
|
|
57
64
|
|
|
@@ -85,7 +92,7 @@ For implementation, start by focusing on:
|
|
|
85
92
|
1. evidence collection and local storage
|
|
86
93
|
2. weekly summaries and story selection
|
|
87
94
|
3. safe prompt generation
|
|
88
|
-
4. provider adapters for
|
|
95
|
+
4. provider adapters for IDE CLIs (Cursor, Copilot) and optional API providers
|
|
89
96
|
|
|
90
97
|
## Getting started
|
|
91
98
|
|
|
@@ -120,10 +127,50 @@ doclog capture
|
|
|
120
127
|
|
|
121
128
|
### Daily use
|
|
122
129
|
|
|
123
|
-
1. Configure `~/.doclog/config.yaml`
|
|
130
|
+
1. Configure `~/.doclog/config.yaml` (created automatically on first run).
|
|
124
131
|
2. Add a scheduler entry outside the CLI to invoke `doclog capture` at your preferred check-in time.
|
|
125
132
|
3. Capture daily progress and generate reusable career artifacts from the same captured story.
|
|
126
133
|
|
|
134
|
+
### Generate posts
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
# Default: prompt file only (paste into Cursor/Copilot chat manually)
|
|
138
|
+
doclog weekly
|
|
139
|
+
doclog generate blog -t "nginx fix"
|
|
140
|
+
|
|
141
|
+
# Auto-generate via logged-in Cursor CLI
|
|
142
|
+
doclog config set provider cursor
|
|
143
|
+
doclog generate blog -t "nginx fix"
|
|
144
|
+
|
|
145
|
+
# Or Copilot
|
|
146
|
+
doclog config set provider copilot
|
|
147
|
+
|
|
148
|
+
# Back to prompt-only
|
|
149
|
+
doclog config set provider prompt_only
|
|
150
|
+
|
|
151
|
+
# Check provider availability
|
|
152
|
+
doclog config
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Ensure Cursor/Copilot CLI is installed and authenticated when using those providers (`agent login` or `copilot`).
|
|
156
|
+
|
|
157
|
+
### Publish a post to git
|
|
158
|
+
|
|
159
|
+
Point DocLogs at a local clone of your blog/docs repo, then commit and push generated posts:
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
doclog publish set repo ~/projects/my-blog
|
|
163
|
+
doclog publish set branch main
|
|
164
|
+
doclog generate blog -t 1
|
|
165
|
+
doclog publish push --latest
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Or push a specific file:
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
doclog publish push ~/.doclog/posts/nginx-fix-blog.md -m "Add nginx TLS post"
|
|
172
|
+
```
|
|
173
|
+
|
|
127
174
|
## Publish to PyPI (GitHub Actions)
|
|
128
175
|
|
|
129
176
|
Publishing runs via GitHub Actions — no local `twine upload` needed.
|