Skip to content

Instantly share code, notes, and snippets.

@dfinke
Created August 11, 2026 20:55
Show Gist options
  • Select an option

  • Save dfinke/3bfd3bebc38d92b0af2c15d85dd11e19 to your computer and use it in GitHub Desktop.

Select an option

Save dfinke/3bfd3bebc38d92b0af2c15d85dd11e19 to your computer and use it in GitHub Desktop.
[CmdletBinding()]
param(
[ValidateSet('init', 'ingest', 'index', 'query', 'lint', 'help')]
[string]$Operation = 'help',
[string]$Path,
[string]$Question,
[string]$Root = (Split-Path -Parent $PSScriptRoot)
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
function Get-WikiPath {
param([string]$Name)
switch ($Name) {
'Wiki' { Join-Path $Root 'wiki'; break }
'Topics' { Join-Path $Root 'wiki\topics'; break }
'Sources' { Join-Path $Root 'wiki\sources'; break }
'Index' { Join-Path $Root 'wiki\index.md'; break }
'Log' { Join-Path $Root 'wiki\log.md'; break }
'Schema' { Join-Path $Root 'AGENTS.md'; break }
default { throw "Unknown wiki path: $Name" }
}
}
function Ensure-WikiLayout {
foreach ($directory in @((Get-WikiPath 'Wiki'), (Get-WikiPath 'Topics'), (Get-WikiPath 'Sources'))) {
if (-not (Test-Path -LiteralPath $directory)) {
New-Item -ItemType Directory -Path $directory -Force | Out-Null
}
}
if (-not (Test-Path -LiteralPath (Get-WikiPath 'Log'))) {
@('# Wiki log', '', 'Append-only record of wiki operations.', '') | Set-Content -LiteralPath (Get-WikiPath 'Log') -Encoding utf8NoBOM
}
}
function Get-MarkdownMetadata {
param([string]$FilePath)
$content = Get-Content -LiteralPath $FilePath -Raw -Encoding utf8
$metadata = [ordered]@{}
$body = $content
if ($content -match '(?s)^---\s*\r?\n(.*?)\r?\n---\s*\r?\n?(.*)$') {
$frontMatter = $Matches[1]
$body = $Matches[2]
foreach ($line in ($frontMatter -split '\r?\n')) {
if ($line -match '^\s*([^:#][^:]*?)\s*:\s*["'']?(.*?)["'']?\s*$') {
$metadata[$Matches[1].Trim()] = $Matches[2].Trim()
}
}
}
[pscustomobject]@{ Path = $FilePath; Metadata = $metadata; Body = $body.Trim() }
}
function ConvertTo-Slug {
param([string]$Text)
$slug = ($Text.ToLowerInvariant() -replace '[^a-z0-9]+', '-') -replace '(^-|-$)', ''
if ([string]::IsNullOrWhiteSpace($slug)) { return 'untitled' }
return $slug
}
function Get-RelativeRawPath {
param([string]$FilePath)
$relative = [IO.Path]::GetRelativePath($Root, $FilePath)
return ($relative -replace '\\', '/')
}
function Get-SourceTitle {
param($Document)
$label = [string]$Document.Metadata['label']
$date = [string]$Document.Metadata['date']
$rawName = [IO.Path]::GetFileNameWithoutExtension($Document.Path)
$firstLine = ($Document.Body -split '\r?\n' | Where-Object { $_.Trim() } | Select-Object -First 1)
if ($firstLine -match '^\[[^]]+\]\s*(.*)$') { $firstLine = $Matches[1] }
if ([string]::IsNullOrWhiteSpace($firstLine)) { $firstLine = $rawName }
$firstLine = ($firstLine -replace '\s+', ' ').Trim()
if ($firstLine.Length -gt 90) { $firstLine = $firstLine.Substring(0, 87) + '...' }
return "$date · $label · $firstLine"
}
function Add-WikiLog {
param([string]$Action, [string]$Details)
$stamp = Get-Date -Format 'yyyy-MM-ddTHH:mm:ssK'
Add-Content -LiteralPath (Get-WikiPath 'Log') -Encoding utf8NoBOM -Value @("## [$stamp] $Action | $Details", '')
}
function Get-WikiDocuments {
Ensure-WikiLayout
Get-ChildItem -LiteralPath (Get-WikiPath 'Wiki') -Recurse -File -Filter '*.md' |
Where-Object { $_.Name -notin @('index.md', 'log.md') } |
ForEach-Object { Get-MarkdownMetadata $_.FullName }
}
function Update-WikiIndex {
Ensure-WikiLayout
$documents = @(Get-WikiDocuments | Sort-Object Path)
$lines = [System.Collections.Generic.List[string]]::new()
$lines.Add('# Personal Wiki Index')
$lines.Add('')
$lines.Add('Content-oriented catalog of the maintained wiki. Raw diary entries remain immutable under [`2025/`](../2025/).')
$lines.Add('')
$lines.Add("_Last rebuilt: $(Get-Date -Format 'yyyy-MM-ddTHH:mm:ssK')_")
$lines.Add('')
$groups = $documents | Group-Object { if ($_.Metadata.Contains('type')) { [string]$_.Metadata['type'] } else { 'unclassified' } } | Sort-Object Name
foreach ($group in $groups) {
$lines.Add("## $($group.Name)")
$lines.Add('')
foreach ($document in ($group.Group | Sort-Object Path)) {
$relative = [IO.Path]::GetRelativePath((Get-WikiPath 'Wiki'), $document.Path) -replace '\\', '/'
$title = [IO.Path]::GetFileNameWithoutExtension($document.Path) -replace '-', ' '
$heading = ($document.Body -split '\r?\n' | Where-Object { $_ -match '^# ' } | Select-Object -First 1)
if ($heading) { $title = $heading.Substring(2).Trim() }
$summary = ($document.Body -split '\r?\n' | Where-Object { $_.Trim() -and $_ -notmatch '^#' -and $_ -notmatch '^---' } | Select-Object -First 1)
if ([string]::IsNullOrWhiteSpace($summary)) { $summary = 'No summary recorded.' }
$summary = ($summary -replace '[\[\]]', '') -replace '\s+', ' '
if ($summary.Length -gt 180) { $summary = $summary.Substring(0, 177) + '...' }
$lines.Add("- [$title]($relative) — $summary")
}
$lines.Add('')
}
$lines.Add('## Navigation')
$lines.Add('')
$lines.Add('- [Overview](overview.md) — current synthesis and open questions')
$lines.Add('- [Wiki log](log.md) — chronological operation history')
$lines.Add('- [Schema](../AGENTS.md) — maintenance contract for agents')
$lines.Add('- [Raw sources](../2025/) — immutable captured diary entries')
$lines | Set-Content -LiteralPath (Get-WikiPath 'Index') -Encoding utf8NoBOM
return "Indexed $($documents.Count) wiki pages."
}
function Invoke-WikiIngest {
param([string]$InputPath)
Ensure-WikiLayout
if ([string]::IsNullOrWhiteSpace($InputPath)) { throw 'ingest requires -Path pointing to a raw markdown file or directory.' }
$resolved = (Resolve-Path -LiteralPath $InputPath).Path
$items = @()
if ((Get-Item -LiteralPath $resolved).PSIsContainer) {
$items = @(Get-ChildItem -LiteralPath $resolved -Recurse -File -Filter '*.md' | Where-Object { $_.FullName -notlike "$(Get-WikiPath 'Wiki')*" -and $_.Name -ne 'llm-wiki.md' })
} else {
$items = @(Get-Item -LiteralPath $resolved)
}
if ($items.Count -eq 0) { throw 'No markdown source files found.' }
$created = 0
foreach ($item in $items) {
$document = Get-MarkdownMetadata $item.FullName
$date = [string]$document.Metadata['date']
if ([string]::IsNullOrWhiteSpace($date)) { $date = $item.LastWriteTime.ToString('yyyy-MM-dd') }
$slug = ConvertTo-Slug ([IO.Path]::GetFileNameWithoutExtension($item.Name))
$target = Join-Path (Get-WikiPath 'Sources') "entry-$date-$slug.md"
$rawLink = Get-RelativeRawPath $item.FullName
$title = Get-SourceTitle $document
$label = [string]$document.Metadata['label']
$createdAt = [string]$document.Metadata['created']
$body = $document.Body
$urls = [regex]::Matches($body, 'https?://[^\s)]+') | ForEach-Object { $_.Value } | Select-Object -Unique
$out = [System.Collections.Generic.List[string]]::new()
$out.Add('---')
$out.Add("type: source")
$out.Add("date: '$date'")
$out.Add("source_file: '$rawLink'")
$out.Add("label: '$label'")
$out.Add('status: captured')
$out.Add('---')
$out.Add('')
$out.Add("# $title")
$out.Add('')
$out.Add("> This page is an indexed interpretation of the immutable source. The source itself is [`$rawLink`](../../$rawLink).")
$out.Add('')
$out.Add('## Captured content')
$out.Add('')
$out.Add($body)
$out.Add('')
if ($urls.Count -gt 0) {
$out.Add('## External links')
$out.Add('')
foreach ($url in $urls) { $out.Add("- $url") }
$out.Add('')
}
$out.Add('## Maintainer review')
$out.Add('')
$out.Add('- LLM synthesis and cross-link review: pending.')
$out.Add("- Captured at: $createdAt")
$out | Set-Content -LiteralPath $target -Encoding utf8NoBOM
$created++
}
Update-WikiIndex | Out-Null
Add-WikiLog 'ingest' "Captured $created source page(s) from $InputPath."
return "Ingested $created source page(s)."
}
function Search-Wiki {
param([string]$Text)
if ([string]::IsNullOrWhiteSpace($Text)) { throw 'query requires -Question or -Path containing the query.' }
Ensure-WikiLayout
$terms = @($Text.ToLowerInvariant() -split '\s+' | Where-Object { $_.Length -ge 2 } | ForEach-Object { [regex]::Escape($_) })
$results = foreach ($document in Get-WikiDocuments) {
$lower = $document.Body.ToLowerInvariant()
$hits = @($terms | Where-Object { $lower -match $_ }).Count
if ($hits -gt 0) {
$relative = [IO.Path]::GetRelativePath($Root, $document.Path) -replace '\\', '/'
$snippet = ($document.Body -split '\r?\n' | Where-Object { $line = $_.ToLowerInvariant(); @($terms | Where-Object { $line -match $_ }).Count -gt 0 } | Select-Object -First 1)
[pscustomobject]@{ Score = $hits; File = $relative; Snippet = ($snippet -replace '\s+', ' ').Trim() }
}
}
$results | Sort-Object Score -Descending, File | Format-Table -AutoSize
Add-WikiLog 'query' "Searched wiki for: $Text"
}
function Test-WikiHealth {
Ensure-WikiLayout
$documents = @(Get-WikiDocuments)
$allPages = @{}
foreach ($document in $documents) { $allPages[[IO.Path]::GetFullPath($document.Path)] = $true }
$broken = [System.Collections.Generic.List[string]]::new()
$inbound = @{}
foreach ($document in $documents) {
$links = [regex]::Matches($document.Body, '\[[^]]+\]\(([^)#]+)(?:#[^)]+)?\)')
foreach ($link in $links) {
$target = $link.Groups[1].Value
if ($target -match '^(https?|mailto):') { continue }
$absolute = [IO.Path]::GetFullPath((Join-Path (Split-Path -Parent $document.Path) $target))
if (-not (Test-Path -LiteralPath $absolute)) { $broken.Add("$($document.Path): $target") }
elseif ($absolute -like "$(Get-WikiPath 'Wiki')*") { $inbound[$absolute] = [int]$inbound[$absolute] + 1 }
}
}
$orphans = @($documents | Where-Object { $path = [IO.Path]::GetFullPath($_.Path); -not $inbound.ContainsKey($path) -and $_.Path -notlike "$(Get-WikiPath 'Wiki')\sources*" } | ForEach-Object { [IO.Path]::GetRelativePath($Root, $_.Path) -replace '\\', '/' })
[pscustomobject]@{
WikiPages = $documents.Count
BrokenLinks = $broken.Count
OrphanPages = $orphans.Count
SourcePages = @($documents | Where-Object { $_.Metadata['type'] -eq 'source' }).Count
BrokenLinkDetails = ($broken -join '; ')
OrphanPageDetails = ($orphans -join '; ')
} | Format-List
Add-WikiLog 'lint' "Health check: $($documents.Count) pages, $($broken.Count) broken links, $($orphans.Count) orphans."
}
switch ($Operation) {
'init' {
Ensure-WikiLayout
if (-not (Test-Path -LiteralPath (Get-WikiPath 'Index'))) { Update-WikiIndex | Out-Null }
Add-WikiLog 'init' 'Initialized wiki layout.'
Write-Output "Wiki initialized at $(Get-WikiPath 'Wiki')."
}
'ingest' { Invoke-WikiIngest -InputPath $Path }
'index' { Update-WikiIndex }
'query' { Search-Wiki -Text $(if ($Question) { $Question } else { $Path }) }
'lint' { Test-WikiHealth }
default {
@('LLM Wiki tool', '', 'Usage:', ' pwsh .\tools\LlmWiki.ps1 init', ' pwsh .\tools\LlmWiki.ps1 ingest -Path .\2025\12\03\entry.md', ' pwsh .\tools\LlmWiki.ps1 index', ' pwsh .\tools\LlmWiki.ps1 query -Question "agent skills"', ' pwsh .\tools\LlmWiki.ps1 lint', '', 'Raw sources are never modified. Ingest creates source pages, index rebuilds navigation, query searches the wiki, and lint reports link/orphan issues.') -join [Environment]::NewLine
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment