diff --git a/Noz.PowerShell/Cmdlets/GetNozNoteCmdlet.cs b/Noz.PowerShell/Cmdlets/GetNozNoteCmdlet.cs
new file mode 100644
index 0000000..a020e8d
--- /dev/null
+++ b/Noz.PowerShell/Cmdlets/GetNozNoteCmdlet.cs
@@ -0,0 +1,41 @@
+using System.Management.Automation;
+
+namespace Noz.PowerShell.Cmdlets;
+
+/// List notes from the noz server.
+///
+/// Get-NozNote
+/// Get-NozNote -Topic psychologie
+/// Get-NozNote -Status seed -Lang de
+/// Get-NozNote | Group-Object Topic | Sort-Object Count -Descending
+/// Get-NozNote | Export-Csv ~/notes.csv
+///
+[Cmdlet(VerbsCommon.Get, "NozNote")]
+[OutputType(typeof(NozNote))]
+public sealed class GetNozNoteCmdlet : NozCmdletBase
+{
+ [Parameter(Position = 0)]
+ public string? Topic { get; set; }
+
+ [Parameter]
+ [ValidateSet("seed", "budding", "evergreen")]
+ public string? Status { get; set; }
+
+ [Parameter]
+ [ValidateSet("de", "en")]
+ public string? Lang { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ var notes = Run(Api.GetNotesAsync());
+
+ var filtered = notes.AsEnumerable();
+
+ if (Topic is not null) filtered = filtered.Where(n => n.Topic == Topic);
+ if (Status is not null) filtered = filtered.Where(n => n.Status == Status);
+ if (Lang is not null) filtered = filtered.Where(n => n.Lang == Lang);
+
+ foreach (var note in filtered.OrderBy(n => n.Slug))
+ WriteObject(NozNote.From(note));
+ }
+}
diff --git a/Noz.PowerShell/Cmdlets/GetNozNoteContentCmdlet.cs b/Noz.PowerShell/Cmdlets/GetNozNoteContentCmdlet.cs
new file mode 100644
index 0000000..0ff7587
--- /dev/null
+++ b/Noz.PowerShell/Cmdlets/GetNozNoteContentCmdlet.cs
@@ -0,0 +1,32 @@
+using System.Management.Automation;
+
+namespace Noz.PowerShell.Cmdlets;
+
+/// Get the markdown content of a note.
+///
+/// Get-NozNoteContent -Slug psychologie/flow-theorie
+/// "psychologie/flow-theorie" | Get-NozNoteContent
+/// Get-NozNote -Topic psychologie | Get-NozNoteContent | Select-Object Slug, Markdown
+///
+[Cmdlet(VerbsCommon.Get, "NozNoteContent")]
+[OutputType(typeof(NozNoteContent))]
+public sealed class GetNozNoteContentCmdlet : NozCmdletBase
+{
+ [Parameter(Mandatory = true, Position = 0,
+ ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
+ public string Slug { get; set; } = "";
+
+ protected override void ProcessRecord()
+ {
+ var content = Run(Api.GetNoteAsync(Slug));
+ WriteObject(new NozNoteContent { Slug = content.Slug, Markdown = content.Markdown });
+ }
+}
+
+public sealed class NozNoteContent
+{
+ public string Slug { get; init; } = "";
+ public string Markdown { get; init; } = "";
+
+ public override string ToString() => Slug;
+}
diff --git a/Noz.PowerShell/Cmdlets/NewNozNoteCmdlet.cs b/Noz.PowerShell/Cmdlets/NewNozNoteCmdlet.cs
new file mode 100644
index 0000000..9000f96
--- /dev/null
+++ b/Noz.PowerShell/Cmdlets/NewNozNoteCmdlet.cs
@@ -0,0 +1,59 @@
+using System.Management.Automation;
+
+namespace Noz.PowerShell.Cmdlets;
+
+/// Create a new note on the server.
+///
+/// New-NozNote -Slug psychologie/neue-notiz -Title "Neue Notiz"
+/// New-NozNote -Slug biosysteme/pilze -Title "Pilze" -Topic biosysteme -Status budding -Lang de
+///
+[Cmdlet(VerbsCommon.New, "NozNote", SupportsShouldProcess = true)]
+[OutputType(typeof(NozNote))]
+public sealed class NewNozNoteCmdlet : NozCmdletBase
+{
+ [Parameter(Mandatory = true, Position = 0)]
+ public string Slug { get; set; } = "";
+
+ [Parameter(Mandatory = true, Position = 1)]
+ public string Title { get; set; } = "";
+
+ [Parameter]
+ public string? Topic { get; set; }
+
+ [Parameter]
+ [ValidateSet("seed", "budding", "evergreen")]
+ public string Status { get; set; } = "seed";
+
+ [Parameter]
+ [ValidateSet("de", "en")]
+ public string Lang { get; set; } = "de";
+
+ [Parameter]
+ public string Description { get; set; } = "";
+
+ protected override void ProcessRecord()
+ {
+ if (!ShouldProcess(Slug, "Create note")) return;
+
+ var topic = Topic ?? (Slug.Contains('/') ? Slug.Split('/')[0] : Slug);
+ var markdown = $"""
+ ---
+ title: "{Title}"
+ description: "{Description}"
+ topic: {topic}
+ status: {Status}
+ lang: {Lang}
+ ---
+
+ # {Title}
+
+ """;
+
+ Run(Api.PutNoteAsync(Slug, markdown));
+
+ var notes = Run(Api.GetNotesAsync());
+ var created = notes.FirstOrDefault(n => n.Slug == Slug);
+ if (created is not null)
+ WriteObject(NozNote.From(created));
+ }
+}
diff --git a/Noz.PowerShell/Cmdlets/RemoveNozNoteCmdlet.cs b/Noz.PowerShell/Cmdlets/RemoveNozNoteCmdlet.cs
new file mode 100644
index 0000000..d782dd2
--- /dev/null
+++ b/Noz.PowerShell/Cmdlets/RemoveNozNoteCmdlet.cs
@@ -0,0 +1,24 @@
+using System.Management.Automation;
+
+namespace Noz.PowerShell.Cmdlets;
+
+/// Delete a note from the server.
+///
+/// Remove-NozNote -Slug psychologie/alte-notiz
+/// Get-NozNote -Status seed | Where-Object Topic -eq "draft" | Remove-NozNote -WhatIf
+/// Get-NozNote -Status seed | Where-Object Topic -eq "draft" | Remove-NozNote -Confirm:$false
+///
+[Cmdlet(VerbsCommon.Remove, "NozNote", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)]
+public sealed class RemoveNozNoteCmdlet : NozCmdletBase
+{
+ [Parameter(Mandatory = true, Position = 0,
+ ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
+ public string Slug { get; set; } = "";
+
+ protected override void ProcessRecord()
+ {
+ if (!ShouldProcess(Slug, "Delete note")) return;
+ Run(Api.DeleteNoteAsync(Slug));
+ WriteVerbose($"Deleted: {Slug}");
+ }
+}
diff --git a/Noz.PowerShell/Cmdlets/SetNozNoteCmdlet.cs b/Noz.PowerShell/Cmdlets/SetNozNoteCmdlet.cs
new file mode 100644
index 0000000..e2e392b
--- /dev/null
+++ b/Noz.PowerShell/Cmdlets/SetNozNoteCmdlet.cs
@@ -0,0 +1,31 @@
+using System.Management.Automation;
+
+namespace Noz.PowerShell.Cmdlets;
+
+/// Update the markdown content of an existing note.
+///
+/// Set-NozNote -Slug psychologie/test -Markdown "# Test`n`nHello."
+/// Get-Content note.md -Raw | Set-NozNote -Slug psychologie/test
+///
+[Cmdlet(VerbsCommon.Set, "NozNote", SupportsShouldProcess = true)]
+[OutputType(typeof(NozNote))]
+public sealed class SetNozNoteCmdlet : NozCmdletBase
+{
+ [Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true)]
+ public string Slug { get; set; } = "";
+
+ [Parameter(Mandatory = true, Position = 1, ValueFromPipeline = true)]
+ public string Markdown { get; set; } = "";
+
+ protected override void ProcessRecord()
+ {
+ if (!ShouldProcess(Slug, "Update note")) return;
+
+ Run(Api.PutNoteAsync(Slug, Markdown));
+
+ var notes = Run(Api.GetNotesAsync());
+ var updated = notes.FirstOrDefault(n => n.Slug == Slug);
+ if (updated is not null)
+ WriteObject(NozNote.From(updated));
+ }
+}
diff --git a/Noz.PowerShell/Install-NozModule.ps1 b/Noz.PowerShell/Install-NozModule.ps1
new file mode 100644
index 0000000..6c49744
--- /dev/null
+++ b/Noz.PowerShell/Install-NozModule.ps1
@@ -0,0 +1,29 @@
+#Requires -Version 7.2
+<#
+.SYNOPSIS
+ Builds and installs the Noz PowerShell module for the current user.
+.EXAMPLE
+ ./Install-NozModule.ps1
+ ./Install-NozModule.ps1 -Verbose
+#>
+
+$ErrorActionPreference = 'Stop'
+
+$moduleDir = Join-Path ([Environment]::GetFolderPath('MyDocuments')) 'PowerShell' 'Modules' 'Noz'
+
+Write-Host "Building Noz.PowerShell..." -ForegroundColor Cyan
+dotnet build "$PSScriptRoot/Noz.PowerShell.csproj" -c Release -v quiet
+
+$src = "$PSScriptRoot/bin/Release/net8.0"
+
+Write-Host "Installing to: $moduleDir" -ForegroundColor Cyan
+New-Item -ItemType Directory -Force -Path $moduleDir | Out-Null
+
+# Copy DLLs and manifest
+Copy-Item "$src/Noz.PowerShell.dll" $moduleDir -Force
+Copy-Item "$src/NozCli.Core.dll" $moduleDir -Force
+Copy-Item "$PSScriptRoot/Noz.psd1" $moduleDir -Force
+
+Write-Host "Done. Import with:" -ForegroundColor Green
+Write-Host " Import-Module Noz" -ForegroundColor White
+Write-Host " Get-NozNote -Topic psychologie" -ForegroundColor White
diff --git a/Noz.PowerShell/Noz.PowerShell.csproj b/Noz.PowerShell/Noz.PowerShell.csproj
new file mode 100644
index 0000000..4f5d5bb
--- /dev/null
+++ b/Noz.PowerShell/Noz.PowerShell.csproj
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+ net8.0
+ enable
+ enable
+ Noz.PowerShell
+ Noz.PowerShell
+ true
+
+
+
diff --git a/Noz.PowerShell/Noz.psd1 b/Noz.PowerShell/Noz.psd1
new file mode 100644
index 0000000..461091b
--- /dev/null
+++ b/Noz.PowerShell/Noz.psd1
@@ -0,0 +1,26 @@
+@{
+ ModuleVersion = '0.1.0'
+ GUID = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'
+ Author = 'Bruno Deanoz'
+ Description = 'PowerShell module for noz — a self-hosted digital garden'
+ PowerShellVersion = '7.2'
+ RootModule = 'Noz.PowerShell.dll'
+
+ CmdletsToExport = @(
+ 'Get-NozNote',
+ 'Get-NozNoteContent',
+ 'Set-NozNote',
+ 'New-NozNote',
+ 'Remove-NozNote'
+ )
+
+ FunctionsToExport = @()
+ AliasesToExport = @()
+
+ PrivateData = @{
+ PSData = @{
+ Tags = @('noz', 'digital-garden', 'notes', 'markdown')
+ ProjectUri = 'https://git.kryptomrx.de/krypto/noz-cli'
+ }
+ }
+}
diff --git a/Noz.PowerShell/NozCmdletBase.cs b/Noz.PowerShell/NozCmdletBase.cs
new file mode 100644
index 0000000..9c93bf4
--- /dev/null
+++ b/Noz.PowerShell/NozCmdletBase.cs
@@ -0,0 +1,47 @@
+using System.Management.Automation;
+using NozCli.Core.Client;
+using NozCli.Core.Config;
+
+namespace Noz.PowerShell;
+
+public abstract class NozCmdletBase : PSCmdlet
+{
+ protected LocalConfig Config { get; private set; } = null!;
+ protected NozApiClient Api { get; private set; } = null!;
+
+ protected override void BeginProcessing()
+ {
+ var cfg = LocalConfig.Load();
+ if (cfg is null)
+ ThrowTerminatingError(new ErrorRecord(
+ new InvalidOperationException(
+ "noz is not initialized. Run: noz init "),
+ "NozNotInitialized",
+ ErrorCategory.NotSpecified,
+ targetObject: null));
+
+ Config = cfg!;
+ Api = new NozApiClient(cfg!);
+ }
+
+ protected override void StopProcessing() => Api?.Dispose();
+
+ protected T Run(Task task)
+ {
+ try { return task.GetAwaiter().GetResult(); }
+ catch (HttpRequestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.Unauthorized)
+ {
+ ThrowTerminatingError(new ErrorRecord(ex, "NozUnauthorized",
+ ErrorCategory.AuthenticationError, null));
+ throw;
+ }
+ catch (HttpRequestException ex)
+ {
+ ThrowTerminatingError(new ErrorRecord(ex, "NozApiError",
+ ErrorCategory.ConnectionError, null));
+ throw;
+ }
+ }
+
+ protected void Run(Task task) => Run(task.ContinueWith(_ => true));
+}
diff --git a/Noz.PowerShell/NozNote.cs b/Noz.PowerShell/NozNote.cs
new file mode 100644
index 0000000..fac164c
--- /dev/null
+++ b/Noz.PowerShell/NozNote.cs
@@ -0,0 +1,29 @@
+using NozCli.Core.Models;
+
+namespace Noz.PowerShell;
+
+/// PS-friendly output object for a noz note.
+/// Clean property set — no internal fields like Hash.
+public sealed class NozNote
+{
+ public string Slug { get; init; } = "";
+ public string Title { get; init; } = "";
+ public string Status { get; init; } = "";
+ public string Topic { get; init; } = "";
+ public string Lang { get; init; } = "";
+ public string? Date { get; init; }
+ public bool Private { get; init; }
+
+ public override string ToString() => Slug;
+
+ internal static NozNote From(NoteInfo n) => new()
+ {
+ Slug = n.Slug,
+ Title = n.Title,
+ Status = n.Status,
+ Topic = n.Topic,
+ Lang = n.Lang,
+ Date = n.Date,
+ Private = n.Private,
+ };
+}
diff --git a/NozCli.Core/NozCli.Core.csproj b/NozCli.Core/NozCli.Core.csproj
index 37780e4..6aa5f96 100644
--- a/NozCli.Core/NozCli.Core.csproj
+++ b/NozCli.Core/NozCli.Core.csproj
@@ -1,7 +1,7 @@
- net10.0
+ net8.0
enable
enable
diff --git a/noz-cli.slnx b/noz-cli.slnx
index 9c52bf0..118ed83 100644
--- a/noz-cli.slnx
+++ b/noz-cli.slnx
@@ -1,4 +1,5 @@
+