add skills
This commit is contained in:
@@ -0,0 +1,469 @@
|
||||
---
|
||||
name: wiki-dashboard
|
||||
description: >
|
||||
Create dynamic, queryable dashboard views of the Obsidian vault using Obsidian Bases or Dataview.
|
||||
Use this skill when the user says "create a dashboard", "vault dashboard", "show all X as a table",
|
||||
"dynamic view", "query my vault", "build a content index", "show me all concepts/entities/projects",
|
||||
or wants a structured, auto-updating view of their wiki content.
|
||||
Bases is native to Obsidian 1.8+ (no plugin needed). Dataview requires the community plugin.
|
||||
---
|
||||
|
||||
# Wiki Dashboard — Dynamic Vault Views
|
||||
|
||||
Two tools available: **Obsidian Bases** (native, GUI-driven, no plugin) and **Dataview** (community plugin, SQL-like, more powerful). Check which the user has and prefer Bases unless they ask for Dataview or need GROUP BY / computed columns.
|
||||
|
||||
## Before You Start
|
||||
|
||||
1. **Resolve config** — follow the Config Resolution Protocol in `llm-wiki/SKILL.md` (walk up CWD for `.env` → `~/.obsidian-wiki/config` → prompt setup). This gives `OBSIDIAN_VAULT_PATH`.
|
||||
2. Read `$OBSIDIAN_VAULT_PATH/index.md` to understand what categories and pages exist.
|
||||
3. Ask the user what they want to view if not specified — folder, tag, category, date range?
|
||||
4. Ask if they have Dataview installed if you're unsure which tool to use.
|
||||
|
||||
---
|
||||
|
||||
## Option A — Obsidian Bases (`.base` files)
|
||||
|
||||
Bases are YAML files that define live views over vault notes. Native to Obsidian 1.8+, no plugin needed.
|
||||
|
||||
### Official canonical schema
|
||||
|
||||
Top-level keys:
|
||||
|
||||
```yaml
|
||||
filters: # Global filter applied to all views (expression strings under and/or/not)
|
||||
formulas: # Named computed properties — referenced as formula.<name>
|
||||
properties: # Display config per property — sets displayName for column headers
|
||||
summaries: # Aggregation formulas (e.g. mean, sum)
|
||||
views: # Array of view definitions (required)
|
||||
```
|
||||
|
||||
Each item in `views:`:
|
||||
|
||||
```yaml
|
||||
views:
|
||||
- type: table # table | list | cards | map
|
||||
name: "View Name" # display label
|
||||
limit: 50 # optional max rows
|
||||
order: # column display order (list of property/formula names)
|
||||
- file.name
|
||||
- note.updated
|
||||
groupBy: # grouping — goes INSIDE the view, NOT at top level
|
||||
property: note.tags
|
||||
direction: ASC # ASC | DESC
|
||||
filters: # view-specific filter (merges with global filters)
|
||||
and:
|
||||
- 'note.status != "done"'
|
||||
summaries:
|
||||
formula.myFormula: Average
|
||||
```
|
||||
|
||||
### Filter syntax — CRITICAL
|
||||
|
||||
**Filters use expression strings, not typed objects.** Always wrap in `and:`, `or:`, or `not:` — a bare list causes a "may only have one of and/or/not keys" parse error.
|
||||
|
||||
```yaml
|
||||
# CORRECT
|
||||
filters:
|
||||
and:
|
||||
- file.inFolder("concepts")
|
||||
|
||||
# WRONG — typed objects (parse error)
|
||||
filters:
|
||||
- type: folder
|
||||
folder: concepts
|
||||
```
|
||||
|
||||
Filters support nesting:
|
||||
```yaml
|
||||
filters:
|
||||
or:
|
||||
- file.hasTag("book")
|
||||
- and:
|
||||
- file.inFolder("concepts")
|
||||
- file.hasTag("research")
|
||||
- not:
|
||||
- file.hasTag("archived")
|
||||
```
|
||||
|
||||
### Property name conventions
|
||||
|
||||
Different contexts use different naming — confirmed from Obsidian's auto-reformat behaviour:
|
||||
|
||||
| Context | Frontmatter field `tags` | File name | Formula |
|
||||
|---|---|---|---|
|
||||
| `properties:` keys | `note.tags` | `file.name` | `formula.<name>` |
|
||||
| `order:` values | `tags` (bare) | `file.name` | `formula.<name>` |
|
||||
| `groupBy.property:` | `tags` (bare) | `file.name` | — |
|
||||
| `filters:` expressions | `file.hasTag(...)` / `note.tags` | `file.name` | `formula.<name>` |
|
||||
| `formulas:` expressions | `note.tags`, `note.updated` | `file.name` | — |
|
||||
|
||||
### Basic table — folder filter
|
||||
|
||||
```yaml
|
||||
filters:
|
||||
and:
|
||||
- file.inFolder("concepts")
|
||||
properties:
|
||||
file.name:
|
||||
displayName: Page
|
||||
note.tags:
|
||||
displayName: Tags
|
||||
note.summary:
|
||||
displayName: Summary
|
||||
note.updated:
|
||||
displayName: Updated
|
||||
views:
|
||||
- type: table
|
||||
name: Table
|
||||
order:
|
||||
- file.name
|
||||
- tags
|
||||
- summary
|
||||
- updated
|
||||
```
|
||||
|
||||
### Cards view — folder filter
|
||||
|
||||
```yaml
|
||||
filters:
|
||||
and:
|
||||
- file.inFolder("entities")
|
||||
properties:
|
||||
file.name:
|
||||
displayName: Entity
|
||||
note.title:
|
||||
displayName: Full Name
|
||||
note.tags:
|
||||
displayName: Tags
|
||||
note.summary:
|
||||
displayName: Summary
|
||||
views:
|
||||
- type: cards
|
||||
name: Cards
|
||||
order:
|
||||
- file.name
|
||||
- title
|
||||
- tags
|
||||
- summary
|
||||
```
|
||||
|
||||
### Group by property — groupBy goes INSIDE the view
|
||||
|
||||
When `groupBy` is set, **omit that property from `order:`** — it becomes the group header row and adding it as a column too causes duplication.
|
||||
|
||||
```yaml
|
||||
filters:
|
||||
and:
|
||||
- file.inFolder("concepts")
|
||||
properties:
|
||||
file.name:
|
||||
displayName: Concept
|
||||
note.summary:
|
||||
displayName: Summary
|
||||
note.updated:
|
||||
displayName: Updated
|
||||
views:
|
||||
- type: table
|
||||
name: By Domain
|
||||
groupBy:
|
||||
property: tags # bare property name, no note. prefix
|
||||
direction: ASC
|
||||
order:
|
||||
- file.name # do NOT include tags here — already the group header
|
||||
- summary
|
||||
- updated
|
||||
```
|
||||
|
||||
### Tag filter
|
||||
|
||||
```yaml
|
||||
filters:
|
||||
and:
|
||||
- file.hasTag("machine-learning")
|
||||
properties:
|
||||
file.name:
|
||||
displayName: Page
|
||||
note.category:
|
||||
displayName: Category
|
||||
note.summary:
|
||||
displayName: Summary
|
||||
views:
|
||||
- type: table
|
||||
name: Table
|
||||
order:
|
||||
- file.name
|
||||
- category
|
||||
- summary
|
||||
```
|
||||
|
||||
### Multi-filter (folder AND tag)
|
||||
|
||||
```yaml
|
||||
filters:
|
||||
and:
|
||||
- file.inFolder("projects")
|
||||
- file.hasTag("active")
|
||||
properties:
|
||||
file.name:
|
||||
displayName: Project
|
||||
note.summary:
|
||||
displayName: Summary
|
||||
note.updated:
|
||||
displayName: Last Updated
|
||||
views:
|
||||
- type: cards
|
||||
name: Cards
|
||||
order:
|
||||
- file.name
|
||||
- summary
|
||||
- updated
|
||||
```
|
||||
|
||||
### OR filter (two folders)
|
||||
|
||||
```yaml
|
||||
filters:
|
||||
or:
|
||||
- file.inFolder("concepts")
|
||||
- file.inFolder("entities")
|
||||
properties:
|
||||
file.name:
|
||||
displayName: Page
|
||||
note.category:
|
||||
displayName: Category
|
||||
note.updated:
|
||||
displayName: Updated
|
||||
views:
|
||||
- type: table
|
||||
name: Table
|
||||
order:
|
||||
- file.name
|
||||
- category
|
||||
- updated
|
||||
```
|
||||
|
||||
### Computed column via formulas
|
||||
|
||||
```yaml
|
||||
filters:
|
||||
and:
|
||||
- file.inFolder("concepts")
|
||||
formulas:
|
||||
days_stale: "floor((now() - note.updated) / 86400000)"
|
||||
properties:
|
||||
file.name:
|
||||
displayName: Page
|
||||
note.updated:
|
||||
displayName: Updated
|
||||
formula.days_stale:
|
||||
displayName: Days Stale
|
||||
views:
|
||||
- type: table
|
||||
name: Stale
|
||||
order:
|
||||
- file.name
|
||||
- updated
|
||||
- formula.days_stale
|
||||
```
|
||||
|
||||
### Filter expression reference
|
||||
|
||||
| Expression | What it does |
|
||||
|---|---|
|
||||
| `file.inFolder("path")` | Pages in that folder |
|
||||
| `file.hasTag("tag")` | Pages with that tag (no `#` prefix) |
|
||||
| `file.hasLink("Note Name")` | Pages linking to a note |
|
||||
| `file.name == "note-name"` | Exact filename match |
|
||||
| `file.ext == "md"` | Filter by extension |
|
||||
| `note.propertyName` | Any frontmatter property |
|
||||
| `formula.formulaName` | A named formula result |
|
||||
| `now()` | Current timestamp in ms |
|
||||
|
||||
> **On Obsidian UI-generated format:** When Obsidian's GUI writes or reformats a `.base` file it may output a simplified shorthand with top-level `columns:`, `sort:`, and `view:` keys instead of the canonical schema. That format also works — Obsidian accepts both. Manually authored files should use the canonical schema above.
|
||||
|
||||
---
|
||||
|
||||
## Option B — Dataview (community plugin)
|
||||
|
||||
Dataview uses a SQL-like query language inside ` ```dataview ``` ` code blocks in any note. More powerful than Bases for computed columns, GROUP BY, and cross-folder queries.
|
||||
|
||||
### Basic table — folder
|
||||
|
||||
````markdown
|
||||
```dataview
|
||||
TABLE
|
||||
tags AS "Tags",
|
||||
summary AS "Summary",
|
||||
file.mtime AS "Last Modified"
|
||||
FROM "concepts"
|
||||
SORT file.mtime DESC
|
||||
```
|
||||
````
|
||||
|
||||
### Table with clickable links (TABLE WITHOUT ID)
|
||||
|
||||
````markdown
|
||||
```dataview
|
||||
TABLE WITHOUT ID
|
||||
file.link AS "Entity",
|
||||
tags AS "Tags",
|
||||
summary AS "Summary"
|
||||
FROM "entities"
|
||||
SORT file.name ASC
|
||||
```
|
||||
````
|
||||
|
||||
### GROUP BY — use `rows.` prefix after grouping
|
||||
|
||||
After `GROUP BY`, individual file properties must be prefixed with `rows.` — otherwise the column is empty or errors.
|
||||
|
||||
````markdown
|
||||
```dataview
|
||||
TABLE WITHOUT ID
|
||||
rows.file.link AS "Concept",
|
||||
rows.summary AS "Summary"
|
||||
FROM "concepts"
|
||||
GROUP BY tags[0] AS "Domain"
|
||||
```
|
||||
````
|
||||
|
||||
### Stale pages — use `file.mtime` for date math
|
||||
|
||||
Avoid `choice(updated, date(updated), file.mtime)` — mixed date formats in `updated` frontmatter cause arithmetic errors. `file.mtime` is always a valid DateTime.
|
||||
|
||||
````markdown
|
||||
```dataview
|
||||
TABLE WITHOUT ID
|
||||
file.link AS "Page",
|
||||
category AS "Type",
|
||||
file.mtime AS "Last Modified",
|
||||
(date(today) - file.mtime).days + " days" AS "Age"
|
||||
FROM "concepts" OR "entities" OR "projects"
|
||||
WHERE file.name != file.folder
|
||||
WHERE (date(today) - file.mtime).days > 30
|
||||
SORT (date(today) - file.mtime).days DESC
|
||||
```
|
||||
````
|
||||
|
||||
### Multi-folder query
|
||||
|
||||
````markdown
|
||||
```dataview
|
||||
TABLE
|
||||
summary AS "Summary",
|
||||
file.mtime AS "Last Modified"
|
||||
FROM "projects"
|
||||
WHERE file.name != file.folder
|
||||
SORT file.mtime DESC
|
||||
```
|
||||
````
|
||||
|
||||
### Dataview reference
|
||||
|
||||
| Clause | Usage |
|
||||
|---|---|
|
||||
| `FROM "folder"` | All notes in folder |
|
||||
| `FROM #tag` | All notes with tag |
|
||||
| `FROM "a" OR "b"` | Union of two folders |
|
||||
| `WHERE file.name != file.folder` | Exclude folder index pages |
|
||||
| `GROUP BY field AS "Label"` | Group rows — use `rows.` for properties after this |
|
||||
| `SORT field DESC` | Sort direction |
|
||||
| `file.link` | Clickable wikilink |
|
||||
| `file.mtime` | Last modified time (always valid DateTime) |
|
||||
| `(date(today) - file.mtime).days` | Days since last modification |
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Write the File
|
||||
|
||||
**Bases:** Target path `$OBSIDIAN_VAULT_PATH/_meta/<dashboard-name>.base`
|
||||
|
||||
**Dataview:** Write queries directly into any `.md` note. A dedicated dashboard note at `$OBSIDIAN_VAULT_PATH/_meta/dashboard.md` works well for multi-section views.
|
||||
|
||||
Slug examples:
|
||||
- "All concepts" → `_meta/concepts-index.base`
|
||||
- "Recent ingests" → `_meta/recent-ingests.base`
|
||||
- "Project overview" → `_meta/projects-overview.base`
|
||||
- "Stale pages" → `_meta/stale-pages.base`
|
||||
- "Full dashboard" → `_meta/dashboard.md`
|
||||
|
||||
Create `_meta/` if it doesn't exist yet.
|
||||
|
||||
## Step 4: Embed Bases (optional)
|
||||
|
||||
To embed a `.base` inside a note:
|
||||
|
||||
```markdown
|
||||
## Entities
|
||||
![[_meta/entities-tracker.base]]
|
||||
```
|
||||
|
||||
Ask before modifying an existing note.
|
||||
|
||||
## Step 5: Update Tracking
|
||||
|
||||
Append to `$OBSIDIAN_VAULT_PATH/log.md`:
|
||||
```
|
||||
- [TIMESTAMP] WIKI_DASHBOARD name="<slug>" tool=bases|dataview view=<type> filter="<description>"
|
||||
```
|
||||
|
||||
No manifest or index update needed — dashboards are live queries, not static pages.
|
||||
|
||||
## Common Dashboard Recipes
|
||||
|
||||
| Dashboard | Best tool | What it shows |
|
||||
|---|---|---|
|
||||
| **Content index** | Bases or Dataview | All pages grouped by category, sorted by updated |
|
||||
| **Entity tracker** | Bases (cards) | Entity pages as a visual card gallery |
|
||||
| **Concepts by domain** | Dataview | Concepts grouped by first tag using GROUP BY |
|
||||
| **Ingestion log** | Either | Pages sorted by `created` date |
|
||||
| **Stale content** | Dataview | Pages not touched in 30+ days with day count |
|
||||
| **Project overview** | Either | Project pages with last-sync date |
|
||||
| **Research tracker** | Dataview | Synthesis pages tagged `research` |
|
||||
|
||||
## Quality Checklist
|
||||
|
||||
- [ ] Bases: filters use expression strings under `and:`/`or:`/`not:`, never typed objects
|
||||
- [ ] Bases: `groupBy` goes inside the view definition — not as a top-level key
|
||||
- [ ] Bases: column headers set via `properties: <name>: displayName: "..."`, not `columns: [{title}]`
|
||||
- [ ] Bases: `formulas:` used for computed columns, referenced as `formula.<name>` in order/properties
|
||||
- [ ] Dataview: GROUP BY queries use `rows.property` not bare `property`
|
||||
- [ ] Dataview: date arithmetic uses `file.mtime`, not `choice(updated, ...)`
|
||||
- [ ] File written to `_meta/` with a descriptive slug
|
||||
- [ ] `log.md` updated
|
||||
- [ ] User told how to embed Bases (`![[_meta/<name>.base]]`) or open the dashboard note
|
||||
|
||||
## QMD Refresh After Vault Writes
|
||||
|
||||
QMD is a search index, not the source of truth. If `$QMD_WIKI_COLLECTION` is empty or unset, skip this step. Run it only after this skill has written or rewritten vault markdown. If QMD refresh fails, do not roll back the vault changes; report the QMD status separately.
|
||||
|
||||
Use `$QMD_CLI` if set; otherwise use `qmd`.
|
||||
|
||||
```bash
|
||||
${QMD_CLI:-qmd} update
|
||||
```
|
||||
|
||||
If the output says vectors are needed or embeddings may be stale, run:
|
||||
|
||||
```bash
|
||||
${QMD_CLI:-qmd} embed
|
||||
```
|
||||
|
||||
Verify the collection with either:
|
||||
|
||||
```bash
|
||||
${QMD_CLI:-qmd} ls "$QMD_WIKI_COLLECTION"
|
||||
```
|
||||
|
||||
or, when a specific page path is known:
|
||||
|
||||
```bash
|
||||
${QMD_CLI:-qmd} get "qmd://$QMD_WIKI_COLLECTION/<page>.md" -l 5
|
||||
```
|
||||
|
||||
Record one of:
|
||||
- `QMD refreshed: update + embed + verified`
|
||||
- `QMD refreshed: update only + verified`
|
||||
- `QMD skipped: QMD_WIKI_COLLECTION unset`
|
||||
- `QMD skipped: qmd CLI unavailable`
|
||||
- `QMD failed: <short error summary>`
|
||||
Reference in New Issue
Block a user