From b3e4175b5b2c5246c1f9fc0d032df67a1bfce4a2 Mon Sep 17 00:00:00 2001 From: Aric Camarata Date: Wed, 28 Oct 2026 08:48:02 -0400 Subject: [PATCH 1/8] fix(wiki): update pricing/bundle docs, backup retention, compose storage, env vars, link audit (D-1289, D-0692, D-0659, D-0726, D-0717) --- .github/wiki/Compose-Generation.md | 4 +- .github/wiki/Config-Env-Vars.md | 1 + .github/wiki/Feature-Plugins.md | 10 +--- .github/wiki/Guide-Backup-Restore.md | 11 ++++ .github/wiki/Licensing-Guide.md | 77 ++++++++-------------------- .github/wiki/Plugin-Licensing.md | 4 +- .github/wiki/Plugin-Overview.md | 3 +- .github/wiki/cmd-ai-studio.md | 2 +- .github/wiki/cmd-alerts.md | 2 +- .github/wiki/cmd-audit.md | 2 +- .github/wiki/cmd-bundle.md | 14 ++--- .github/wiki/cmd-costs.md | 2 +- .github/wiki/cmd-dlq.md | 2 +- .github/wiki/cmd-doctor.md | 2 +- .github/wiki/cmd-dogfood.md | 2 +- .github/wiki/cmd-encryption.md | 2 +- .github/wiki/cmd-federation.md | 2 +- .github/wiki/cmd-gateway.md | 2 +- .github/wiki/cmd-gauth.md | 2 +- .github/wiki/cmd-gdpr.md | 2 +- .github/wiki/cmd-generate.md | 2 +- .github/wiki/cmd-k8s.md | 2 +- .github/wiki/cmd-mail.md | 2 +- .github/wiki/cmd-model.md | 2 +- .github/wiki/cmd-monitor.md | 2 +- .github/wiki/cmd-oauth.md | 4 +- .github/wiki/cmd-pentest-kit.md | 2 +- .github/wiki/cmd-soak.md | 2 +- .github/wiki/cmd-start.md | 2 +- .github/wiki/cmd-template.md | 4 +- .github/wiki/cmd-update.md | 2 +- .github/wiki/cmd-waf.md | 2 +- .github/wiki/cmd-watchdog.md | 2 +- tools/wikigen/main.go | 53 +++++++++++++++++++ 34 files changed, 129 insertions(+), 102 deletions(-) diff --git a/.github/wiki/Compose-Generation.md b/.github/wiki/Compose-Generation.md index 5468d3948..ada704f78 100644 --- a/.github/wiki/Compose-Generation.md +++ b/.github/wiki/Compose-Generation.md @@ -65,11 +65,13 @@ Networks follow the fragment rules in [[Plugins#compose-fragment-rules]]. In v1. The compose file is assembled in this order. Services later in the list can depend on services earlier in the list. 1. **Core**, `postgres`, `hasura`, `auth`, `nginx` -2. **Optional**, `minio` (with init container), `redis`, `mailpit`, `admin`, `functions`, `mlflow`, `search` +2. **Optional**, object storage (`seaweedfs` + `seaweedfs-init` in v1.5 when `OBJECT_STORAGE_ENABLED=true`; `minio` with init container in v1.4, or in v1.5 only after `nself add minio`), `redis`, `mailpit`, `admin`, `functions`, `mlflow`, `search` 3. **Monitoring**, `prometheus`, `grafana`, `loki`, `promtail`, `tempo`, `alertmanager`, `cadvisor`, `node-exporter`, `postgres-exporter`, `redis-exporter` 4. **Custom**, `CS_1` through `CS_10` 5. **Plugins**, loaded from `~/.nself/plugins/`, in install order +Object storage selection depends on the project version. A v1.5 project (`NSELF_V15=1`) generates `seaweedfs` and its `seaweedfs-init` sidecar when `OBJECT_STORAGE_ENABLED=true`; MinIO is no longer emitted by default and must be added explicitly with `nself add minio` (which fails with [[error-codes#e138]] when seaweedfs is already present). A v1.4 project keeps the original `minio` + `minio-init` pair. See [[Object-Storage]] for migration and configuration details. + --- ## Security Hardening diff --git a/.github/wiki/Config-Env-Vars.md b/.github/wiki/Config-Env-Vars.md index 06339acfa..94cb51b87 100644 --- a/.github/wiki/Config-Env-Vars.md +++ b/.github/wiki/Config-Env-Vars.md @@ -108,6 +108,7 @@ These vars control the top-level identity and behavior of a project. | `NSELF_DOCTOR_STRICT` | bool (`1` or `true`, any case) | unset | No | Strict mode for `nself doctor --deep --only isolation`: the ISO-01 warning (an app table without `source_account_id` in a multi-app project) becomes a failure. Read from the process environment or the project's `.env` cascade. See [[Multi-App-Isolation]]. | | `NSELF_V15` | bool (`1` or `true`, any case) | unset (off in 1.4.x, on by default from 1.5.0) | No | Opts in to the v1.5 behaviour of every P7 change that breaks an existing user-visible contract (ADR 0021). Anything else (unset, empty, `0`, `yes`) keeps v1.4 behaviour. Read on every call, never cached. See [[Compat-V15]]. | | `NSELF_UPDATE_CHECK` | `1` to opt in | unset (off) | No | Process environment only. With `1`, a successful command prints at most one stderr line per 24 h when a newer CLI release exists (cached in `~/.nself/cache/update-check.json`, refreshed in the background from `ping.nself.org/version`). Never in JSON mode, CI or with a non-TTY stderr. See [[Update-Notifications]]. | +| `NSELF_PLUGIN_REGISTRY` | string | `https://plugins.nself.org` | No | Override URL for the plugin registry used by `nself plugin` and `nself update` commands. Defaults to the public registry (`https://plugins.nself.org`). Set to a private or mirror registry when running behind a proxy or in an air-gapped environment. The same value is read by `plugin install`, `plugin search`, `plugin info`, `plugin outdated`, and `update plan`. | | `NSELF_MCP_TOKEN` | string | unset | No | Bearer token for the `sse` and `http` transports of `nself mcp`. Wins over `~/.nself/mcp-token` (mode `0600`). With neither, a v1.5 loopback start generates the file and prints only its path; a v1.4 loopback start serves open with one warning. A non-loopback `--host` needs this or the file plus `--allowed-host`. Never passed to commands the server runs. Secret names (`config.IsSecretKey`, see Masking below) are masked in output and redacted in the machine audit log. See [[MCP-Server]]. | | `NSELF_INVOKE_PATH` | string | unset | No | Internal. Set by the machine invoker (MCP, HTTP) on the `nself` child it runs: the command path it checked. The child refuses with `E421` when its own command lookup resolves a different command, and removes the variable before running. Never set it by hand. See [[MCP-Server]]. | | `NSELF_INVOKE_SECRETS` | JSON | unset | No | Internal. Set by the machine invoker (MCP, HTTP) on the `nself` child it runs: a JSON bundle (a nonce and the values) of the secret-marked arguments and flags, so the secrets never appear on the child argv. The child puts the values back before it parses its arguments, then removes the variable. It is ignored unless `NSELF_INVOKED_BY` is set, and a bad or unmatched bundle fails with `E421`. Never set it by hand. See [[MCP-Server]]. | diff --git a/.github/wiki/Feature-Plugins.md b/.github/wiki/Feature-Plugins.md index 7692f7b56..c6cae5647 100644 --- a/.github/wiki/Feature-Plugins.md +++ b/.github/wiki/Feature-Plugins.md @@ -12,15 +12,7 @@ A plugin is a manifest + Docker Compose overlay + optional Nginx config. When yo ## Plugin Tiers -| Tier | Price | Included | -|------|-------|---------| -| Free | $0 | All free plugins, no license key required | -| Basic | $0.99/mo | Licensed plugins outside the Pro-tier AI suite | -| Pro | $1.99/mo | Basic + AI suite | -| Elite | $4.99/mo | Pro + email support | -| Business | $9.99/mo | Elite + 24h support | - -See [[Plugin-Licensing]] for full tier details. +The CLI no longer uses Basic / Pro / Elite / Business tiers. Bundles are independent purchases at $0.99/mo ($9.99/yr), with a free Task bundle and an all-access ɳSelf+ subscription at $3.99/mo ($39.99/yr). The authoritative catalog and pricing live at [nself.org/pricing](https://nself.org/pricing) and are fetched by the CLI from `https://plugins.nself.org/bundles.json`. See [[Licensing-Guide]] and [[Plugin-Licensing]] for details. ## Free Plugins diff --git a/.github/wiki/Guide-Backup-Restore.md b/.github/wiki/Guide-Backup-Restore.md index 69b6ef142..f41bd1d9a 100644 --- a/.github/wiki/Guide-Backup-Restore.md +++ b/.github/wiki/Guide-Backup-Restore.md @@ -72,6 +72,17 @@ The restore command: 3. Restores from the dump file 4. Restarts services +## Retention and Prune Holds + +`nself backup prune` removes backups older than `BACKUP_RETENTION_DAYS`, but it never deletes a backup tagged `keep` or `keep-*`. The `update stack` command tags its pre-update backup `keep-stack-` so an interrupted or failed update can always roll back; those backups survive prune until you release them. To release a held backup, rename or remove the tag: + +```bash +nself backup tag --rename released- # drops the keep prefix +nself backup prune # now eligible for removal +``` + +A restore that targets a `keep-stack-*` backup runs in strict mode: it refuses to continue when the backup manifest does not match the current project name or when the dump header is missing the expected schema version marker. Strict mode prevents restoring an unrelated stack snapshot into a live project. Override with `--force` only when you have verified the backup belongs to this project. + > **Important:** Test your restores regularly. Untested backups are not backups. ## Backup Verification Checklist diff --git a/.github/wiki/Licensing-Guide.md b/.github/wiki/Licensing-Guide.md index d2565a5fb..066fd3cae 100644 --- a/.github/wiki/Licensing-Guide.md +++ b/.github/wiki/Licensing-Guide.md @@ -20,67 +20,34 @@ Licensed plugins are source-available Rust binaries distributed through the ɳSe ### Per-Bundle Pricing -| Option | Monthly | Annual | What You Get | -|--------|---------|--------|-------------| -| **Free** | $0 | $0 | Core CLI + free plugins | -| **Any single bundle** | $0.99/mo | $9.99/yr | All plugins in that bundle (ɳClaw, ɳChat, ɳFamily, or ClawDE) | -| **ɳSelf+** | $3.99/mo | $39.99/yr | Every bundle + all apps + support | +The authoritative bundle catalog, pricing, and plugin membership live at [nself.org/pricing](https://nself.org/pricing) and are fetched by the CLI from `https://plugins.nself.org/bundles.json`. The wiki pages below summarize each bundle; always treat the live catalog as the source of truth for what is sold. -Annual gives roughly 16% savings versus monthly (about 2 months free). +Current bundles (as of v1.4.13): + +| Bundle | Tier | Monthly | Annual | Notes | +|--------|------|---------|--------|-------| +| Task | free | $0 | $0 | Core productivity plugins; no license key required | +| Chat | paid | $0.99 | $9.99 | Messaging and voice | +| Claw | paid | $0.99 | $9.99 | AI personal assistant | +| Family | paid | $0.99 | $9.99 | Private family social platform | +| Sentry | paid | $0.99 | $9.99 | Observability and incident management | +| ClawDE | paid | $0.99 | $9.99 | Dev environment cloud sync | +| ɳSelf+ | paid | $3.99 | $39.99 | Every bundle + all apps + support | + +There is no "Basic" or "Pro" tier. Paid bundles are independent purchases; ɳSelf+ includes every paid bundle. Annual pricing saves roughly two months versus monthly. --- ## Per-Product Plugin Bundles -Each ɳSelf product has a recommended plugin bundle. Bundles are not separate SKUs. They are a way to understand which plugins power each product. All plugins are available at the Basic tier and above. - -### ɳClaw Bundle -AI personal assistant. Requires Pro tier (includes AI suite). - -| Plugin | Purpose | -|--------|---------| -| ai | Multi-provider LLM routing | -| claw | Core assistant, threads, memory, tools | -| claw-web | Svelte web dashboard | -| claw-budget | Token spend limits and tracking | -| mux | Email pipeline (Gmail, rules, auto-reply) | -| voice | TTS, STT, phone calls | -| browser | Web scraping and automation | -| google | Gmail, Calendar, Drive, Sheets | -| notify | Telegram and webhook notifications | -| cron | Scheduled tasks | -| post | Social media publishing | - -### ɳChat Bundle -Messaging. Available at Basic tier. - -| Plugin | Purpose | -|--------|---------| -| chat | Channels, DMs, threads, reactions | -| livekit | Video and audio calls | -| recording | Call recording | -| moderation | Content filtering | -| bots | Automated responses | -| realtime | Enhanced WebSocket presence | -| auth | SSO, SAML, LDAP | - -### ɳFamily Bundle (Planned) -Private family social media. Will be available at Basic tier. - -| Plugin | Purpose | -|--------|---------| -| social | Activity feed, posts, reactions | -| photos | Photo upload, organization | -| activity-feed | Timeline aggregation | -| moderation | Parental controls | -| realtime | Live updates | -| cms | Family pages | -| chat | Family messaging | - -### ClawDE Bundle -AI dev environment cloud sync. Available at $0.99/mo or $9.99/yr. - -Remote and mobile sync via API for the ClawDE Flutter desktop and mobile app. +Each bundle page lists its current plugin membership and purpose. Plugin lists change with releases; the live catalog at [nself.org/pricing](https://nself.org/pricing) is authoritative. + +- [[bundle-nclaw]] — AI personal assistant +- [[bundle-nchat]] — Messaging and voice +- [[bundle-nfamily]] — Private family social platform +- [[bundle-nsentry]] — Observability and incident management +- [[bundle-clawde]] — Dev environment cloud sync +- [[bundle-ntask]] — Free core productivity plugins --- diff --git a/.github/wiki/Plugin-Licensing.md b/.github/wiki/Plugin-Licensing.md index a20d32cbb..661aadeff 100644 --- a/.github/wiki/Plugin-Licensing.md +++ b/.github/wiki/Plugin-Licensing.md @@ -11,8 +11,8 @@ nself_pro_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ``` Accepted prefixes: -- `nself_pro_`, Pro tier and above -- `nself_max_`, Pro tier +- `nself_pro_`, paid bundles and ɳSelf+ +- `nself_max_`, ɳSelf+ and enterprise - `nself_ent_`, Enterprise tier - `nself_owner_`, Owner/self-hosted tier diff --git a/.github/wiki/Plugin-Overview.md b/.github/wiki/Plugin-Overview.md index d69d3bdd7..343cc42c2 100644 --- a/.github/wiki/Plugin-Overview.md +++ b/.github/wiki/Plugin-Overview.md @@ -14,8 +14,7 @@ | Tier | Price | Includes | |------|-------|---------| | **Free** | $0 | Core CLI + free plugins (MIT licensed) | -| **Basic** | $0.99/mo or $9.99/yr | The standard licensed plugins (excludes Pro-tier AI suite) | -| **Pro** | $1.99/mo or $19.99/yr | Basic + AI suite (ai, browser, claw, claw-budget, claw-web, post, voice) | +The CLI no longer uses Basic / Pro tiers. Bundles are independent purchases; see [[Licensing-Guide]] and [nself.org/pricing](https://nself.org/pricing) for the current catalog. | **Elite** | $4.99/mo or $49.99/yr | Pro + email support | | **Business** | $9.99/mo or $99.99/yr | Elite + 24h email support + priority feature requests | | **Business+** | $49.99/mo or $499.99/yr | Business + dedicated support channel | diff --git a/.github/wiki/cmd-ai-studio.md b/.github/wiki/cmd-ai-studio.md index f65f0c792..7f0adb46d 100644 --- a/.github/wiki/cmd-ai-studio.md +++ b/.github/wiki/cmd-ai-studio.md @@ -7,7 +7,7 @@ ## This command moved `ai-studio` was extracted from the core binary into the `ai-studio` plugin as -part of [CLI-R11](https://github.com/nself-org/cli/blob/main/.claude) (the +part of the thin-core extraction (CLI-R11); see the thin-core extraction). The command surface is unchanged — only where the code lives changed. diff --git a/.github/wiki/cmd-alerts.md b/.github/wiki/cmd-alerts.md index 622dd465f..870d1b185 100644 --- a/.github/wiki/cmd-alerts.md +++ b/.github/wiki/cmd-alerts.md @@ -7,7 +7,7 @@ ## This command moved `alerts` was extracted from the core binary into the `alerts` plugin as -part of [CLI-R11](https://github.com/nself-org/cli/blob/main/.claude) (the +part of the thin-core extraction (CLI-R11); see the thin-core extraction). The command surface is unchanged — only where the code lives changed. diff --git a/.github/wiki/cmd-audit.md b/.github/wiki/cmd-audit.md index 0f726b13b..58ef99f37 100644 --- a/.github/wiki/cmd-audit.md +++ b/.github/wiki/cmd-audit.md @@ -7,7 +7,7 @@ ## This command moved `audit` was extracted from the core binary into the `audit` plugin as part of -[CLI-R11](https://github.com/nself-org/cli/blob/main/.claude) (the thin-core +the thin-core extraction (CLI-R11); see the thin-core extraction). The command surface is unchanged — only where the code lives changed. Note this is the `nself audit docs` documentation audit; the table audit at `nself plugin audit-tables` and the security event log stayed in diff --git a/.github/wiki/cmd-bundle.md b/.github/wiki/cmd-bundle.md index 8c16df639..d540697a8 100644 --- a/.github/wiki/cmd-bundle.md +++ b/.github/wiki/cmd-bundle.md @@ -195,14 +195,16 @@ nself bundle remove nchat --dry-run | Slug | Display name | Price | Status | |------|-------------|-------|--------| -| `nclaw` | ɳClaw | $0.99/mo or $9.99/yr | active | -| `nchat` | ɳChat | $0.99/mo or $9.99/yr | active | -| `nfamily` | ɳFamily | $0.99/mo or $9.99/yr | planned v1.1.0 | -| `clawde` | ClawDE | $0.99/mo or $9.99/yr | planned v1.1.0 | -| `nsentry` | ɳSentry | $0.99/mo or $9.99/yr | planned v1.1.0 | -| `ntask` | ɳTask | FREE | active | +| `task` | Task Bundle | FREE | active | +| `chat` | Chat Bundle | $0.99/mo or $9.99/yr | active | +| `claw` | Claw Bundle | $0.99/mo or $9.99/yr | active | +| `family` | Family Bundle | $0.99/mo or $9.99/yr | active | +| `sentry` | Sentry Bundle | $0.99/mo or $9.99/yr | active | +| `clawde` | ClawDE Bundle | $0.99/mo or $9.99/yr | active | | `nself-plus` | ɳSelf+ | $3.99/mo or $39.99/yr | active | +Bundle membership, pricing and status are fetched live from `https://plugins.nself.org/bundles.json`; the table above is a snapshot. See [[Licensing-Guide]] and [nself.org/pricing](https://nself.org/pricing) for the current catalog. + Buy or manage subscriptions at: https://nself.org/pricing --- diff --git a/.github/wiki/cmd-costs.md b/.github/wiki/cmd-costs.md index e0d1438e3..f538c0010 100644 --- a/.github/wiki/cmd-costs.md +++ b/.github/wiki/cmd-costs.md @@ -7,7 +7,7 @@ ## This command moved `costs` was extracted from the core binary into the `costs` plugin as part of -[CLI-R11](https://github.com/nself-org/cli/blob/main/.claude) (the thin-core +the thin-core extraction (CLI-R11); see the thin-core extraction). The command surface is unchanged — only where the code lives changed. diff --git a/.github/wiki/cmd-dlq.md b/.github/wiki/cmd-dlq.md index 7d1a37486..ca30014c0 100644 --- a/.github/wiki/cmd-dlq.md +++ b/.github/wiki/cmd-dlq.md @@ -7,7 +7,7 @@ ## This command moved `dlq` was extracted from the core binary into the `dlq` plugin as part of -[CLI-R11](https://github.com/nself-org/cli/blob/main/.claude) (the thin-core +the thin-core extraction (CLI-R11); see the thin-core extraction). The command surface is unchanged — only where the code lives changed. diff --git a/.github/wiki/cmd-doctor.md b/.github/wiki/cmd-doctor.md index f64679169..4fc88e9aa 100644 --- a/.github/wiki/cmd-doctor.md +++ b/.github/wiki/cmd-doctor.md @@ -230,7 +230,7 @@ nself db rls apply-table public np_chat_messages nself doctor --deep --only security ``` -**See also:** [multi-tenant conventions](https://nself.org/docs/multi-tenancy/conventions) for the canonical wall doc on `source_account_id` vs `tenant_id`. +**See also:** [multi-tenant conventions]([[Multi-App-Isolation]]) for the canonical wall doc on `source_account_id` vs `tenant_id`. --- diff --git a/.github/wiki/cmd-dogfood.md b/.github/wiki/cmd-dogfood.md index 7fecc08a5..03d028250 100644 --- a/.github/wiki/cmd-dogfood.md +++ b/.github/wiki/cmd-dogfood.md @@ -7,7 +7,7 @@ ## This command moved `dogfood` was extracted from the core binary into the `dogfood` plugin as -part of [CLI-R11](https://github.com/nself-org/cli/blob/main/.claude) (the +part of the thin-core extraction (CLI-R11); see the thin-core extraction). The command surface is unchanged — only where the code lives changed. diff --git a/.github/wiki/cmd-encryption.md b/.github/wiki/cmd-encryption.md index da7008654..8f83734d2 100644 --- a/.github/wiki/cmd-encryption.md +++ b/.github/wiki/cmd-encryption.md @@ -7,7 +7,7 @@ ## This command moved `encryption` was extracted from the core binary into the `encryption` plugin -as part of [CLI-R11](https://github.com/nself-org/cli/blob/main/.claude) (the +as part of the thin-core extraction (CLI-R11); see the thin-core extraction). The command surface is unchanged — only where the code lives changed. diff --git a/.github/wiki/cmd-federation.md b/.github/wiki/cmd-federation.md index daf8e36d2..addd46999 100644 --- a/.github/wiki/cmd-federation.md +++ b/.github/wiki/cmd-federation.md @@ -7,7 +7,7 @@ ## This command moved `federation` was extracted from the core binary into the `federation` plugin -as part of [CLI-R11](https://github.com/nself-org/cli/blob/main/.claude) (the +as part of the thin-core extraction (CLI-R11); see the thin-core extraction). The command surface is unchanged — only where the code lives changed. diff --git a/.github/wiki/cmd-gateway.md b/.github/wiki/cmd-gateway.md index 4829efe7d..ee8eba2d4 100644 --- a/.github/wiki/cmd-gateway.md +++ b/.github/wiki/cmd-gateway.md @@ -7,7 +7,7 @@ ## This command moved `gateway` was extracted from the core binary into the `gateway` plugin as -part of [CLI-R11](https://github.com/nself-org/cli/blob/main/.claude) (the +part of the thin-core extraction (CLI-R11); see the thin-core extraction). The command surface is unchanged — only where the code lives changed. diff --git a/.github/wiki/cmd-gauth.md b/.github/wiki/cmd-gauth.md index 4258ae2e5..2b81aa33a 100644 --- a/.github/wiki/cmd-gauth.md +++ b/.github/wiki/cmd-gauth.md @@ -7,7 +7,7 @@ ## This command moved `gauth` was extracted from the core binary into the `gauth` plugin as part of -[CLI-R11](https://github.com/nself-org/cli/blob/main/.claude) (the thin-core +the thin-core extraction (CLI-R11); see the thin-core extraction). The command surface is unchanged — only where the code lives changed. diff --git a/.github/wiki/cmd-gdpr.md b/.github/wiki/cmd-gdpr.md index 5bd4d9551..ad85f05ba 100644 --- a/.github/wiki/cmd-gdpr.md +++ b/.github/wiki/cmd-gdpr.md @@ -7,7 +7,7 @@ ## This command moved `gdpr` was extracted from the core binary into the `gdpr` plugin as part of -[CLI-R11](https://github.com/nself-org/cli/blob/main/.claude) (the thin-core +the thin-core extraction (CLI-R11); see the thin-core extraction). The command surface is unchanged — only where the code lives changed. diff --git a/.github/wiki/cmd-generate.md b/.github/wiki/cmd-generate.md index d019310bf..2b4130213 100644 --- a/.github/wiki/cmd-generate.md +++ b/.github/wiki/cmd-generate.md @@ -95,7 +95,7 @@ Use generated types with the ɳSelf SDK packages: | Kotlin | `io.nself:client` | `import io.nself.generated.*` | | Python | `nself-client` | `from generated.python.models import User` | -See [SDK reference](https://nself.org/docs/sdks/) for full usage. +See [[SDK-Reference]] for full usage. ## Notes diff --git a/.github/wiki/cmd-k8s.md b/.github/wiki/cmd-k8s.md index 9f5f68e1d..10de87d18 100644 --- a/.github/wiki/cmd-k8s.md +++ b/.github/wiki/cmd-k8s.md @@ -7,7 +7,7 @@ ## This command moved `k8s` was extracted from the core binary into the `k8s` plugin as part of -[CLI-R11](https://github.com/nself-org/cli/blob/main/.claude) (the thin-core +the thin-core extraction (CLI-R11); see the thin-core extraction). The command surface is unchanged — only where the code lives changed. diff --git a/.github/wiki/cmd-mail.md b/.github/wiki/cmd-mail.md index 394ab3d30..d20b1f5e1 100644 --- a/.github/wiki/cmd-mail.md +++ b/.github/wiki/cmd-mail.md @@ -7,7 +7,7 @@ ## This command moved `mail` was extracted from the core binary into the `mail` plugin as part of -[CLI-R11](https://github.com/nself-org/cli/blob/main/.claude) (the thin-core +the thin-core extraction (CLI-R11); see the thin-core extraction). The command surface is unchanged — only where the code lives changed. diff --git a/.github/wiki/cmd-model.md b/.github/wiki/cmd-model.md index 84f7a59a3..ffc20b5c8 100644 --- a/.github/wiki/cmd-model.md +++ b/.github/wiki/cmd-model.md @@ -7,7 +7,7 @@ ## This command moved `model` was extracted from the core binary into the `model` plugin as part of -[CLI-R11](https://github.com/nself-org/cli/blob/main/.claude) (the thin-core +the thin-core extraction (CLI-R11); see the thin-core extraction). The command surface is unchanged — only where the code lives changed. This includes the `ollama` subcommand tree ([[cmd-ollama]]), which was already a child of `model`, not a separate top-level command. diff --git a/.github/wiki/cmd-monitor.md b/.github/wiki/cmd-monitor.md index db81557b1..cad5cc48a 100644 --- a/.github/wiki/cmd-monitor.md +++ b/.github/wiki/cmd-monitor.md @@ -7,7 +7,7 @@ ## This command moved `monitor` was extracted from the core binary into the `monitor` plugin as -part of [CLI-R11](https://github.com/nself-org/cli/blob/main/.claude) (the +part of the thin-core extraction (CLI-R11); see the thin-core extraction). The command surface is unchanged — only where the code lives changed. diff --git a/.github/wiki/cmd-oauth.md b/.github/wiki/cmd-oauth.md index 2892ed45c..0aaad5574 100644 --- a/.github/wiki/cmd-oauth.md +++ b/.github/wiki/cmd-oauth.md @@ -62,8 +62,8 @@ nself oauth refresh --all ### See Also -- [Observability](https://nself.org/docs/operations/observability), oauth_token_refresh_total metric -- [google plugin](https://nself.org/docs/plugins/google), Google OAuth configuration +- [[Monitoring]], oauth_token_refresh_total metric +- [[plugin-google]], Google OAuth configuration ### Examples diff --git a/.github/wiki/cmd-pentest-kit.md b/.github/wiki/cmd-pentest-kit.md index f8e89d79a..7c82f6abf 100644 --- a/.github/wiki/cmd-pentest-kit.md +++ b/.github/wiki/cmd-pentest-kit.md @@ -8,7 +8,7 @@ `pentest-kit` was extracted from the core binary into the `pentest-kit` plugin (Business+ tier) as part of -[CLI-R11](https://github.com/nself-org/cli/blob/main/.claude) (the thin-core +the thin-core extraction (CLI-R11); see the thin-core extraction). The command surface is unchanged — only where the code lives changed. It still requires `NSELF_PENTEST_KIT=true`, the `pentest` service plugin installed, and a Business+/Enterprise/Owner license key. diff --git a/.github/wiki/cmd-soak.md b/.github/wiki/cmd-soak.md index 87bb917ba..e4f381853 100644 --- a/.github/wiki/cmd-soak.md +++ b/.github/wiki/cmd-soak.md @@ -7,7 +7,7 @@ ## This command moved `soak` was extracted from the core binary into the `soak` plugin as part of -[CLI-R11](https://github.com/nself-org/cli/blob/main/.claude) (the thin-core +the thin-core extraction (CLI-R11); see the thin-core extraction). The command surface is unchanged — only where the code lives changed. diff --git a/.github/wiki/cmd-start.md b/.github/wiki/cmd-start.md index 2d9ef0931..c3feaa2c7 100644 --- a/.github/wiki/cmd-start.md +++ b/.github/wiki/cmd-start.md @@ -56,7 +56,7 @@ Since v1.2.2, when the project uses a default local domain (unset, `localhost`, ``` error: v0.9 project detected. Found 3 legacy artifact(s): docker-compose.yml, .env, nginx/nginx.conf -Run `nself migrate` first. See https://nself.org/docs/migrate/from-v0.9 +Run `nself migrate` first. See [[Guide-Migration]] ``` A single hit produces a non-blocking warning. Use `nself migrate detect` to see all detected artifacts before running the migration. See [[Upgrade-From-v0.9]] for the full migration guide. diff --git a/.github/wiki/cmd-template.md b/.github/wiki/cmd-template.md index 59bd0dd33..979b66a84 100644 --- a/.github/wiki/cmd-template.md +++ b/.github/wiki/cmd-template.md @@ -18,7 +18,7 @@ Templates are full-stack app packages that include a Postgres schema, Hasura met Use `nself template list` to browse from the CLI, `nself template info ` to inspect a specific template, and `nself init --template ` to install one into a new project directory. -Authors publish via `nself template publish` and receive 80% revenue share on paid templates (requires a KYC-approved author account, see [B55 plugin-author-revenue-share](https://nself.org/docs/developers/revenue-share)). +Authors publish via `nself template publish` and receive 80% revenue share on paid templates (requires a KYC-approved author account, see the plugin author revenue share policy). ## Bundled Clone Templates @@ -184,7 +184,7 @@ nself template update --force - [[cmd-init]], Project initialisation including built-in language scaffolds - [[cmd-plugin]], Plugin management - [Template Marketplace](https://nself.org/templates), Browse templates in the browser -- [Publish a template](https://nself.org/docs/templates/publish-template), Author guide +- [[cmd-template-publish]], Author guide [[Home]] diff --git a/.github/wiki/cmd-update.md b/.github/wiki/cmd-update.md index 0bb379935..cad40889b 100644 --- a/.github/wiki/cmd-update.md +++ b/.github/wiki/cmd-update.md @@ -150,7 +150,7 @@ When running on a v0.9 CLI (any `0.x.y` binary), `nself update --check` queries ``` WARNING: You are running a legacy v0.9 CLI. Migration to v1.1.1 is required. -Upgrade guide: https://nself.org/docs/migrate/from-v0.9 +Upgrade guide: [[Guide-Migration]] ``` The check is read-only and non-destructive. It does not install anything. See [[Upgrade-From-v0.9]] for the full migration guide. diff --git a/.github/wiki/cmd-waf.md b/.github/wiki/cmd-waf.md index ea564b762..996e944ac 100644 --- a/.github/wiki/cmd-waf.md +++ b/.github/wiki/cmd-waf.md @@ -7,7 +7,7 @@ ## This command moved `waf` was extracted from the core binary into the `waf` plugin as part of -[CLI-R11](https://github.com/nself-org/cli/blob/main/.claude) (the thin-core +the thin-core extraction (CLI-R11); see the thin-core extraction). The command surface is unchanged — only where the code lives changed. diff --git a/.github/wiki/cmd-watchdog.md b/.github/wiki/cmd-watchdog.md index f87bd3858..e32657c2c 100644 --- a/.github/wiki/cmd-watchdog.md +++ b/.github/wiki/cmd-watchdog.md @@ -7,7 +7,7 @@ ## This command moved `watchdog` was extracted from the core binary into the `watchdog` plugin as -part of [CLI-R11](https://github.com/nself-org/cli/blob/main/.claude) (the +part of the thin-core extraction (CLI-R11); see the thin-core extraction). The command surface is unchanged — only where the code lives changed. diff --git a/tools/wikigen/main.go b/tools/wikigen/main.go index f4cd6b51c..7006c8d10 100644 --- a/tools/wikigen/main.go +++ b/tools/wikigen/main.go @@ -34,6 +34,7 @@ import ( "fmt" "os" "path/filepath" + "regexp" "sort" "strings" @@ -192,6 +193,18 @@ func main() { } if *check { + badLinks, err := auditProseLinks(*dir) + if err != nil { + fmt.Fprintf(os.Stderr, "audit prose links: %v\n", err) + os.Exit(1) + } + if len(badLinks) > 0 { + fmt.Fprintf(os.Stderr, "%d wiki command page(s) contain forbidden links:\n", len(badLinks)) + for _, b := range badLinks { + fmt.Fprintf(os.Stderr, " %s\n", b) + } + os.Exit(1) + } if len(stale) > 0 { fmt.Fprintf(os.Stderr, "%d wiki command page(s) are stale — run `make wiki-commands`:\n %s\n", len(stale), strings.Join(stale, "\n ")) @@ -239,3 +252,43 @@ func pageStale(path, expected string) bool { func legacyPageNames(cmd string) []string { return []string{cmd + ".md"} } + +// auditProseLinks fails when any cmd-*.md page contains a link into the +// private .claude workspace or an nself.org/docs URL that is not in the +// published docs nav. Both classes of link 404 in public renders (D-0717). +func auditProseLinks(dir string) ([]string, error) { + entries, err := os.ReadDir(dir) + if err != nil { + return nil, err + } + var bad []string + claudeRe := regexp.MustCompile(`github\.com/nself-org/cli/blob/main/\.claude`) + docsRe := regexp.MustCompile(`https://nself\.org/docs/[^\s\)]+`) + for _, e := range entries { + if e.IsDir() || !strings.HasPrefix(e.Name(), "cmd-") || !strings.HasSuffix(e.Name(), ".md") { + continue + } + path := filepath.Join(dir, e.Name()) + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + body := string(data) + if claudeRe.MatchString(body) { + bad = append(bad, fmt.Sprintf("%s: contains .claude/ link", e.Name())) + } + for _, m := range docsRe.FindAllString(body, -1) { + // Allow only pages known to exist in the public docs nav. + switch strings.TrimRight(m, ").,;:") { + case "https://nself.org/docs/commands", + "https://nself.org/docs/plugins", + "https://nself.org/docs/guides", + "https://nself.org/docs/reference": + continue + default: + bad = append(bad, fmt.Sprintf("%s: broken docs link %s", e.Name(), m)) + } + } + } + return bad, nil +} From 6f04072873edde15f7aa7696e2ff3da2ac6c4d49 Mon Sep 17 00:00:00 2001 From: Aric Camarata Date: Wed, 28 Oct 2026 09:31:50 -0400 Subject: [PATCH 2/8] fix(wiki): regenerate surface parity matrix for documented NSELF_PLUGIN_REGISTRY (D-0659) --- .github/surface-parity.json | 2 +- .github/surface-parity.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/surface-parity.json b/.github/surface-parity.json index 39bb6739a..276442a74 100644 --- a/.github/surface-parity.json +++ b/.github/surface-parity.json @@ -216,7 +216,7 @@ "wiki_page": true, "mcp_tool": true, "mcp_tools": 10, - "env_vars": "undocumented: NSELF_PLUGIN_REGISTRY", + "env_vars": "documented", "http_route": true, "http_routes": 10 } diff --git a/.github/surface-parity.md b/.github/surface-parity.md index d5f48ee70..41872b96a 100644 --- a/.github/surface-parity.md +++ b/.github/surface-parity.md @@ -29,6 +29,6 @@ One row per top-level command (CLI-R17), scored against the four surfaces a comm | `nself start` | core | yes | 1 | undocumented: NSELF_PROFILE, NSELF_SKIP_DB_INIT | 1 | | `nself status` | core | yes | 8 | n/a | 8 | | `nself stop` | core | yes | 1 | n/a | 1 | -| `nself update` | account | yes | 10 | undocumented: NSELF_PLUGIN_REGISTRY | 10 | +| `nself update` | account | yes | 10 | documented | 10 | -Total: 20 commands. Missing wiki page: 0. No MCP tool: 1. No HTTP route: 1. Env vars found but undocumented: 8. +Total: 20 commands. Missing wiki page: 0. No MCP tool: 1. No HTTP route: 1. Env vars found but undocumented: 7. From 7d883aa8c8ba06ce49bff06e997e6bb66e616eca Mon Sep 17 00:00:00 2001 From: Aric Camarata Date: Wed, 28 Oct 2026 16:37:29 -0400 Subject: [PATCH 3/8] fix(wiki): correct backup retention docs, repair broken wikilinks, audit wikilink targets on all pages (D-0717) --- .github/wiki/Guide-Backup-Restore.md | 12 +-- .github/wiki/Licensing-Guide.md | 4 +- .github/wiki/cmd-generate.md | 2 - .github/wiki/cmd-oauth.md | 2 +- .github/wiki/cmd-start.md | 2 +- .github/wiki/cmd-template.md | 1 - .github/wiki/cmd-update.md | 2 +- tools/wikigen/audit.go | 105 +++++++++++++++++++++++++++ tools/wikigen/audit_test.go | 95 ++++++++++++++++++++++++ tools/wikigen/main.go | 43 +---------- 10 files changed, 213 insertions(+), 55 deletions(-) create mode 100644 tools/wikigen/audit.go create mode 100644 tools/wikigen/audit_test.go diff --git a/.github/wiki/Guide-Backup-Restore.md b/.github/wiki/Guide-Backup-Restore.md index f41bd1d9a..700192210 100644 --- a/.github/wiki/Guide-Backup-Restore.md +++ b/.github/wiki/Guide-Backup-Restore.md @@ -74,15 +74,17 @@ The restore command: ## Retention and Prune Holds -`nself backup prune` removes backups older than `BACKUP_RETENTION_DAYS`, but it never deletes a backup tagged `keep` or `keep-*`. The `update stack` command tags its pre-update backup `keep-stack-` so an interrupted or failed update can always roll back; those backups survive prune until you release them. To release a held backup, rename or remove the tag: +`nself backup prune` applies a tiered retention policy: it keeps the newest backup of each of the last N days, N weeks (Sundays) and N months (the 1st). The defaults are 7 daily, 4 weekly and 12 monthly. Set them with `BACKUP_RETENTION_DAILY`, `BACKUP_RETENTION_WEEKLY` and `BACKUP_RETENTION_MONTHLY`, or per run with `--keep-daily`, `--keep-weekly` and `--keep-monthly`. `BACKUP_RETENTION_DAYS` is a legacy setting that prune does not read. Preview a run with `nself backup prune --dry-run`. + +Prune never deletes a backup whose tag is `keep` or starts with `keep-`. The `update stack` command tags its pre-update backup `keep-stack--` so an interrupted or failed update can always roll back; those backups survive prune until you release them. + +The tag is the part of the file name after the timestamp, for example `myproj_full_20261028_120000_keep-stack--.dump`. There is no command to change a tag. To release a held backup, rename the file in your backup directory so the tag no longer starts with `keep` (keep the extension, including `.age` on encrypted backups), then prune: ```bash -nself backup tag --rename released- # drops the keep prefix -nself backup prune # now eligible for removal +mv myproj_full_20261028_120000_keep-stack-abc123.dump myproj_full_20261028_120000_released-stack-abc123.dump +nself backup prune --dry-run # the renamed backup is now subject to the policy ``` -A restore that targets a `keep-stack-*` backup runs in strict mode: it refuses to continue when the backup manifest does not match the current project name or when the dump header is missing the expected schema version marker. Strict mode prevents restoring an unrelated stack snapshot into a live project. Override with `--force` only when you have verified the backup belongs to this project. - > **Important:** Test your restores regularly. Untested backups are not backups. ## Backup Verification Checklist diff --git a/.github/wiki/Licensing-Guide.md b/.github/wiki/Licensing-Guide.md index 066fd3cae..356e62036 100644 --- a/.github/wiki/Licensing-Guide.md +++ b/.github/wiki/Licensing-Guide.md @@ -45,9 +45,9 @@ Each bundle page lists its current plugin membership and purpose. Plugin lists c - [[bundle-nclaw]] — AI personal assistant - [[bundle-nchat]] — Messaging and voice - [[bundle-nfamily]] — Private family social platform -- [[bundle-nsentry]] — Observability and incident management +- [[cmd-sentry]] — Observability and incident management - [[bundle-clawde]] — Dev environment cloud sync -- [[bundle-ntask]] — Free core productivity plugins +- [[Bundles]] — Free core productivity plugins (the Task Bundle) --- diff --git a/.github/wiki/cmd-generate.md b/.github/wiki/cmd-generate.md index 2b4130213..d2e730cd2 100644 --- a/.github/wiki/cmd-generate.md +++ b/.github/wiki/cmd-generate.md @@ -95,8 +95,6 @@ Use generated types with the ɳSelf SDK packages: | Kotlin | `io.nself:client` | `import io.nself.generated.*` | | Python | `nself-client` | `from generated.python.models import User` | -See [[SDK-Reference]] for full usage. - ## Notes - The command uses the existing `HASURA_GRAPHQL_ADMIN_SECRET` from your `.env` cascade for local environments. diff --git a/.github/wiki/cmd-oauth.md b/.github/wiki/cmd-oauth.md index 0aaad5574..905c39006 100644 --- a/.github/wiki/cmd-oauth.md +++ b/.github/wiki/cmd-oauth.md @@ -62,7 +62,7 @@ nself oauth refresh --all ### See Also -- [[Monitoring]], oauth_token_refresh_total metric +- [[monitoring]], oauth_token_refresh_total metric - [[plugin-google]], Google OAuth configuration diff --git a/.github/wiki/cmd-start.md b/.github/wiki/cmd-start.md index c3feaa2c7..2d9ef0931 100644 --- a/.github/wiki/cmd-start.md +++ b/.github/wiki/cmd-start.md @@ -56,7 +56,7 @@ Since v1.2.2, when the project uses a default local domain (unset, `localhost`, ``` error: v0.9 project detected. Found 3 legacy artifact(s): docker-compose.yml, .env, nginx/nginx.conf -Run `nself migrate` first. See [[Guide-Migration]] +Run `nself migrate` first. See https://nself.org/docs/migrate/from-v0.9 ``` A single hit produces a non-blocking warning. Use `nself migrate detect` to see all detected artifacts before running the migration. See [[Upgrade-From-v0.9]] for the full migration guide. diff --git a/.github/wiki/cmd-template.md b/.github/wiki/cmd-template.md index 979b66a84..edcd5e27a 100644 --- a/.github/wiki/cmd-template.md +++ b/.github/wiki/cmd-template.md @@ -184,7 +184,6 @@ nself template update --force - [[cmd-init]], Project initialisation including built-in language scaffolds - [[cmd-plugin]], Plugin management - [Template Marketplace](https://nself.org/templates), Browse templates in the browser -- [[cmd-template-publish]], Author guide [[Home]] diff --git a/.github/wiki/cmd-update.md b/.github/wiki/cmd-update.md index cad40889b..0bb379935 100644 --- a/.github/wiki/cmd-update.md +++ b/.github/wiki/cmd-update.md @@ -150,7 +150,7 @@ When running on a v0.9 CLI (any `0.x.y` binary), `nself update --check` queries ``` WARNING: You are running a legacy v0.9 CLI. Migration to v1.1.1 is required. -Upgrade guide: [[Guide-Migration]] +Upgrade guide: https://nself.org/docs/migrate/from-v0.9 ``` The check is read-only and non-destructive. It does not install anything. See [[Upgrade-From-v0.9]] for the full migration guide. diff --git a/tools/wikigen/audit.go b/tools/wikigen/audit.go new file mode 100644 index 000000000..5d48eab1a --- /dev/null +++ b/tools/wikigen/audit.go @@ -0,0 +1,105 @@ +package main + +import ( + "fmt" + "io/fs" + "os" + "path/filepath" + "regexp" + "sort" + "strings" +) + +var ( + claudeLinkRe = regexp.MustCompile(`github\.com/nself-org/cli/blob/main/\.claude`) + docsLinkRe = regexp.MustCompile(`https://nself\.org/docs/[^\s\)]+`) + wikiLinkRe = regexp.MustCompile(`\[\[([^\]|#]+)`) + inlineCodeRe = regexp.MustCompile("`[^`]*`") + publishedDocs = map[string]bool{ + "https://nself.org/docs/commands": true, + "https://nself.org/docs/plugins": true, + "https://nself.org/docs/guides": true, + "https://nself.org/docs/reference": true, + } +) + +// wikiPageSet returns the basename (no .md) of every page under dir. GitHub +// Wiki flattens the namespace, so a page in a subdirectory is reachable by its +// basename. +func wikiPageSet(dir string) (map[string]bool, error) { + pages := map[string]bool{} + err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if !d.IsDir() && strings.HasSuffix(d.Name(), ".md") { + pages[strings.TrimSuffix(d.Name(), ".md")] = true + } + return nil + }) + return pages, err +} + +// auditProseLinks fails when a cmd-*.md page links into the private .claude +// workspace or to an nself.org/docs URL that is not in the published docs nav, +// or when any wiki page has a [[wikilink]] whose target page does not exist. +// Each class of link 404s in public renders (D-0717). +// +// Inputs: the wiki directory (searched recursively). Outputs: one message per +// problem, sorted by page. Constraints: fenced code blocks hold verbatim CLI +// output, so they are skipped for wikilinks (as scripts/ci/wiki-link-audit.sh +// does) and for docs URLs; inline `code` spans are skipped for wikilinks. +func auditProseLinks(dir string) ([]string, error) { + pages, err := wikiPageSet(dir) + if err != nil { + return nil, err + } + var paths []string + err = filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error { + if err == nil && !d.IsDir() && strings.HasSuffix(d.Name(), ".md") { + paths = append(paths, path) + } + return err + }) + if err != nil { + return nil, err + } + sort.Strings(paths) + + var bad []string + for _, path := range paths { + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + name := filepath.Base(path) + proseOnly := strings.HasPrefix(name, "cmd-") + if proseOnly && claudeLinkRe.Match(data) { + bad = append(bad, fmt.Sprintf("%s: contains .claude/ link", name)) + } + inFence := false + for _, line := range strings.Split(string(data), "\n") { + trimmed := strings.TrimSpace(line) + if strings.HasPrefix(trimmed, "```") || strings.HasPrefix(trimmed, "~~~") { + inFence = !inFence + continue + } + if inFence { + continue + } + for _, m := range docsLinkRe.FindAllString(line, -1) { + if proseOnly && !publishedDocs[strings.TrimRight(m, ").,;:")] { + bad = append(bad, fmt.Sprintf("%s: broken docs link %s", name, m)) + } + } + for _, m := range wikiLinkRe.FindAllStringSubmatch(inlineCodeRe.ReplaceAllString(line, ""), -1) { + target := strings.TrimSuffix(strings.TrimSpace(m[1]), ".md") + target = target[strings.LastIndex(target, "/")+1:] + if target != "" && !pages[target] { + bad = append(bad, fmt.Sprintf("%s: broken wikilink [[%s]]", name, target)) + } + } + } + } + return bad, nil +} diff --git a/tools/wikigen/audit_test.go b/tools/wikigen/audit_test.go new file mode 100644 index 000000000..65d9e6a84 --- /dev/null +++ b/tools/wikigen/audit_test.go @@ -0,0 +1,95 @@ +package main + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func writeWiki(t *testing.T, files map[string]string) string { + t.Helper() + dir := t.TempDir() + for name, body := range files { + path := filepath.Join(dir, name) + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, []byte(body), 0o644); err != nil { + t.Fatal(err) + } + } + return dir +} + +func audit(t *testing.T, files map[string]string) string { + t.Helper() + bad, err := auditProseLinks(writeWiki(t, files)) + if err != nil { + t.Fatal(err) + } + return strings.Join(bad, "\n") +} + +// A wikilink to a page that does not exist is reported on cmd-* pages and on +// every other page (D-0717). +func TestAuditProseLinksBrokenWikilink(t *testing.T) { + got := audit(t, map[string]string{ + "cmd-start.md": "See [[Guide-Migration]]\n", + "Licensing-Guide.md": "- [[bundle-ntask]] and [[Home]]\n", + "Home.md": "home\n", + }) + for _, want := range []string{ + "cmd-start.md: broken wikilink [[Guide-Migration]]", + "Licensing-Guide.md: broken wikilink [[bundle-ntask]]", + } { + if !strings.Contains(got, want) { + t.Errorf("missing %q in:\n%s", want, got) + } + } + if strings.Contains(got, "[[Home]]") { + t.Errorf("existing page reported broken:\n%s", got) + } +} + +// Targets resolve by basename (subdirectories are flattened), with anchors, +// pipes and a trailing .md ignored; fenced and inline code are not links. +func TestAuditProseLinksResolution(t *testing.T) { + got := audit(t, map[string]string{ + "Home.md": "[[releasing]] [[sdk/releasing#top]] [[releasing.md]] [[Home|the home page]]\n`[[nope]]`\n```\n[[also-nope]]\n```\n~~~\n[[tilde-nope]]\n~~~\n", + "sdk/releasing.md": "x\n", + }) + if got != "" { + t.Errorf("expected no findings, got:\n%s", got) + } +} + +// The docs-URL allowlist is exactly the four published nav pages. +func TestAuditProseLinksDocsAllowlist(t *testing.T) { + allowed := []string{"commands", "plugins", "guides", "reference"} + var body strings.Builder + for _, a := range allowed { + body.WriteString("[x](https://nself.org/docs/" + a + ")\n") + } + if got := audit(t, map[string]string{"cmd-a.md": body.String()}); got != "" { + t.Errorf("allowlisted URLs flagged:\n%s", got) + } + got := audit(t, map[string]string{"cmd-a.md": "[x](https://nself.org/docs/migrate/from-v0.9)\nhttps://nself.org/docs/commands/db.\n"}) + for _, want := range []string{"broken docs link https://nself.org/docs/migrate/from-v0.9", "broken docs link https://nself.org/docs/commands/db."} { + if !strings.Contains(got, want) { + t.Errorf("missing %q in:\n%s", want, got) + } + } +} + +// Docs URLs in fenced CLI output are verbatim text, not links, and docs URLs +// on non-cmd pages are outside this audit; a .claude link on a cmd page fails. +func TestAuditProseLinksScope(t *testing.T) { + got := audit(t, map[string]string{ + "cmd-start.md": "```\nSee https://nself.org/docs/migrate/from-v0.9\n```\n[c](https://github.com/nself-org/cli/blob/main/.claude/x.md)\n", + "other.md": "https://nself.org/docs/licensing/offline\n[c](https://github.com/nself-org/cli/blob/main/.claude/x.md)\n", + }) + if got != "cmd-start.md: contains .claude/ link" { + t.Errorf("unexpected findings:\n%s", got) + } +} diff --git a/tools/wikigen/main.go b/tools/wikigen/main.go index 7006c8d10..3ecb6ad04 100644 --- a/tools/wikigen/main.go +++ b/tools/wikigen/main.go @@ -34,7 +34,6 @@ import ( "fmt" "os" "path/filepath" - "regexp" "sort" "strings" @@ -199,7 +198,7 @@ func main() { os.Exit(1) } if len(badLinks) > 0 { - fmt.Fprintf(os.Stderr, "%d wiki command page(s) contain forbidden links:\n", len(badLinks)) + fmt.Fprintf(os.Stderr, "%d wiki page(s) contain broken or forbidden links:\n", len(badLinks)) for _, b := range badLinks { fmt.Fprintf(os.Stderr, " %s\n", b) } @@ -252,43 +251,3 @@ func pageStale(path, expected string) bool { func legacyPageNames(cmd string) []string { return []string{cmd + ".md"} } - -// auditProseLinks fails when any cmd-*.md page contains a link into the -// private .claude workspace or an nself.org/docs URL that is not in the -// published docs nav. Both classes of link 404 in public renders (D-0717). -func auditProseLinks(dir string) ([]string, error) { - entries, err := os.ReadDir(dir) - if err != nil { - return nil, err - } - var bad []string - claudeRe := regexp.MustCompile(`github\.com/nself-org/cli/blob/main/\.claude`) - docsRe := regexp.MustCompile(`https://nself\.org/docs/[^\s\)]+`) - for _, e := range entries { - if e.IsDir() || !strings.HasPrefix(e.Name(), "cmd-") || !strings.HasSuffix(e.Name(), ".md") { - continue - } - path := filepath.Join(dir, e.Name()) - data, err := os.ReadFile(path) - if err != nil { - return nil, err - } - body := string(data) - if claudeRe.MatchString(body) { - bad = append(bad, fmt.Sprintf("%s: contains .claude/ link", e.Name())) - } - for _, m := range docsRe.FindAllString(body, -1) { - // Allow only pages known to exist in the public docs nav. - switch strings.TrimRight(m, ").,;:") { - case "https://nself.org/docs/commands", - "https://nself.org/docs/plugins", - "https://nself.org/docs/guides", - "https://nself.org/docs/reference": - continue - default: - bad = append(bad, fmt.Sprintf("%s: broken docs link %s", e.Name(), m)) - } - } - } - return bad, nil -} From 33372875738312d9335cedb7b6cdea6d66ba59a6 Mon Sep 17 00:00:00 2001 From: Aric Camarata Date: Wed, 28 Oct 2026 18:59:22 -0400 Subject: [PATCH 4/8] fix(backup): honour BACKUP_RETENTION_* in prune when no --keep-* flag is passed (D-0717) --- cmd/commands/backup_prune_retention_test.go | 48 +++++++++++++++++++++ cmd/commands/backup_verify_prune.go | 23 ++++++++-- 2 files changed, 68 insertions(+), 3 deletions(-) create mode 100644 cmd/commands/backup_prune_retention_test.go diff --git a/cmd/commands/backup_prune_retention_test.go b/cmd/commands/backup_prune_retention_test.go new file mode 100644 index 000000000..633f0eec5 --- /dev/null +++ b/cmd/commands/backup_prune_retention_test.go @@ -0,0 +1,48 @@ +package commands + +// Purpose: `nself backup prune` must honour BACKUP_RETENTION_* config when no +// --keep-* flag is passed (D-0717). Inputs: a throwaway cobra command with the +// real flag defaults and a config carrying retention values. Outputs: none. + +import ( + "testing" + + "github.com/nself-org/cli/internal/config" + "github.com/spf13/cobra" +) + +func newPruneRetentionCmd() *cobra.Command { + c := &cobra.Command{Use: "prune"} + c.Flags().Int("keep-daily", 7, "") + c.Flags().Int("keep-weekly", 4, "") + c.Flags().Int("keep-monthly", 12, "") + return c +} + +func TestPruneRetention(t *testing.T) { + cfg := &config.Config{} + cfg.Backup.RetentionDaily, cfg.Backup.RetentionWeekly, cfg.Backup.RetentionMonthly = 14, 8, 24 + + t.Run("config applies without flags", func(t *testing.T) { + d, w, m := pruneRetention(newPruneRetentionCmd(), cfg) + if d != 14 || w != 8 || m != 24 { + t.Fatalf("got %d/%d/%d, want config 14/8/24", d, w, m) + } + }) + t.Run("flag beats config", func(t *testing.T) { + c := newPruneRetentionCmd() + if err := c.Flags().Parse([]string{"--keep-daily", "2"}); err != nil { + t.Fatal(err) + } + d, w, m := pruneRetention(c, cfg) + if d != 2 || w != 8 || m != 24 { + t.Fatalf("got %d/%d/%d, want 2/8/24", d, w, m) + } + }) + t.Run("flag defaults when config empty", func(t *testing.T) { + d, w, m := pruneRetention(newPruneRetentionCmd(), &config.Config{}) + if d != 7 || w != 4 || m != 12 { + t.Fatalf("got %d/%d/%d, want 7/4/12", d, w, m) + } + }) +} diff --git a/cmd/commands/backup_verify_prune.go b/cmd/commands/backup_verify_prune.go index 0c137bafb..14ea5963a 100644 --- a/cmd/commands/backup_verify_prune.go +++ b/cmd/commands/backup_verify_prune.go @@ -9,6 +9,7 @@ import ( "fmt" "github.com/nself-org/cli/internal/backup" + "github.com/nself-org/cli/internal/config" "github.com/spf13/cobra" ) @@ -56,6 +57,24 @@ var backupPruneCmd = &cobra.Command{ RunE: runBackupPrune, } +// pruneRetention resolves the tiered retention counts for one prune run. +// An explicit --keep-* flag wins; otherwise the configured +// BACKUP_RETENTION_DAILY/WEEKLY/MONTHLY value applies, and the flag default is +// the last resort when config carries none. Reading the flag value alone would +// always return its non-zero default and hide the configured values. +func pruneRetention(cmd *cobra.Command, cfg *config.Config) (daily, weekly, monthly int) { + pick := func(flag string, configured int) int { + v, _ := cmd.Flags().GetInt(flag) + if !cmd.Flags().Changed(flag) && configured > 0 { + return configured + } + return v + } + return pick("keep-daily", cfg.Backup.RetentionDaily), + pick("keep-weekly", cfg.Backup.RetentionWeekly), + pick("keep-monthly", cfg.Backup.RetentionMonthly) +} + func runBackupPrune(cmd *cobra.Command, _ []string) error { cfg, err := loadProjectConfig() if err != nil { @@ -63,9 +82,7 @@ func runBackupPrune(cmd *cobra.Command, _ []string) error { } dryRun, _ := cmd.Flags().GetBool("dry-run") - keepDaily, _ := cmd.Flags().GetInt("keep-daily") - keepWeekly, _ := cmd.Flags().GetInt("keep-weekly") - keepMonthly, _ := cmd.Flags().GetInt("keep-monthly") + keepDaily, keepWeekly, keepMonthly := pruneRetention(cmd, cfg) format, _ := cmd.Flags().GetString("format") result, err := backup.Prune(cfg, backup.PruneOptions{ From 0ccb128c960b94c11a22942e355ab54d03308cd8 Mon Sep 17 00:00:00 2001 From: Aric Camarata Date: Thu, 29 Oct 2026 10:22:56 -0400 Subject: [PATCH 5/8] fix(wiki): replace stale tier tables with the bundle model and audit pricing pages (D-1289) --- .github/wiki/Plugin-Licensing.md | 18 +++++++----------- .github/wiki/Plugin-Overview.md | 13 +++++-------- tools/wikigen/audit.go | 25 ++++++++++++++++++++----- tools/wikigen/audit_test.go | 23 +++++++++++++++++++++++ 4 files changed, 55 insertions(+), 24 deletions(-) diff --git a/.github/wiki/Plugin-Licensing.md b/.github/wiki/Plugin-Licensing.md index 661aadeff..554490846 100644 --- a/.github/wiki/Plugin-Licensing.md +++ b/.github/wiki/Plugin-Licensing.md @@ -89,21 +89,17 @@ The same requirement applies to `nself license import` when importing unsigned c NSELF_LICENSE_SKIP_VERIFY=1 nself license import cache.json --force ``` -## Tier Entitlements +## Bundle Entitlements -| Tier | Monthly | Annual | Plugin Access | +| Plan | Monthly | Annual | Plugin Access | |------|---------|--------|--------------| -| Free | $0 | $0 | Free plugins only | -| Basic | $0.99 | $9.99 | Licensed plugins outside the Pro-tier AI suite | -| Pro | $1.99 | $19.99 | Basic + full AI suite | -| Elite | $4.99 | $49.99 | Pro + email support | -| Business | $9.99 | $99.99 | Elite + 24h support | -| Business+ | $49.99 | $499.99 | Business + dedicated channel | -| Enterprise | $99.99 | $999.99 | Business+ + managed DevOps | +| Free (incl. Task bundle) | $0 | $0 | Free plugins only | +| Single bundle (Chat, Claw, Family, Sentry, ClawDE) | $0.99 | $9.99 | That bundle's licensed plugins | +| ɳSelf+ | $3.99 | $39.99 | Every bundle + all apps + support | -Existing $9.99/yr keys are grandfathered to the Basic tier. +There is no Basic, Pro, Elite or Business tier. The authoritative catalog is [nself.org/pricing](https://nself.org/pricing), fetched by the CLI from `https://plugins.nself.org/bundles.json`. See [[Licensing-Guide]]. ## Related Pages -- [[Plugin-Overview]], Tier comparison and plugin categories +- [[Plugin-Overview]], Bundle comparison and plugin categories - [[Plugin-Install]], How to install plugins diff --git a/.github/wiki/Plugin-Overview.md b/.github/wiki/Plugin-Overview.md index 343cc42c2..0256e20c1 100644 --- a/.github/wiki/Plugin-Overview.md +++ b/.github/wiki/Plugin-Overview.md @@ -13,14 +13,11 @@ | Tier | Price | Includes | |------|-------|---------| -| **Free** | $0 | Core CLI + free plugins (MIT licensed) | -The CLI no longer uses Basic / Pro tiers. Bundles are independent purchases; see [[Licensing-Guide]] and [nself.org/pricing](https://nself.org/pricing) for the current catalog. -| **Elite** | $4.99/mo or $49.99/yr | Pro + email support | -| **Business** | $9.99/mo or $99.99/yr | Elite + 24h email support + priority feature requests | -| **Business+** | $49.99/mo or $499.99/yr | Business + dedicated support channel | -| **Enterprise** | $99.99/mo or $999.99/yr | Business+ + managed DevOps | +| **Free** | $0 | Core CLI + free plugins (MIT licensed), including the Task bundle | +| **Bundle** | $0.99/mo or $9.99/yr each | One product's licensed plugins (Chat, Claw, Family, Sentry, ClawDE) | +| **ɳSelf+** | $3.99/mo or $39.99/yr | Every bundle + all apps + support | -Annual plans save ~17% compared to monthly. Existing $9.99/yr keys are grandfathered to the Basic tier. +The CLI no longer uses Basic / Pro tiers. Bundles are independent purchases; see [[Licensing-Guide]] and [nself.org/pricing](https://nself.org/pricing) for the current catalog. ## Plugin Categories @@ -88,7 +85,7 @@ The free `monitoring` plugin bundles 10 services in one install: alertmanager, g ## Related Pages - [[Plugin-Install]], How to install, remove, and update plugins -- [[Plugin-Licensing]], License keys, tiers, and validation +- [[Plugin-Licensing]], License keys, bundles, and validation - [[Plugin-Dev-Guide]], Build your own plugin - [[Plugin-Architecture]], Technical internals diff --git a/tools/wikigen/audit.go b/tools/wikigen/audit.go index 5d48eab1a..513cf4ba3 100644 --- a/tools/wikigen/audit.go +++ b/tools/wikigen/audit.go @@ -11,10 +11,20 @@ import ( ) var ( - claudeLinkRe = regexp.MustCompile(`github\.com/nself-org/cli/blob/main/\.claude`) - docsLinkRe = regexp.MustCompile(`https://nself\.org/docs/[^\s\)]+`) - wikiLinkRe = regexp.MustCompile(`\[\[([^\]|#]+)`) - inlineCodeRe = regexp.MustCompile("`[^`]*`") + claudeLinkRe = regexp.MustCompile(`github\.com/nself-org/cli/blob/main/\.claude`) + docsLinkRe = regexp.MustCompile(`https://nself\.org/docs/[^\s\)]+`) + wikiLinkRe = regexp.MustCompile(`\[\[([^\]|#]+)`) + inlineCodeRe = regexp.MustCompile("`[^`]*`") + // staleTierRe matches a pricing-table row for a retired tier, or the retired + // Basic-grandfathering line (D-1289). + staleTierRe = regexp.MustCompile(`(?i)^\|\s*\**(Elite|Business\+?|Enterprise)\**\s*\|.*\$|grandfathered to the Basic tier`) + // pricingPages are the pages whose tier tables must follow the bundle model. + pricingPages = map[string]bool{ + "Plugin-Overview.md": true, + "Plugin-Licensing.md": true, + "Licensing-Guide.md": true, + "Feature-Plugins.md": true, + } publishedDocs = map[string]bool{ "https://nself.org/docs/commands": true, "https://nself.org/docs/plugins": true, @@ -43,7 +53,9 @@ func wikiPageSet(dir string) (map[string]bool, error) { // auditProseLinks fails when a cmd-*.md page links into the private .claude // workspace or to an nself.org/docs URL that is not in the published docs nav, // or when any wiki page has a [[wikilink]] whose target page does not exist. -// Each class of link 404s in public renders (D-0717). +// Each class of link 404s in public renders (D-0717). It also fails when a +// pricing page still carries a retired Elite/Business/Business+/Enterprise row +// or the Basic-grandfathering line (D-1289). // // Inputs: the wiki directory (searched recursively). Outputs: one message per // problem, sorted by page. Constraints: fenced code blocks hold verbatim CLI @@ -87,6 +99,9 @@ func auditProseLinks(dir string) ([]string, error) { if inFence { continue } + if pricingPages[name] && staleTierRe.MatchString(trimmed) { + bad = append(bad, fmt.Sprintf("%s: stale tier pricing line %q", name, trimmed)) + } for _, m := range docsLinkRe.FindAllString(line, -1) { if proseOnly && !publishedDocs[strings.TrimRight(m, ").,;:")] { bad = append(bad, fmt.Sprintf("%s: broken docs link %s", name, m)) diff --git a/tools/wikigen/audit_test.go b/tools/wikigen/audit_test.go index 65d9e6a84..1ed2ecb14 100644 --- a/tools/wikigen/audit_test.go +++ b/tools/wikigen/audit_test.go @@ -93,3 +93,26 @@ func TestAuditProseLinksScope(t *testing.T) { t.Errorf("unexpected findings:\n%s", got) } } + +// Retired tier rows and the Basic-grandfathering line fail on the pricing +// pages only; the bundle model passes (D-1289). +func TestAuditProseLinksStaleTiers(t *testing.T) { + stale := "| **Elite** | $4.99/mo | Pro |\n| Business+ | $49.99 | x |\n| Enterprise | $99.99 | x |\nExisting $9.99/yr keys are grandfathered to the Basic tier.\n" + got := audit(t, map[string]string{ + "Plugin-Overview.md": stale, + "Plugin-Licensing.md": stale, + "Support.md": stale, + }) + for _, page := range []string{"Plugin-Overview.md", "Plugin-Licensing.md"} { + if n := strings.Count(got, page+": stale tier pricing line"); n != 4 { + t.Errorf("%s: want 4 findings, got %d in:\n%s", page, n, got) + } + } + if strings.Contains(got, "Support.md") { + t.Errorf("non-pricing page flagged:\n%s", got) + } + ok := "| **Bundle** | $0.99/mo | One product |\n| ɳSelf+ | $3.99 | All |\nThere is no Basic tier.\n" + if got := audit(t, map[string]string{"Plugin-Overview.md": ok}); got != "" { + t.Errorf("bundle model flagged:\n%s", got) + } +} From a29662e43b793c66dbb83fa9762e6862a2881e4e Mon Sep 17 00:00:00 2001 From: Aric Camarata Date: Thu, 29 Oct 2026 10:39:05 -0400 Subject: [PATCH 6/8] fix(wiki): correct when E138 fires in Compose-Generation (D-1289) --- .github/wiki/Compose-Generation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/wiki/Compose-Generation.md b/.github/wiki/Compose-Generation.md index ada704f78..b93bab8e8 100644 --- a/.github/wiki/Compose-Generation.md +++ b/.github/wiki/Compose-Generation.md @@ -70,7 +70,7 @@ The compose file is assembled in this order. Services later in the list can depe 4. **Custom**, `CS_1` through `CS_10` 5. **Plugins**, loaded from `~/.nself/plugins/`, in install order -Object storage selection depends on the project version. A v1.5 project (`NSELF_V15=1`) generates `seaweedfs` and its `seaweedfs-init` sidecar when `OBJECT_STORAGE_ENABLED=true`; MinIO is no longer emitted by default and must be added explicitly with `nself add minio` (which fails with [[error-codes#e138]] when seaweedfs is already present). A v1.4 project keeps the original `minio` + `minio-init` pair. See [[Object-Storage]] for migration and configuration details. +Object storage selection depends on the project version. A v1.5 project (`NSELF_V15=1`) generates `seaweedfs` and its `seaweedfs-init` sidecar when `OBJECT_STORAGE_ENABLED=true`; MinIO is no longer emitted by default and must be added explicitly with `nself add minio`. A build that wants MinIO (`MINIO_ENABLED=true`) without the minio plugin installed fails with [[error-codes#e138]] before any file is written. A v1.4 project keeps the original `minio` + `minio-init` pair. See [[Object-Storage]] for migration and configuration details. --- From 024a576564cdcfa97da1e9078c9f84abc0f8c813 Mon Sep 17 00:00:00 2001 From: Aric Camarata Date: Thu, 29 Oct 2026 10:58:34 -0400 Subject: [PATCH 7/8] fix(wiki): list license key prefixes from validProductPrefixes and test the page against the code (D-1289) --- .github/wiki/Plugin-Licensing.md | 18 +++++++++---- internal/license/wiki_prefixes_test.go | 35 ++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 5 deletions(-) create mode 100644 internal/license/wiki_prefixes_test.go diff --git a/.github/wiki/Plugin-Licensing.md b/.github/wiki/Plugin-Licensing.md index 554490846..676adb3ba 100644 --- a/.github/wiki/Plugin-Licensing.md +++ b/.github/wiki/Plugin-Licensing.md @@ -10,11 +10,19 @@ License keys use the prefix format: nself_pro_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ``` -Accepted prefixes: -- `nself_pro_`, paid bundles and ɳSelf+ -- `nself_max_`, ɳSelf+ and enterprise -- `nself_ent_`, Enterprise tier -- `nself_owner_`, Owner/self-hosted tier +Accepted prefixes (`nself license set` rejects any other): +- `nself_owner_`, ɳSelf Owner +- `nself_plus_`, ɳSelf+ +- `nself_claw_`, ɳClaw +- `nself_clawde_`, ClawDE +- `nself_chat_`, ɳChat +- `nself_media_`, nTV +- `nself_family_`, nFamily +- `nself_pro_`, ɳSelf Pro +- `nself_ent_`, ɳSelf Enterprise +- `nself_max_`, ɳSelf Business+ + +The prefix only names the key's product for display. Which plugins a key unlocks comes from the signed license response, not the prefix. Keys must be at least 32 characters total. diff --git a/internal/license/wiki_prefixes_test.go b/internal/license/wiki_prefixes_test.go new file mode 100644 index 000000000..1d023d74d --- /dev/null +++ b/internal/license/wiki_prefixes_test.go @@ -0,0 +1,35 @@ +package license + +import ( + "os" + "regexp" + "testing" +) + +// TestWikiAcceptedPrefixesMatchCode keeps the "Accepted prefixes" list in +// Plugin-Licensing.md identical to validProductPrefixes, so the wiki cannot +// drift from what ValidateKeyFormat accepts (D-1289). +func TestWikiAcceptedPrefixesMatchCode(t *testing.T) { + data, err := os.ReadFile("../../.github/wiki/Plugin-Licensing.md") + if err != nil { + t.Fatalf("reading wiki page: %v", err) + } + re := regexp.MustCompile("(?m)^- `(nself_[a-z]+_)`, (.+)$") + got := map[string]string{} + for _, m := range re.FindAllStringSubmatch(string(data), -1) { + got[m[1]] = m[2] + } + if len(got) != len(validProductPrefixes) { + t.Errorf("wiki lists %d prefixes, code defines %d", len(got), len(validProductPrefixes)) + } + for _, pp := range validProductPrefixes { + name, ok := got[pp.Prefix] + if !ok { + t.Errorf("wiki is missing prefix %s", pp.Prefix) + continue + } + if name != pp.DisplayName { + t.Errorf("wiki names %s %q, code says %q", pp.Prefix, name, pp.DisplayName) + } + } +} From 042c306925bd0b1619c9b629f566cd6a42e35ba2 Mon Sep 17 00:00:00 2001 From: Aric Camarata Date: Thu, 29 Oct 2026 11:19:48 -0400 Subject: [PATCH 8/8] fix(wiki): replace Basic/Pro plan text and $1.99 price on bundle pages and extend the pricing guard to bundle-*.md (D-1289) --- .github/wiki/Licensing-Guide.md | 11 ++--------- .github/wiki/Plugin-Overview.md | 2 +- .github/wiki/bundle-clawde.md | 6 +++--- .github/wiki/bundle-nchat.md | 8 ++++---- .github/wiki/bundle-nclaw.md | 8 ++++---- .github/wiki/bundle-nfamily.md | 8 ++++---- tools/wikigen/audit.go | 11 +++++++++-- tools/wikigen/audit_test.go | 20 ++++++++++++++++++++ 8 files changed, 47 insertions(+), 27 deletions(-) diff --git a/.github/wiki/Licensing-Guide.md b/.github/wiki/Licensing-Guide.md index 356e62036..941abcf49 100644 --- a/.github/wiki/Licensing-Guide.md +++ b/.github/wiki/Licensing-Guide.md @@ -40,7 +40,7 @@ There is no "Basic" or "Pro" tier. Paid bundles are independent purchases; ɳSel ## Per-Product Plugin Bundles -Each bundle page lists its current plugin membership and purpose. Plugin lists change with releases; the live catalog at [nself.org/pricing](https://nself.org/pricing) is authoritative. +Each bundle page lists its plugin set and purpose. Plugin lists change with releases, and the ClawDE set is provisional; the live catalog at [nself.org/pricing](https://nself.org/pricing) is authoritative. - [[bundle-nclaw]] — AI personal assistant - [[bundle-nchat]] — Messaging and voice @@ -85,14 +85,7 @@ nself license remove ### Key Format -Keys use a prefix format: - -| Prefix | Tier | -|--------|------| -| `nself_pro_` | Pro tier and above | -| `nself_max_` | Pro tier | -| `nself_ent_` | Enterprise tier | -| `nself_owner_` | Owner/internal tier | +Keys use a `nself__` prefix, for example `nself_plus_` for ɳSelf+. The prefix only names the key's product; the signed license response decides which plugins it unlocks. [[Plugin-Licensing]] lists every accepted prefix. Keys must be at least 32 characters total. diff --git a/.github/wiki/Plugin-Overview.md b/.github/wiki/Plugin-Overview.md index 0256e20c1..8cfe7cd7f 100644 --- a/.github/wiki/Plugin-Overview.md +++ b/.github/wiki/Plugin-Overview.md @@ -72,7 +72,7 @@ Bundles group plugins that work together to deliver a complete feature set. Per | Bundle | Price | Plugins | Use Case | |--------|-------|---------|----------| | [[bundle-nclaw]] | $0.99/mo | ai, claw, claw-web, mux, voice, browser, google, notify, cron, claw-budget, claw-news | AI personal assistant: email, calendar, news, budget, tools | -| [[bundle-clawde]] | $1.99/mo / $19.99/yr | TBD (cloud sync, premium models, team features) | ClawDE+ remote/mobile sync via API | +| [[bundle-clawde]] | $0.99/mo / $9.99/yr | TBD (cloud sync, premium models, team features) | ClawDE+ remote/mobile sync via API | | [[bundle-nfamily]] | $0.99/mo | social, photos, activity-feed, moderation, realtime, cms, chat | Private family social, photo sharing, activity feeds | | [[bundle-nchat]] | $0.99/mo | chat, livekit, recording, moderation, bots, realtime, auth | Messaging with video calls, bots, and moderation | diff --git a/.github/wiki/bundle-clawde.md b/.github/wiki/bundle-clawde.md index 0d3c42c95..2bea07fd4 100644 --- a/.github/wiki/bundle-clawde.md +++ b/.github/wiki/bundle-clawde.md @@ -19,7 +19,7 @@ ClawDE itself runs free locally with limited functionality. The ClawDE bundle un ## Plugins Included -Candidate set: licensed plugins, all `tier: pro`. +Candidate set: licensed plugins, all with a `tier: pro` manifest label. Buying the ClawDE bundle (or ɳSelf+) unlocks them; there is no separate Basic plan. | Plugin | Tier | Language | What It Does | |--------|------|----------|-------------| @@ -58,7 +58,7 @@ ClawDE Desktop ↔ realtime ↔ ClawDE Mobile ## Installation ```bash -# Basic tier or higher (all ClawDE plugins are tier: pro) +# ClawDE bundle or ɳSelf+ license key nself license set nself_pro_xxxxx... nself plugin install realtime auth cms notify @@ -66,7 +66,7 @@ nself plugin install realtime auth cms notify nself build && nself start ``` -ClawDE subscription is $0.99/mo or $9.99/yr (separately bundled, see [Plugin-Licensing](Plugin-Licensing)). +The ClawDE bundle is $0.99/mo or $9.99/yr; ɳSelf+ ($3.99/mo or $39.99/yr) includes it. See [Plugin-Licensing](Plugin-Licensing) for license keys. ## Getting Started diff --git a/.github/wiki/bundle-nchat.md b/.github/wiki/bundle-nchat.md index 720f3d34c..51771e809 100644 --- a/.github/wiki/bundle-nchat.md +++ b/.github/wiki/bundle-nchat.md @@ -17,7 +17,7 @@ ## Plugins Included -The bundle's licensed plugins are all `tier: pro` (Basic tier or higher). +The bundle's licensed plugins all carry a `tier: pro` manifest label. Buying the ɳChat bundle (or ɳSelf+) unlocks all of them; there is no separate Basic plan. | Plugin | Tier | Language | What It Does | |--------|------|----------|-------------| @@ -61,7 +61,7 @@ Client (chat/) → realtime + chat → channels, DMs, threads ## Installation ```bash -# Basic tier or higher (all nChat plugins are tier: pro) +# ɳChat bundle or ɳSelf+ license key nself license set nself_pro_xxxxx... nself plugin install chat livekit recording moderation bots realtime auth @@ -69,7 +69,7 @@ nself plugin install chat livekit recording moderation bots realtime auth nself build && nself start ``` -Basic tier is $0.99/mo or $9.99/yr. See [Plugin-Licensing](Plugin-Licensing) for the tier comparison. +The ɳChat bundle is $0.99/mo or $9.99/yr; ɳSelf+ ($3.99/mo or $39.99/yr) includes it. See [Plugin-Licensing](Plugin-Licensing) for license keys. ## Getting Started @@ -77,7 +77,7 @@ Basic tier is $0.99/mo or $9.99/yr. See [Plugin-Licensing](Plugin-Licensing) for - ɳSelf CLI installed and a project initialized (`nself init`) - Backend running (`nself start`) -- Basic tier license key or higher +- ɳChat bundle or ɳSelf+ license key - TURN/STUN credentials for `livekit` if running across NAT - Object storage configured for `recording` (MinIO comes free with the ɳSelf stack) diff --git a/.github/wiki/bundle-nclaw.md b/.github/wiki/bundle-nclaw.md index 29b7f6b56..e0fd0861a 100644 --- a/.github/wiki/bundle-nclaw.md +++ b/.github/wiki/bundle-nclaw.md @@ -17,7 +17,7 @@ ## Plugins Included -The bundle's licensed plugins are mostly `tier: max` (require Pro tier or higher); a few are `tier: pro` (Basic tier or higher). +The bundle's licensed plugins carry a `tier: max` or `tier: pro` manifest label. Buying the ɳClaw bundle (or ɳSelf+) unlocks all of them; there is no separate Basic or Pro plan. | Plugin | Tier | Language | What It Does | |--------|------|----------|-------------| @@ -67,7 +67,7 @@ Voice / Text input → AI gateway → Claw runtime → Tools (browser, google, m ## Installation ```bash -# Pro tier required for tier: max plugins (ai, claw, claw-web, voice, browser, claw-budget) +# ɳClaw bundle or ɳSelf+ license key nself license set nself_pro_xxxxx... nself plugin install ai claw claw-web claw-budget claw-news voice browser google mux notify cron @@ -75,7 +75,7 @@ nself plugin install ai claw claw-web claw-budget claw-news voice browser google nself build && nself start ``` -Pro tier is $1.99/mo or $19.99/yr. See [Plugin-Licensing](Plugin-Licensing) for the tier comparison. +The ɳClaw bundle is $0.99/mo or $9.99/yr; ɳSelf+ ($3.99/mo or $39.99/yr) includes it. See [Plugin-Licensing](Plugin-Licensing) for license keys. ## Getting Started @@ -83,7 +83,7 @@ Pro tier is $1.99/mo or $19.99/yr. See [Plugin-Licensing](Plugin-Licensing) for - ɳSelf CLI installed and a project initialized (`nself init`) - Backend running (`nself start`) -- Pro tier license key (Basic does not include `tier: max` plugins) +- ɳClaw bundle or ɳSelf+ license key - Provider API keys (Anthropic, OpenAI, etc.) for the `ai` plugin - Google OAuth credentials for the `google` plugin diff --git a/.github/wiki/bundle-nfamily.md b/.github/wiki/bundle-nfamily.md index b427fa94e..381a6a10a 100644 --- a/.github/wiki/bundle-nfamily.md +++ b/.github/wiki/bundle-nfamily.md @@ -17,7 +17,7 @@ When the bundle is installed, your ɳSelf backend gains social posts, photo stor ## Plugins Included -The bundle's licensed plugins are all `tier: pro` (Basic tier or higher). +The bundle's licensed plugins all carry a `tier: pro` manifest label. Buying the ɳFamily bundle (or ɳSelf+) unlocks all of them; there is no separate Basic plan. | Plugin | Tier | Language | What It Does | |--------|------|----------|-------------| @@ -64,7 +64,7 @@ Family member posts → social → activity-feed → realtime → other family m ## Installation ```bash -# Basic tier or higher (all nFamily plugins are tier: pro) +# ɳFamily bundle or ɳSelf+ license key nself license set nself_pro_xxxxx... nself plugin install social photos activity-feed moderation realtime cms chat @@ -72,7 +72,7 @@ nself plugin install social photos activity-feed moderation realtime cms chat nself build && nself start ``` -Basic tier is $0.99/mo or $9.99/yr. See [Plugin-Licensing](Plugin-Licensing) for the tier comparison. +The ɳFamily bundle is $0.99/mo or $9.99/yr; ɳSelf+ ($3.99/mo or $39.99/yr) includes it. See [Plugin-Licensing](Plugin-Licensing) for license keys. ## Getting Started @@ -80,7 +80,7 @@ Basic tier is $0.99/mo or $9.99/yr. See [Plugin-Licensing](Plugin-Licensing) for - ɳSelf CLI installed and a project initialized (`nself init`) - Backend running (`nself start`) -- Basic tier license key or higher +- ɳFamily bundle or ɳSelf+ license key - Object storage (MinIO ships with the ɳSelf stack) for `photos` ### Step 1: Install the plugins diff --git a/tools/wikigen/audit.go b/tools/wikigen/audit.go index 513cf4ba3..ccef1991d 100644 --- a/tools/wikigen/audit.go +++ b/tools/wikigen/audit.go @@ -18,6 +18,10 @@ var ( // staleTierRe matches a pricing-table row for a retired tier, or the retired // Basic-grandfathering line (D-1289). staleTierRe = regexp.MustCompile(`(?i)^\|\s*\**(Elite|Business\+?|Enterprise)\**\s*\|.*\$|grandfathered to the Basic tier`) + // stalePlanRe matches a claim of the retired Basic/Pro plans or the retired + // $1.99 Pro price; noPlanRe exempts a sentence that denies them (D-1289). + stalePlanRe = regexp.MustCompile(`(?i)\b(basic|pro) tier\b|\$1\.99/mo`) + noPlanRe = regexp.MustCompile(`(?i)\bno\b[^.]*\b(basic|pro)\b`) // pricingPages are the pages whose tier tables must follow the bundle model. pricingPages = map[string]bool{ "Plugin-Overview.md": true, @@ -55,7 +59,8 @@ func wikiPageSet(dir string) (map[string]bool, error) { // or when any wiki page has a [[wikilink]] whose target page does not exist. // Each class of link 404s in public renders (D-0717). It also fails when a // pricing page still carries a retired Elite/Business/Business+/Enterprise row -// or the Basic-grandfathering line (D-1289). +// or the Basic-grandfathering line, and when a pricing or bundle-*.md page +// still sells a Basic/Pro tier or the $1.99 Pro price (D-1289). // // Inputs: the wiki directory (searched recursively). Outputs: one message per // problem, sorted by page. Constraints: fenced code blocks hold verbatim CLI @@ -99,7 +104,9 @@ func auditProseLinks(dir string) ([]string, error) { if inFence { continue } - if pricingPages[name] && staleTierRe.MatchString(trimmed) { + stalePlan := (pricingPages[name] || strings.HasPrefix(name, "bundle-")) && + stalePlanRe.MatchString(trimmed) && !noPlanRe.MatchString(trimmed) + if stalePlan || (pricingPages[name] && staleTierRe.MatchString(trimmed)) { bad = append(bad, fmt.Sprintf("%s: stale tier pricing line %q", name, trimmed)) } for _, m := range docsLinkRe.FindAllString(line, -1) { diff --git a/tools/wikigen/audit_test.go b/tools/wikigen/audit_test.go index 1ed2ecb14..4528eaf87 100644 --- a/tools/wikigen/audit_test.go +++ b/tools/wikigen/audit_test.go @@ -116,3 +116,23 @@ func TestAuditProseLinksStaleTiers(t *testing.T) { t.Errorf("bundle model flagged:\n%s", got) } } + +// Bundle pages must not sell a retired Basic/Pro plan or the retired $1.99 +// Pro price; a negation ("no Basic tier") and non-bundle pages pass (D-1289). +func TestAuditProseLinksBundlePagesStalePlans(t *testing.T) { + stale := "Pro tier is $1.99/mo or $19.99/yr.\n- Basic tier license key or higher\n# Pro tier required for tier: max plugins\n" + got := audit(t, map[string]string{ + "bundle-nclaw.md": stale, + "Support.md": stale, + }) + if n := strings.Count(got, "bundle-nclaw.md: stale tier pricing line"); n != 3 { + t.Errorf("bundle-nclaw.md: want 3 findings, got %d in:\n%s", n, got) + } + if strings.Contains(got, "Support.md") { + t.Errorf("non-pricing page flagged:\n%s", got) + } + ok := "The ɳClaw bundle is $0.99/mo; there is no separate Basic or Pro plan.\nThere is no Basic tier.\n" + if got := audit(t, map[string]string{"bundle-nclaw.md": ok}); got != "" { + t.Errorf("bundle model flagged:\n%s", got) + } +}