buildlog 0.1.0__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.
buildlog/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """buildlog - Engineering notebook for AI-assisted development."""
2
+
3
+ __version__ = "0.1.0"
buildlog/cli.py ADDED
@@ -0,0 +1,437 @@
1
+ """CLI for buildlog - engineering notebook for AI-assisted development."""
2
+
3
+ import shutil
4
+ import subprocess
5
+ import sys
6
+ from datetime import date, datetime
7
+ from pathlib import Path
8
+
9
+ import click
10
+
11
+ from buildlog.distill import CATEGORIES, distill_all, format_output
12
+ from buildlog.skills import generate_skills, format_skills
13
+ from buildlog.stats import calculate_stats, format_dashboard, format_json
14
+
15
+
16
+ def get_template_dir() -> Path | None:
17
+ """Get the template directory from package data.
18
+
19
+ Returns the directory containing copier.yml, or None to fall back to GitHub.
20
+ """
21
+ # 1. Check if we're in development (template dir exists relative to package)
22
+ # src/buildlog/cli.py -> src/buildlog -> src -> project root
23
+ pkg_dir = Path(__file__).parent.parent.parent
24
+ dev_copier = pkg_dir / "copier.yml"
25
+ if dev_copier.exists():
26
+ return pkg_dir
27
+
28
+ # 2. Check installed location (site-packages/../share/buildlog)
29
+ import sysconfig
30
+ data_dir = Path(sysconfig.get_path("data")) / "share" / "buildlog"
31
+ if (data_dir / "copier.yml").exists():
32
+ return data_dir
33
+
34
+ # 3. Fall back to using copier directly from GitHub
35
+ return None
36
+
37
+
38
+ @click.group()
39
+ @click.version_option()
40
+ def main():
41
+ """buildlog - Engineering notebook for AI-assisted development.
42
+
43
+ Capture your work as publishable content. Include the fuckups.
44
+ """
45
+ pass
46
+
47
+
48
+ @main.command()
49
+ @click.option("--no-claude-md", is_flag=True, help="Don't update CLAUDE.md")
50
+ def init(no_claude_md: bool):
51
+ """Initialize buildlog in the current directory.
52
+
53
+ Sets up the buildlog/ directory with templates and optionally
54
+ adds instructions to CLAUDE.md.
55
+ """
56
+ buildlog_dir = Path("buildlog")
57
+
58
+ if buildlog_dir.exists():
59
+ click.echo("buildlog/ directory already exists.", err=True)
60
+ raise SystemExit(1)
61
+
62
+ template_dir = get_template_dir()
63
+
64
+ if template_dir:
65
+ # Use local template
66
+ click.echo("Initializing buildlog from local template...")
67
+ try:
68
+ subprocess.run(
69
+ [
70
+ sys.executable, "-m", "copier", "copy",
71
+ "--trust",
72
+ *(["--data", "update_claude_md=false"] if no_claude_md else []),
73
+ str(template_dir),
74
+ "."
75
+ ],
76
+ check=True
77
+ )
78
+ except subprocess.CalledProcessError:
79
+ click.echo("Failed to initialize buildlog.", err=True)
80
+ raise SystemExit(1)
81
+ else:
82
+ # Fall back to GitHub
83
+ click.echo("Initializing buildlog from GitHub...")
84
+ try:
85
+ subprocess.run(
86
+ [
87
+ sys.executable, "-m", "copier", "copy",
88
+ "--trust",
89
+ *(["--data", "update_claude_md=false"] if no_claude_md else []),
90
+ "gh:Peleke/buildlog-template",
91
+ "."
92
+ ],
93
+ check=True
94
+ )
95
+ except subprocess.CalledProcessError:
96
+ click.echo("Failed to initialize buildlog.", err=True)
97
+ raise SystemExit(1)
98
+
99
+ click.echo("\n✓ buildlog initialized!")
100
+ click.echo("\nNext: buildlog new my-feature")
101
+
102
+
103
+ @main.command()
104
+ @click.argument("slug")
105
+ @click.option("--date", "-d", "entry_date", default=None, help="Date for entry (YYYY-MM-DD)")
106
+ def new(slug: str, entry_date: str | None):
107
+ """Create a new buildlog entry.
108
+
109
+ SLUG is a short identifier for the entry (e.g., 'auth-api', 'bugfix-login').
110
+
111
+ Examples:
112
+
113
+ buildlog new auth-api
114
+ buildlog new runpod-deploy --date 2026-01-15
115
+ """
116
+ buildlog_dir = Path("buildlog")
117
+ template_file = buildlog_dir / "_TEMPLATE.md"
118
+
119
+ if not buildlog_dir.exists():
120
+ click.echo("No buildlog/ directory found. Run 'buildlog init' first.", err=True)
121
+ raise SystemExit(1)
122
+
123
+ if not template_file.exists():
124
+ click.echo("No _TEMPLATE.md found in buildlog/. Run 'buildlog init' first.", err=True)
125
+ raise SystemExit(1)
126
+
127
+ # Determine date
128
+ if entry_date:
129
+ try:
130
+ # Validate date format
131
+ year, month, day = entry_date.split("-")
132
+ date_str = f"{int(year):04d}-{int(month):02d}-{int(day):02d}"
133
+ except ValueError:
134
+ click.echo("Invalid date format. Use YYYY-MM-DD.", err=True)
135
+ raise SystemExit(1)
136
+ else:
137
+ date_str = date.today().isoformat()
138
+
139
+ # Sanitize slug
140
+ safe_slug = slug.lower().replace(" ", "-").replace("_", "-")
141
+ safe_slug = "".join(c for c in safe_slug if c.isalnum() or c == "-")
142
+
143
+ # Create entry
144
+ entry_name = f"{date_str}-{safe_slug}.md"
145
+ entry_path = buildlog_dir / entry_name
146
+
147
+ if entry_path.exists():
148
+ click.echo(f"Entry already exists: {entry_path}", err=True)
149
+ raise SystemExit(1)
150
+
151
+ # Copy template
152
+ shutil.copy(template_file, entry_path)
153
+
154
+ # Replace placeholder date in the new file
155
+ content = entry_path.read_text()
156
+ content = content.replace("[YYYY-MM-DD]", date_str)
157
+ entry_path.write_text(content)
158
+
159
+ click.echo(f"✓ Created {entry_path}")
160
+ click.echo(f"\nOpen it: $EDITOR {entry_path}")
161
+
162
+
163
+ @main.command()
164
+ def list():
165
+ """List all buildlog entries."""
166
+ buildlog_dir = Path("buildlog")
167
+
168
+ if not buildlog_dir.exists():
169
+ click.echo("No buildlog/ directory found. Run 'buildlog init' first.", err=True)
170
+ raise SystemExit(1)
171
+
172
+ entries = sorted(
173
+ buildlog_dir.glob("20??-??-??-*.md"),
174
+ reverse=True # Most recent first
175
+ )
176
+
177
+ if not entries:
178
+ click.echo("No entries yet. Create one with: buildlog new my-feature")
179
+ return
180
+
181
+ click.echo(f"Found {len(entries)} entries:\n")
182
+ for entry in entries:
183
+ # Extract title from first line if possible
184
+ try:
185
+ first_line = entry.read_text().split("\n")[0]
186
+ title = first_line.replace("# Build Journal: ", "").replace("# ", "").strip()
187
+ if title == "[TITLE]":
188
+ title = "(untitled)"
189
+ except Exception:
190
+ title = "(unreadable)"
191
+
192
+ click.echo(f" {entry.name}")
193
+ click.echo(f" {title}\n")
194
+
195
+
196
+ @main.command()
197
+ def update():
198
+ """Update buildlog templates to latest version."""
199
+ template_dir = get_template_dir()
200
+
201
+ if template_dir:
202
+ click.echo("Updating from local template...")
203
+ try:
204
+ subprocess.run(
205
+ [sys.executable, "-m", "copier", "update", "--trust"],
206
+ check=True
207
+ )
208
+ except subprocess.CalledProcessError:
209
+ click.echo("Failed to update. Try running 'copier update' directly.", err=True)
210
+ raise SystemExit(1)
211
+ else:
212
+ click.echo("Updating from GitHub...")
213
+ try:
214
+ subprocess.run(
215
+ [sys.executable, "-m", "copier", "update", "--trust"],
216
+ check=True
217
+ )
218
+ except subprocess.CalledProcessError:
219
+ click.echo("Failed to update. Try running 'copier update' directly.", err=True)
220
+ raise SystemExit(1)
221
+
222
+ click.echo("\n✓ buildlog updated!")
223
+
224
+
225
+ @main.command()
226
+ @click.option("--output", "-o", type=click.Path(), help="Output file (default: stdout)")
227
+ @click.option(
228
+ "--format",
229
+ "fmt",
230
+ type=click.Choice(["json", "yaml"]),
231
+ default="json",
232
+ help="Output format",
233
+ )
234
+ @click.option(
235
+ "--since",
236
+ type=click.DateTime(formats=["%Y-%m-%d"]),
237
+ help="Only include entries from this date onward (YYYY-MM-DD)",
238
+ )
239
+ @click.option(
240
+ "--category",
241
+ type=click.Choice(CATEGORIES),
242
+ help="Filter to a specific category",
243
+ )
244
+ def distill(output: str | None, fmt: str, since: datetime | None, category: str | None):
245
+ """Extract patterns from all buildlog entries.
246
+
247
+ Parses the Improvements section of each buildlog entry and aggregates
248
+ insights into structured output (JSON or YAML).
249
+
250
+ Examples:
251
+
252
+ buildlog distill # JSON to stdout
253
+ buildlog distill -o patterns.json # Write to file
254
+ buildlog distill --format yaml # YAML output
255
+ buildlog distill --since 2026-01-01 # Filter by date
256
+ buildlog distill --category workflow # Filter by category
257
+ """
258
+ buildlog_dir = Path("buildlog")
259
+
260
+ if not buildlog_dir.exists():
261
+ click.echo("No buildlog/ directory found. Run 'buildlog init' first.", err=True)
262
+ raise SystemExit(1)
263
+
264
+ # Convert datetime to date if provided
265
+ since_date = since.date() if since else None
266
+
267
+ # Run distillation
268
+ try:
269
+ result = distill_all(buildlog_dir, since=since_date, category_filter=category)
270
+ except Exception as e:
271
+ click.echo(f"Failed to distill entries: {e}", err=True)
272
+ raise SystemExit(1)
273
+
274
+ # Format output
275
+ try:
276
+ formatted = format_output(result, fmt)
277
+ except ImportError as e:
278
+ click.echo(str(e), err=True)
279
+ raise SystemExit(1)
280
+
281
+ # Write output
282
+ if output:
283
+ output_path = Path(output)
284
+ try:
285
+ output_path.write_text(formatted, encoding="utf-8")
286
+ click.echo(f"Wrote {result.statistics.get('total_patterns', 0)} patterns to {output_path}")
287
+ except Exception as e:
288
+ click.echo(f"Failed to write output: {e}", err=True)
289
+ raise SystemExit(1)
290
+ else:
291
+ click.echo(formatted)
292
+
293
+
294
+ @main.command()
295
+ @click.option("--json", "output_json", is_flag=True, help="Output as JSON")
296
+ @click.option("--detailed", is_flag=True, help="Show detailed breakdown including top sources")
297
+ @click.option("--since", "since_date", default=None, help="Only include entries since date (YYYY-MM-DD)")
298
+ def stats(output_json: bool, detailed: bool, since_date: str | None):
299
+ """Show buildlog statistics and analytics.
300
+
301
+ Provides insights on buildlog usage, coverage, and quality.
302
+
303
+ Examples:
304
+
305
+ buildlog stats # Terminal dashboard
306
+ buildlog stats --json # JSON output for scripts
307
+ buildlog stats --detailed # Include top sources
308
+ buildlog stats --since 2026-01-01
309
+ """
310
+ buildlog_dir = Path("buildlog")
311
+
312
+ if not buildlog_dir.exists():
313
+ click.echo("No buildlog/ directory found. Run 'buildlog init' first.", err=True)
314
+ raise SystemExit(1)
315
+
316
+ # Parse since date if provided
317
+ parsed_since = None
318
+ if since_date:
319
+ try:
320
+ parsed_since = datetime.strptime(since_date, "%Y-%m-%d").date()
321
+ except ValueError:
322
+ click.echo("Invalid date format. Use YYYY-MM-DD.", err=True)
323
+ raise SystemExit(1)
324
+
325
+ # Calculate stats
326
+ stats_data = calculate_stats(buildlog_dir, since_date=parsed_since)
327
+
328
+ # Output in requested format
329
+ if output_json:
330
+ click.echo(format_json(stats_data))
331
+ else:
332
+ click.echo(format_dashboard(stats_data, detailed=detailed))
333
+
334
+
335
+ @main.command()
336
+ @click.option("--output", "-o", type=click.Path(), help="Output file (default: stdout)")
337
+ @click.option(
338
+ "--format",
339
+ "fmt",
340
+ type=click.Choice(["yaml", "json", "markdown", "rules", "settings"]),
341
+ default="yaml",
342
+ help="Output format: yaml, json, markdown, rules (CLAUDE.md), settings (.claude/settings.json)",
343
+ )
344
+ @click.option(
345
+ "--min-frequency",
346
+ type=int,
347
+ default=1,
348
+ help="Only include skills seen at least this many times",
349
+ )
350
+ @click.option(
351
+ "--since",
352
+ type=click.DateTime(formats=["%Y-%m-%d"]),
353
+ help="Only include entries from this date onward (YYYY-MM-DD)",
354
+ )
355
+ @click.option(
356
+ "--embeddings",
357
+ type=click.Choice(["token", "sentence-transformers", "openai"]),
358
+ default=None,
359
+ help="Embedding backend for semantic deduplication",
360
+ )
361
+ def skills(
362
+ output: str | None,
363
+ fmt: str,
364
+ min_frequency: int,
365
+ since: datetime | None,
366
+ embeddings: str | None,
367
+ ):
368
+ """Generate agent-consumable skills from buildlog patterns.
369
+
370
+ Transforms distilled patterns into actionable rules with deduplication,
371
+ confidence scoring, and stable IDs.
372
+
373
+ Examples:
374
+
375
+ buildlog skills # YAML to stdout
376
+ buildlog skills -o skills.yml # Write to file
377
+ buildlog skills --format markdown # For CLAUDE.md injection
378
+ buildlog skills --min-frequency 2 # Only repeated patterns
379
+ buildlog skills --embeddings sentence-transformers # Semantic dedup
380
+
381
+ Embedding backends:
382
+ token (default): Fast, no dependencies, token-based similarity
383
+ sentence-transformers: Local semantic embeddings (pip install buildlog[embeddings])
384
+ openai: OpenAI API embeddings (requires OPENAI_API_KEY)
385
+ """
386
+ buildlog_dir = Path("buildlog")
387
+
388
+ if not buildlog_dir.exists():
389
+ click.echo("No buildlog/ directory found. Run 'buildlog init' first.", err=True)
390
+ raise SystemExit(1)
391
+
392
+ # Convert datetime to date if provided
393
+ since_date = since.date() if since else None
394
+
395
+ # Generate skills
396
+ try:
397
+ skill_set = generate_skills(
398
+ buildlog_dir,
399
+ min_frequency=min_frequency,
400
+ since_date=since_date,
401
+ embedding_backend=embeddings,
402
+ )
403
+ except ImportError as e:
404
+ click.echo(f"Missing dependency: {e}", err=True)
405
+ raise SystemExit(1)
406
+ except Exception as e:
407
+ click.echo(f"Failed to generate skills: {e}", err=True)
408
+ raise SystemExit(1)
409
+
410
+ # Format output
411
+ try:
412
+ formatted = format_skills(skill_set, fmt)
413
+ except ImportError as e:
414
+ click.echo(str(e), err=True)
415
+ raise SystemExit(1)
416
+ except ValueError as e:
417
+ click.echo(str(e), err=True)
418
+ raise SystemExit(1)
419
+
420
+ # Write output
421
+ if output:
422
+ output_path = Path(output)
423
+ try:
424
+ output_path.write_text(formatted, encoding="utf-8")
425
+ click.echo(
426
+ f"Wrote {skill_set.total_skills} skills to {output_path} "
427
+ f"(from {skill_set.source_entries} entries)"
428
+ )
429
+ except Exception as e:
430
+ click.echo(f"Failed to write output: {e}", err=True)
431
+ raise SystemExit(1)
432
+ else:
433
+ click.echo(formatted)
434
+
435
+
436
+ if __name__ == "__main__":
437
+ main()
@@ -0,0 +1,25 @@
1
+ """Core operations for buildlog skill management."""
2
+
3
+ from buildlog.core.operations import (
4
+ DiffResult,
5
+ PromoteResult,
6
+ RejectResult,
7
+ StatusResult,
8
+ diff,
9
+ find_skills_by_ids,
10
+ promote,
11
+ reject,
12
+ status,
13
+ )
14
+
15
+ __all__ = [
16
+ "StatusResult",
17
+ "PromoteResult",
18
+ "RejectResult",
19
+ "DiffResult",
20
+ "status",
21
+ "promote",
22
+ "reject",
23
+ "diff",
24
+ "find_skills_by_ids",
25
+ ]