onenote-cli 0.1.3

Read and capture Microsoft OneNote notes through its Windows desktop application
Documentation
# Windows PowerShell 5.1; fixed code, JSON request on stdin, no user code evaluation.
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
[Console]::InputEncoding = New-Object System.Text.UTF8Encoding($false)
[Console]::OutputEncoding = New-Object System.Text.UTF8Encoding($false)
$app = $null
$failed = $false
try {
    $request = [Console]::In.ReadToEnd() | ConvertFrom-Json
    if ($request.operation -notin @('probe', 'notebooks', 'sections', 'pages', 'search', 'read', 'scan')) {
        throw [ArgumentException]::new('Unknown OneNote operation.')
    }
    $app = New-Object -ComObject OneNote.Application
    [string]$xml = ''
    # xs2013 = 2; use a fixed schema version on every call.
    switch ($request.operation) {
        { $_ -in 'probe', 'notebooks' } { $app.GetHierarchy('', 2, [ref]$xml, 2) }
        'sections' { $app.GetHierarchy([string]$request.scope, 3, [ref]$xml, 2) }
        'pages' { $app.GetHierarchy([string]$request.scope, 4, [ref]$xml, 2) }
        'search' {
            $searchService = Get-Service -Name WSearch -ErrorAction SilentlyContinue
            if ($null -eq $searchService -or $searchService.Status -ne 'Running') {
                throw 'OneNote search requires the Windows Search (WSearch) service. Start Windows Search and retry. Notebook listing and page reading do not require it.'
            }
            # Include unindexed metadata so Rust can report incomplete indexing;
            # those pages are NOT necessarily matches and must be filtered there.
            $app.FindPages([string]$request.scope, [string]$request.query, [ref]$xml, $true, $false, 2)
        }
        'scan' {
            if ($request.scan_limit -lt 1 -or $request.scan_limit -gt 500 -or $request.scan_offset -lt 0 -or $request.scan_offset -gt 1000000) {
                throw [ArgumentException]::new('Invalid scan bounds.')
            }
            $clock = [Diagnostics.Stopwatch]::StartNew()
            $app.GetHierarchy([string]$request.scope, 4, [ref]$xml, 2)
            # Leave headroom for JSON escaping within the 32 MiB bridge cap.
            $budget = 4 * 1024 * 1024
            if ($xml.Length -gt $budget) { throw 'Scan hierarchy is too large; narrow --scope.' }
            $doc = New-Object Xml.XmlDocument
            $doc.XmlResolver = $null
            $settings = New-Object Xml.XmlReaderSettings
            $settings.DtdProcessing = [Xml.DtdProcessing]::Prohibit
            $reader = [Xml.XmlReader]::Create((New-Object IO.StringReader($xml)), $settings)
            try { $doc.Load($reader) } finally { $reader.Dispose() }
            $ns = New-Object Xml.XmlNamespaceManager($doc.NameTable)
            $ns.AddNamespace('one', 'http://schemas.microsoft.com/office/onenote/2013/onenote')
            if ($doc.DocumentElement.NamespaceURI -ne $ns.LookupNamespace('one')) { throw 'Unexpected OneNote XML namespace.' }
            $visible = "not(ancestor-or-self::*[@isRecycleBin='true' or @isInRecycleBin='true'])"
            $locked = @($doc.SelectNodes("//one:Section[@locked='true' and $visible]", $ns)).Count
            $candidates = @($doc.SelectNodes("//one:Page[$visible and not(ancestor-or-self::*[@locked='true'])]", $ns))
            $pages = New-Object 'Collections.Generic.List[object]'
            $errors = New-Object 'Collections.Generic.List[object]'
            $position = [int]$request.scan_offset
            $attempted = 0
            $used = $xml.Length
            while ($position -lt $candidates.Count -and $attempted -lt $request.scan_limit -and $clock.Elapsed.TotalSeconds -lt 20) {
                $id = $candidates[$position].GetAttribute('ID')
                if ([string]::IsNullOrEmpty($id)) { throw 'OneNote returned a page without an ID.' }
                [string]$content = ''
                try {
                    $app.GetPageContent($id, [ref]$content, 0, 2)
                    if ($content.Length -gt $budget - $xml.Length) {
                        $errors.Add(@{id=$id;reason='page_too_large'})
                    } elseif ($used + $content.Length -gt $budget) {
                        break
                    } else {
                        $pages.Add(@{id=$id;xml=$content})
                        $used += $content.Length
                    }
                } catch {
                    $errors.Add(@{id=$id;reason='unreadable'})
                }
                $position++
                $attempted++
            }
            $next = if ($position -lt $candidates.Count) { $position } else { $null }
            $scanResult = @{xml=$xml;pages=$pages.ToArray();skipped_pages=$errors.ToArray();skipped_locked_sections=$locked;
                scanned_pages=$pages.Count;attempted_pages=$attempted;candidate_pages=$candidates.Count;
                scan_offset=[int]$request.scan_offset;next_scan_offset=$next}
        }
        'read' { $app.GetPageContent([string]$request.id, [ref]$xml, 0, 2) }
    }
    $result = if ($request.operation -eq 'scan') { $scanResult } else { @{xml=$xml} }
} catch {
    $failed = $true
    $kind = 'desktop_error'
    $message = $_.Exception.Message
    $exception = $_.Exception
    while ($null -ne $exception.InnerException) { $exception = $exception.InnerException }
    if ($exception -is [ArgumentException]) { $kind = 'invalid_input' }
    if ($null -eq $app -and $kind -ne 'invalid_input') {
        $message = 'Cannot connect to OneNote.Application. Install Windows OneNote desktop, open it under this Windows user, and complete any setup prompts. ' + $message
    } elseif ($kind -eq 'desktop_error' -and $exception.HResult -eq -2147467259) {
        $message += ' Open OneNote in the Windows desktop session, complete first-run sign-in, and open a notebook before retrying.'
    }
    if (('{0:X8}' -f $exception.HResult) -eq '8004201B') {
        $message += ' This section is password-protected. Unlock it in OneNote desktop and retry.'
    }
    $result = @{error=@{kind=$kind; message=$message}}
} finally {
    if ($null -ne $app -and [Runtime.InteropServices.Marshal]::IsComObject($app)) {
        [void][Runtime.InteropServices.Marshal]::ReleaseComObject($app)
    }
    # Never quit the user's OneNote application.
}
$result | ConvertTo-Json -Depth 8 -Compress
if ($failed) { exit 1 }