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.
- brainlayer/__init__.py +3 -0
- brainlayer/cli/__init__.py +1545 -0
- brainlayer/cli/wizard.py +132 -0
- brainlayer/cli_new.py +151 -0
- brainlayer/client.py +164 -0
- brainlayer/clustering.py +736 -0
- brainlayer/daemon.py +1105 -0
- brainlayer/dashboard/README.md +129 -0
- brainlayer/dashboard/__init__.py +5 -0
- brainlayer/dashboard/app.py +151 -0
- brainlayer/dashboard/search.py +229 -0
- brainlayer/dashboard/views.py +230 -0
- brainlayer/embeddings.py +131 -0
- brainlayer/engine.py +550 -0
- brainlayer/index_new.py +87 -0
- brainlayer/mcp/__init__.py +1558 -0
- brainlayer/migrate.py +205 -0
- brainlayer/paths.py +43 -0
- brainlayer/pipeline/__init__.py +47 -0
- brainlayer/pipeline/analyze_communication.py +508 -0
- brainlayer/pipeline/brain_graph.py +567 -0
- brainlayer/pipeline/chat_tags.py +63 -0
- brainlayer/pipeline/chunk.py +422 -0
- brainlayer/pipeline/classify.py +472 -0
- brainlayer/pipeline/cluster_sampling.py +73 -0
- brainlayer/pipeline/enrichment.py +810 -0
- brainlayer/pipeline/extract.py +66 -0
- brainlayer/pipeline/extract_claude_desktop.py +149 -0
- brainlayer/pipeline/extract_corrections.py +231 -0
- brainlayer/pipeline/extract_markdown.py +195 -0
- brainlayer/pipeline/extract_whatsapp.py +227 -0
- brainlayer/pipeline/git_overlay.py +301 -0
- brainlayer/pipeline/longitudinal_analyzer.py +568 -0
- brainlayer/pipeline/obsidian_export.py +455 -0
- brainlayer/pipeline/operation_grouping.py +486 -0
- brainlayer/pipeline/plan_linking.py +313 -0
- brainlayer/pipeline/sanitize.py +549 -0
- brainlayer/pipeline/semantic_style.py +574 -0
- brainlayer/pipeline/session_enrichment.py +472 -0
- brainlayer/pipeline/style_embed.py +67 -0
- brainlayer/pipeline/style_index.py +139 -0
- brainlayer/pipeline/temporal_chains.py +203 -0
- brainlayer/pipeline/time_batcher.py +248 -0
- brainlayer/pipeline/unified_timeline.py +569 -0
- brainlayer/storage.py +66 -0
- brainlayer/store.py +155 -0
- brainlayer/taxonomy.json +80 -0
- brainlayer/vector_store.py +1891 -0
- brainlayer-1.0.0.dist-info/METADATA +313 -0
- brainlayer-1.0.0.dist-info/RECORD +53 -0
- brainlayer-1.0.0.dist-info/WHEEL +4 -0
- brainlayer-1.0.0.dist-info/entry_points.txt +4 -0
- 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
|