brainlayer 1.0.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.
Files changed (53) hide show
  1. brainlayer/__init__.py +3 -0
  2. brainlayer/cli/__init__.py +1545 -0
  3. brainlayer/cli/wizard.py +132 -0
  4. brainlayer/cli_new.py +151 -0
  5. brainlayer/client.py +164 -0
  6. brainlayer/clustering.py +736 -0
  7. brainlayer/daemon.py +1105 -0
  8. brainlayer/dashboard/README.md +129 -0
  9. brainlayer/dashboard/__init__.py +5 -0
  10. brainlayer/dashboard/app.py +151 -0
  11. brainlayer/dashboard/search.py +229 -0
  12. brainlayer/dashboard/views.py +230 -0
  13. brainlayer/embeddings.py +131 -0
  14. brainlayer/engine.py +550 -0
  15. brainlayer/index_new.py +87 -0
  16. brainlayer/mcp/__init__.py +1558 -0
  17. brainlayer/migrate.py +205 -0
  18. brainlayer/paths.py +43 -0
  19. brainlayer/pipeline/__init__.py +47 -0
  20. brainlayer/pipeline/analyze_communication.py +508 -0
  21. brainlayer/pipeline/brain_graph.py +567 -0
  22. brainlayer/pipeline/chat_tags.py +63 -0
  23. brainlayer/pipeline/chunk.py +422 -0
  24. brainlayer/pipeline/classify.py +472 -0
  25. brainlayer/pipeline/cluster_sampling.py +73 -0
  26. brainlayer/pipeline/enrichment.py +810 -0
  27. brainlayer/pipeline/extract.py +66 -0
  28. brainlayer/pipeline/extract_claude_desktop.py +149 -0
  29. brainlayer/pipeline/extract_corrections.py +231 -0
  30. brainlayer/pipeline/extract_markdown.py +195 -0
  31. brainlayer/pipeline/extract_whatsapp.py +227 -0
  32. brainlayer/pipeline/git_overlay.py +301 -0
  33. brainlayer/pipeline/longitudinal_analyzer.py +568 -0
  34. brainlayer/pipeline/obsidian_export.py +455 -0
  35. brainlayer/pipeline/operation_grouping.py +486 -0
  36. brainlayer/pipeline/plan_linking.py +313 -0
  37. brainlayer/pipeline/sanitize.py +549 -0
  38. brainlayer/pipeline/semantic_style.py +574 -0
  39. brainlayer/pipeline/session_enrichment.py +472 -0
  40. brainlayer/pipeline/style_embed.py +67 -0
  41. brainlayer/pipeline/style_index.py +139 -0
  42. brainlayer/pipeline/temporal_chains.py +203 -0
  43. brainlayer/pipeline/time_batcher.py +248 -0
  44. brainlayer/pipeline/unified_timeline.py +569 -0
  45. brainlayer/storage.py +66 -0
  46. brainlayer/store.py +155 -0
  47. brainlayer/taxonomy.json +80 -0
  48. brainlayer/vector_store.py +1891 -0
  49. brainlayer-1.0.0.dist-info/METADATA +313 -0
  50. brainlayer-1.0.0.dist-info/RECORD +53 -0
  51. brainlayer-1.0.0.dist-info/WHEEL +4 -0
  52. brainlayer-1.0.0.dist-info/entry_points.txt +4 -0
  53. brainlayer-1.0.0.dist-info/licenses/LICENSE +190 -0
@@ -0,0 +1,455 @@
1
+ """Obsidian Export Pipeline — Generate Obsidian vault from BrainLayer data.
2
+
3
+ Phase 9: Export enriched knowledge graph as Obsidian-compatible
4
+ markdown notes with YAML frontmatter and wikilinks.
5
+
6
+ Usage:
7
+ from brainlayer.pipeline.obsidian_export import export_obsidian
8
+ export_obsidian(vector_store, vault_path="~/.brainlayer-brain")
9
+ """
10
+
11
+ import logging
12
+ from pathlib import Path
13
+ from typing import Any, Dict, List, Optional
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ DEFAULT_VAULT = Path.home() / ".brainlayer-brain" / "BrainLayer"
18
+
19
+
20
+ def _sanitize_filename(name: str) -> str:
21
+ """Make a string safe for use as a filename."""
22
+ # Strip common extensions to avoid double .md.md
23
+ for ext in (
24
+ ".md",
25
+ ".ts",
26
+ ".tsx",
27
+ ".js",
28
+ ".py",
29
+ ".json",
30
+ ".css",
31
+ ".svg",
32
+ ".txt",
33
+ ".yaml",
34
+ ".toml",
35
+ ):
36
+ if name.endswith(ext):
37
+ name = name[: -len(ext)]
38
+ break
39
+ bad = '<>:"/\\|?*'
40
+ for c in bad:
41
+ name = name.replace(c, "-")
42
+ name = name.strip(". ")
43
+ # Clean up ugly directory names
44
+ if name == "__tests__":
45
+ name = "tests"
46
+ return name or "unnamed"
47
+
48
+
49
+ def _format_date(ts: Optional[str]) -> str:
50
+ """Extract YYYY-MM-DD from ISO timestamp."""
51
+ if not ts:
52
+ return "unknown"
53
+ return ts[:10]
54
+
55
+
56
+ def _session_title(ctx: Dict[str, Any]) -> str:
57
+ """Generate a human-readable session title."""
58
+ date = _format_date(ctx.get("started_at"))
59
+ plan = ctx.get("plan_name") or ""
60
+ phase = ctx.get("plan_phase") or ""
61
+ sid = (ctx.get("session_id") or "")[:8]
62
+
63
+ # Use plan+phase if available, otherwise branch
64
+ if plan and phase:
65
+ label = f"{plan}-{phase}"
66
+ elif plan:
67
+ label = plan
68
+ else:
69
+ branch = ctx.get("branch") or ""
70
+ label = branch.replace("feature/", "").replace("llm-", "").replace("componentize-", "")
71
+
72
+ if label:
73
+ return f"{date} {_sanitize_filename(label)} ({sid})"
74
+ return f"{date} session ({sid})"
75
+
76
+
77
+ def generate_session_note(
78
+ ctx: Dict[str, Any],
79
+ operations: List[Dict[str, Any]],
80
+ files: List[str],
81
+ ) -> str:
82
+ """Generate a session note with YAML frontmatter."""
83
+ date = _format_date(ctx.get("started_at"))
84
+ sid = ctx.get("session_id", "")[:8]
85
+ branch = ctx.get("branch") or ""
86
+ pr = ctx.get("pr_number")
87
+ plan = ctx.get("plan_name") or ""
88
+ phase = ctx.get("plan_phase") or ""
89
+
90
+ # Build tags from operations
91
+ op_types = list({o.get("operation_type", "unknown") for o in operations})
92
+
93
+ # YAML frontmatter
94
+ lines = [
95
+ "---",
96
+ f"date: {date}",
97
+ f"session_id: {sid}",
98
+ ]
99
+ if branch:
100
+ lines.append(f"branch: {branch}")
101
+ if pr:
102
+ lines.append(f"pr: {pr}")
103
+ if plan:
104
+ lines.append(f"plan: {plan}")
105
+ if phase:
106
+ lines.append(f"phase: {phase}")
107
+ if files:
108
+ file_list = ", ".join(f.split("/")[-1] for f in files[:20])
109
+ lines.append(f"files: [{file_list}]")
110
+ if op_types:
111
+ tags = ", ".join(op_types)
112
+ lines.append(f"tags: [{tags}]")
113
+ lines.append(f"operations: {len(operations)}")
114
+ lines.append("---")
115
+ lines.append("")
116
+
117
+ # Title
118
+ title_parts = []
119
+ if plan:
120
+ title_parts.append(plan)
121
+ if phase:
122
+ title_parts.append(phase)
123
+ if branch:
124
+ title_parts.append(branch.replace("feature/", ""))
125
+ title = " / ".join(title_parts) if title_parts else f"Session {sid}"
126
+ lines.append(f"# {title}")
127
+ lines.append("")
128
+
129
+ # Operations (limit to 50 most significant)
130
+ if operations:
131
+ # Prioritize non-research ops, then by chunk count
132
+ sorted_ops = sorted(
133
+ operations,
134
+ key=lambda o: (
135
+ o.get("operation_type") == "research",
136
+ -len(o.get("chunk_ids") or []),
137
+ ),
138
+ )
139
+ display_ops = sorted_ops[:50]
140
+ lines.append(f"## Operations ({len(operations)} total, showing {len(display_ops)})")
141
+ lines.append("")
142
+ for i, op in enumerate(display_ops, 1):
143
+ otype = op.get("operation_type", "?")
144
+ summary = op.get("summary", "")
145
+ outcome = op.get("outcome", "")
146
+ icon = {
147
+ "research": "🔍",
148
+ "edit-cycle": "✏️",
149
+ "feature-cycle": "🚀",
150
+ "debug": "🐛",
151
+ "review": "👀",
152
+ }.get(otype, "📋")
153
+ line = f"{i}. {icon} **{otype}**"
154
+ if summary:
155
+ line += f" — {summary}"
156
+ if outcome:
157
+ line += f" [{outcome}]"
158
+ lines.append(line)
159
+ lines.append("")
160
+
161
+ # Related files
162
+ if files:
163
+ lines.append("## Files Touched")
164
+ lines.append("")
165
+ for f in files[:30]:
166
+ fname = f.split("/")[-1]
167
+ lines.append(f"- [[{fname}]]")
168
+ lines.append("")
169
+
170
+ # Related links
171
+ lines.append("## Related")
172
+ lines.append("")
173
+ if pr:
174
+ lines.append(f"- [[PR-{pr}]]")
175
+ if plan:
176
+ lines.append(f"- [[{plan}]]")
177
+ lines.append("")
178
+
179
+ return "\n".join(lines)
180
+
181
+
182
+ def generate_file_note(
183
+ file_path: str,
184
+ interactions: List[Dict[str, Any]],
185
+ ) -> str:
186
+ """Generate a file note with interaction timeline."""
187
+ fname = file_path.split("/")[-1]
188
+ last_ts = ""
189
+ if interactions:
190
+ last_ts = _format_date(interactions[-1].get("timestamp"))
191
+
192
+ lines = [
193
+ "---",
194
+ f"file: {file_path}",
195
+ f"interactions: {len(interactions)}",
196
+ f"last_modified: {last_ts}",
197
+ "---",
198
+ "",
199
+ f"# {fname}",
200
+ "",
201
+ f"Full path: `{file_path}`",
202
+ "",
203
+ "## Interaction Timeline",
204
+ "",
205
+ "| Date | Action | Branch | Session |",
206
+ "|------|--------|--------|---------|",
207
+ ]
208
+
209
+ seen: set = set()
210
+ for i in interactions[:50]:
211
+ ts = _format_date(i.get("timestamp"))
212
+ action = i.get("action", "?")
213
+ branch = i.get("branch") or ""
214
+ sid = (i.get("session_id") or "")[:8]
215
+ key = (ts, action, sid)
216
+ if key in seen:
217
+ continue
218
+ seen.add(key)
219
+ lines.append(f"| {ts} | {action} | {branch} | {sid} |")
220
+
221
+ lines.append("")
222
+
223
+ # Dataview query for sessions that touched this file
224
+ lines.append("## Sessions (Dataview)")
225
+ lines.append("")
226
+ lines.append("```dataview")
227
+ lines.append("TABLE date, tags, plan, phase")
228
+ lines.append('FROM "Sessions"')
229
+ lines.append(f'WHERE contains(files, "{fname}")')
230
+ lines.append("SORT date DESC")
231
+ lines.append("```")
232
+ lines.append("")
233
+
234
+ return "\n".join(lines)
235
+
236
+
237
+ def generate_plan_note(
238
+ plan_name: str,
239
+ sessions: List[Dict[str, Any]],
240
+ ) -> str:
241
+ """Generate a plan note."""
242
+ lines = [
243
+ "---",
244
+ f"plan: {plan_name}",
245
+ f"sessions: {len(sessions)}",
246
+ "---",
247
+ "",
248
+ f"# {plan_name}",
249
+ "",
250
+ "## Sessions",
251
+ "",
252
+ "| Date | Phase | Branch | Session |",
253
+ "|------|-------|--------|---------|",
254
+ ]
255
+
256
+ for s in sessions:
257
+ date = _format_date(s.get("started_at"))
258
+ phase = s.get("plan_phase") or ""
259
+ branch = s.get("branch") or ""
260
+ title = _session_title(s)
261
+ lines.append(f"| {date} | {phase} | {branch} | [[{title}]] |")
262
+
263
+ lines.append("")
264
+
265
+ # Dataview query
266
+ lines.append("## All Sessions (Dataview)")
267
+ lines.append("")
268
+ lines.append("```dataview")
269
+ lines.append("TABLE date, phase, branch, tags")
270
+ lines.append('FROM "Sessions"')
271
+ lines.append(f'WHERE plan = "{plan_name}"')
272
+ lines.append("SORT date ASC")
273
+ lines.append("```")
274
+ lines.append("")
275
+
276
+ return "\n".join(lines)
277
+
278
+
279
+ def generate_dashboard(dashboard_type: str) -> str:
280
+ """Generate a Dataview dashboard note."""
281
+ if dashboard_type == "recent":
282
+ return "\n".join(
283
+ [
284
+ "---",
285
+ "dashboard: recent-sessions",
286
+ "---",
287
+ "",
288
+ "# Recent Sessions",
289
+ "",
290
+ "```dataview",
291
+ "TABLE date, tags, plan, phase, operations",
292
+ 'FROM "Sessions"',
293
+ "WHERE date >= date(today) - dur(7 days)",
294
+ "SORT date DESC",
295
+ "```",
296
+ "",
297
+ ]
298
+ )
299
+ elif dashboard_type == "files":
300
+ return "\n".join(
301
+ [
302
+ "---",
303
+ "dashboard: most-modified",
304
+ "---",
305
+ "",
306
+ "# Most Modified Files",
307
+ "",
308
+ "```dataview",
309
+ "TABLE interactions, last_modified",
310
+ 'FROM "Files"',
311
+ "SORT interactions DESC",
312
+ "LIMIT 20",
313
+ "```",
314
+ "",
315
+ ]
316
+ )
317
+ elif dashboard_type == "plans":
318
+ return "\n".join(
319
+ [
320
+ "---",
321
+ "dashboard: plans",
322
+ "---",
323
+ "",
324
+ "# Plans Overview",
325
+ "",
326
+ "```dataview",
327
+ "TABLE sessions, plan",
328
+ 'FROM "Plans"',
329
+ "SORT sessions DESC",
330
+ "```",
331
+ "",
332
+ ]
333
+ )
334
+ return ""
335
+
336
+
337
+ def export_obsidian(
338
+ vector_store: Any,
339
+ vault_path: Optional[str] = None,
340
+ project: Optional[str] = None,
341
+ force: bool = False,
342
+ ) -> Dict[str, int]:
343
+ """Export BrainLayer data to Obsidian vault.
344
+
345
+ Args:
346
+ vector_store: VectorStore instance
347
+ vault_path: Path to Obsidian vault root
348
+ project: Filter to specific project
349
+ force: Overwrite existing notes
350
+
351
+ Returns:
352
+ Dict with counts of generated notes
353
+ """
354
+ vault = Path(vault_path) if vault_path else DEFAULT_VAULT
355
+ counts = {
356
+ "sessions": 0,
357
+ "files": 0,
358
+ "plans": 0,
359
+ "dashboards": 0,
360
+ }
361
+
362
+ # Create directory structure
363
+ dirs = ["Sessions", "Files", "Plans", "Dashboards"]
364
+ for d in dirs:
365
+ (vault / d).mkdir(parents=True, exist_ok=True)
366
+
367
+ # 1. Export session notes
368
+ cursor = vector_store.conn.cursor()
369
+ query = "SELECT session_id FROM session_context"
370
+ params: list = []
371
+ if project:
372
+ query += " WHERE project = ?"
373
+ params.append(project)
374
+ query += " ORDER BY started_at ASC"
375
+
376
+ session_ids = [r[0] for r in cursor.execute(query, params)]
377
+ logger.info("Exporting %d sessions", len(session_ids))
378
+
379
+ for sid in session_ids:
380
+ ctx = vector_store.get_session_context(sid)
381
+ if not ctx:
382
+ continue
383
+
384
+ title = _session_title(ctx)
385
+ note_path = vault / "Sessions" / f"{title}.md"
386
+ if note_path.exists() and not force:
387
+ continue
388
+
389
+ # Get operations for this session
390
+ ops = vector_store.get_session_operations(sid)
391
+
392
+ # Get files touched
393
+ file_interactions = list(
394
+ cursor.execute(
395
+ "SELECT DISTINCT file_path FROM file_interactions WHERE session_id = ?",
396
+ (sid,),
397
+ )
398
+ )
399
+ files = [r[0] for r in file_interactions]
400
+
401
+ content = generate_session_note(ctx, ops, files)
402
+ note_path.write_text(content)
403
+ counts["sessions"] += 1
404
+
405
+ # 2. Export file notes (top 100 most-interacted files)
406
+ file_query = "SELECT file_path, COUNT(*) as cnt FROM file_interactions"
407
+ file_params: list = []
408
+ if project:
409
+ file_query += " WHERE project = ?"
410
+ file_params.append(project)
411
+ file_query += " GROUP BY file_path ORDER BY cnt DESC LIMIT 100"
412
+
413
+ top_files = list(cursor.execute(file_query, file_params))
414
+ logger.info("Exporting %d file notes", len(top_files))
415
+
416
+ for row in top_files:
417
+ fp = row[0]
418
+ fname = _sanitize_filename(fp.split("/")[-1])
419
+ note_path = vault / "Files" / f"{fname}.md"
420
+ if note_path.exists() and not force:
421
+ continue
422
+
423
+ timeline = vector_store.get_file_timeline(fp, project=project, limit=50)
424
+ content = generate_file_note(fp, timeline)
425
+ note_path.write_text(content)
426
+ counts["files"] += 1
427
+
428
+ # 3. Export plan notes
429
+ plan_stats = vector_store.get_plan_linking_stats()
430
+ for plan_name in plan_stats.get("plans", {}):
431
+ note_path = vault / "Plans" / f"{plan_name}.md"
432
+ if note_path.exists() and not force:
433
+ continue
434
+
435
+ sessions = vector_store.get_sessions_by_plan(plan_name=plan_name, project=project)
436
+ content = generate_plan_note(plan_name, sessions)
437
+ note_path.write_text(content)
438
+ counts["plans"] += 1
439
+
440
+ # 4. Generate dashboards
441
+ dashboards = {
442
+ "Recent Sessions": "recent",
443
+ "Most Modified Files": "files",
444
+ "Plans Overview": "plans",
445
+ }
446
+ for name, dtype in dashboards.items():
447
+ note_path = vault / "Dashboards" / f"{name}.md"
448
+ if note_path.exists() and not force:
449
+ continue
450
+ content = generate_dashboard(dtype)
451
+ if content:
452
+ note_path.write_text(content)
453
+ counts["dashboards"] += 1
454
+
455
+ return counts