From 6d791f2b3893493074decca5f94bd2cb6f16e4af Mon Sep 17 00:00:00 2001 From: ammann <161159040+agammann@users.noreply.github.com> Date: Tue, 6 Oct 2026 22:08:29 -0700 Subject: [PATCH] Release dependable Sift 1.0 with MIT licensing and verified package delivery --- .github/workflows/check.yml | 66 ++++++++- CHANGELOG.md | 10 ++ CONTRIBUTING.md | 2 +- LICENSE | 21 +++ README.md | 17 ++- THIRD_PARTY_NOTICES.md | 6 +- docs/GETTING_STARTED.md | 22 +-- docs/MCP.md | 2 +- docs/OPERATIONS.md | 4 +- docs/RELEASING.md | 6 +- docs/V1.md | 39 +++++ docs/verification-v1.md | 13 ++ package.json | 9 +- pnpm-lock.yaml | 30 ++-- pnpm-workspace.yaml | 4 + scripts/install-check.mjs | 38 ++++- scripts/publish-release.cjs | 288 ++++++++++++++++++++++++++++++++++++ scripts/release.mjs | 12 +- src/model.ts | 2 +- ui/style.css | 4 +- 20 files changed, 541 insertions(+), 54 deletions(-) create mode 100644 LICENSE create mode 100644 docs/V1.md create mode 100644 docs/verification-v1.md create mode 100644 scripts/publish-release.cjs diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index a7739d5..b4af135 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -1,13 +1,14 @@ name: Sift checks - on: pull_request: push: branches: [main] - + workflow_dispatch: permissions: contents: read - +concurrency: + group: sift-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false jobs: verify: strategy: @@ -17,8 +18,8 @@ jobs: runs-on: ${{ matrix.os }} timeout-minutes: 15 steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: 24.19.0 - run: npm install --global pnpm@11.19.0 @@ -29,3 +30,58 @@ jobs: - run: pnpm audit --audit-level=low - run: pnpm run release - run: pnpm run verify:package + - name: Keep the tested Linux release archive + if: matrix.os == 'ubuntu-22.04' + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: sift-release + path: | + artifacts/sift-local-*.tgz + artifacts/sift-local-*.tgz.sha256 + artifacts/SHA256SUMS + if-no-files-found: error + package_acceptance: + name: Released archive on ${{ matrix.os }} + needs: verify + strategy: + fail-fast: false + matrix: + os: [windows-latest, ubuntu-22.04, macos-latest] + runs-on: ${{ matrix.os }} + timeout-minutes: 10 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: 24.19.0 + - run: npm install --global pnpm@11.19.0 + - run: pnpm install --frozen-lockfile + - name: Download the exact Linux archive from this checked run + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: sift-release + path: artifacts + - run: pnpm run verify:package + publish: + name: Publish verified release + needs: [verify, package_acceptance] + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + permissions: + contents: write + concurrency: + group: sift-release + cancel-in-progress: false + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - name: Read the same accepted archive + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: sift-release + path: release-artifacts + - name: Publish complete archive and checksum assets + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const publish = require('./scripts/publish-release.cjs'); + await publish({ github, context, core }); diff --git a/CHANGELOG.md b/CHANGELOG.md index 5cda64b..ea4330c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## 1.0.0 + +Defined the bounded local documentation/MCP contract, supported-source matrix and backup-first upgrade from 0.1.2. Sift's own source and bundled release are now MIT licensed; third-party notices retain their original licenses. + +Updated MCP SDK to 1.31.0 and patched the compatible proxy-addr and source-map-js dependencies. Collection behavior, SQLite schema, extraction, search and five read-only MCP tool names remain unchanged. + +Separated the mobile collection-deletion action from the footer explanation so the destructive control remains clear. + +Added an immutable GitHub release archive with SHA256 sidecars. Publication requires source checks and fresh package installation on Windows, Linux and macOS. All three platforms check the exact Linux-built archive before it is published. The archive includes the UI, bundled backend, documentation, migrations, examples and license; it needs Node 24 and no runtime npm installation. + ## 0.1.2 — 2026-10-02 Updated compatible `fast-uri` and `ip-address` dependencies used by the MCP SDK. The complete dependency audit now reports no known advisories. The release bundles the updated runtime. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 53a124f..80beecf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -58,4 +58,4 @@ For an installable artifact, follow the complete [release procedure](docs/RELEAS Use [GitHub Issues](https://github.com/agammann/sift/issues) for reproducible bugs or focused feature proposals. Include the Sift version, Node version, operating system, expected result, actual result, and minimal steps. Remove credentials, private URLs, database paths containing personal information, and document content before sharing logs. -Discuss substantial changes before implementing them. Keep proposed patches focused and describe how they were verified. Public source visibility does not grant an open source license; the project remains `UNLICENSED`, and licensing decisions belong to the repository owner. +Discuss substantial changes before implementing them. Keep proposed patches focused and describe how they were verified. Sift's own source is available under the [MIT License](LICENSE). Bundled third-party dependencies retain their original licenses in [Third party notices](THIRD_PARTY_NOTICES.md). diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e095f52 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 agammann + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 9a6dcad..5393dff 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Collect the public documentation your project needs, search it locally, and let your coding assistant retrieve passages with source URLs and revision history through MCP. -[Website](https://sift-docs.alx21.chatgpt.site/) · [Download 0.1.2](https://sift-docs.alx21.chatgpt.site/downloads/sift-local-0.1.2.tgz) · [Getting started](docs/GETTING_STARTED.md) · [MCP setup](docs/MCP.md) +[Website](https://sift-docs.alx21.chatgpt.site/) · [Download 1.0.0](https://github.com/agammann/sift/releases/download/v1.0.0/sift-local-1.0.0.tgz) · [Getting started](docs/GETTING_STARTED.md) · [MCP setup](docs/MCP.md) ## What Sift does @@ -12,24 +12,24 @@ Collect the public documentation your project needs, search it locally, and let 2. **Inspect:** search retained pages, open their original sources, compare refresh outcomes, and read previous revisions. 3. **Connect:** expose selected collections to a local coding assistant through five read only MCP tools. -Sift 0.1.2 is a local application for one user. Your collections live in a SQLite database on your computer. No Sift account, model API key, paid service, or telemetry is required. The public website provides information and downloads; the application runs on your machine. +Sift 1.0.0 is a local application for one user. Your collections live in a SQLite database on your computer. No Sift account, model API key, paid service, or telemetry is required. Current immutable archives and checksums are available from GitHub Releases. The companion website retains earlier downloads; the application runs on your machine. ## Quick start -Requires **Node.js 24.15 or later within the Node 24 release line**. Select Node 24 on the [official download page](https://nodejs.org/en/download), then open a new terminal and check `node --version`. Node 22 and Node 25 or later are not supported by this release. Clean package installation, CLI, HTTP interface and stdio MCP checks passed on Windows, Ubuntu 22.04 and macOS with Node 24.19.0. The browser walkthrough ran on Windows; see the [verification record](docs/verification-2026-10-02.md) for scope. +Requires **Node.js 24.15 or later within the Node 24 release line**. Select Node 24 on the [official download page](https://nodejs.org/en/download), then open a new terminal and check `node --version`. Node 22 and Node 25 or later are not supported by this release. Version 1.0 requires the exact release archive to pass clean CLI, HTTP interface and stdio MCP installation checks on Windows, Ubuntu 22.04 and macOS with Node 24.19.0. The browser walkthrough runs on Windows; see the [v1 contract](docs/V1.md) and [verification record](docs/verification-v1.md) for scope. -1. Download the [Sift archive](https://sift-docs.alx21.chatgpt.site/downloads/sift-local-0.1.2.tgz) and [SHA256 checksum](https://sift-docs.alx21.chatgpt.site/downloads/sift-local-0.1.2.tgz.sha256). +1. Download the [Sift archive](https://github.com/agammann/sift/releases/download/v1.0.0/sift-local-1.0.0.tgz) and [SHA256 checksum](https://github.com/agammann/sift/releases/download/v1.0.0/sift-local-1.0.0.tgz.sha256). 2. Put the archive in a new folder you will keep, and open a terminal in that folder. See the [installation guide](docs/GETTING_STARTED.md) for checksum commands and Windows instructions. 3. Extract and start Sift: ```sh -tar -xzf sift-local-0.1.2.tgz +tar -xzf sift-local-1.0.0.tgz node package/dist/cli.js version node package/dist/cli.js doctor node package/dist/cli.js start ``` -Expect `Sift 0.1.2`, a doctor result with `"ok": true`, and a startup message containing `http://127.0.0.1:4587`. +Expect `Sift 1.0.0`, a doctor result with `"ok": true`, and a startup message containing `http://127.0.0.1:4587`. 4. Open **http://127.0.0.1:4587** in your browser. Keep the terminal running. Press **Ctrl+C** to stop. @@ -58,7 +58,8 @@ For a small, repeatable walkthrough, use the [one page first collection](docs/GE | Find my data, back it up, upgrade, or troubleshoot | [Operations](docs/OPERATIONS.md) | | Build from source or propose a change | [Development and contributions](CONTRIBUTING.md) | | Understand crawling, storage, and access boundaries | [Architecture](docs/ARCHITECTURE.md) | -| See what was tested and what remains unverified | [October 2 acceptance checks](docs/verification-2026-10-02.md) · [0.1.1 checks](docs/REAL_WORLD_VERIFICATION.md) · [0.1.0 report](docs/VERIFICATION.md) | +| Understand supported sources, stable interfaces and upgrade | [v1 contract](docs/V1.md) | +| See what was tested and what remains unverified | [v1 checks](docs/verification-v1.md) · [October 2 acceptance checks](docs/verification-2026-10-02.md) · [0.1.1 checks](docs/REAL_WORLD_VERIFICATION.md) · [0.1.0 report](docs/VERIFICATION.md) | | Prepare a release | [Release procedure](docs/RELEASING.md) | | See release history and dependency notices | [Changelog](CHANGELOG.md) · [Third party notices](THIRD_PARTY_NOTICES.md) | @@ -68,4 +69,4 @@ Sift collects public, server rendered UTF-8 HTML. It does not sign in to website The management interface binds to `127.0.0.1`. MCP uses local stdio, with an explicit collection allowlist. The website address and management URL are not remote MCP endpoints. -Source is publicly visible, but an open source license has not been selected. The package is marked `UNLICENSED`. See the [verification report](docs/VERIFICATION.md) for the limits of the tested first release. +Sift’s own source and bundled release are available under the [MIT License](LICENSE). Third-party dependencies retain their original licenses in [Third party notices](THIRD_PARTY_NOTICES.md). See the [v1 contract](docs/V1.md) for supported use and the initial release stopping point. diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index de55216..b2d871c 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -76,7 +76,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -## @modelcontextprotocol/sdk@1.30.0 +## @modelcontextprotocol/sdk@1.31.0 Declared license: "MIT" @@ -6960,7 +6960,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -## proxy-addr@2.0.7 +## proxy-addr@2.0.8 Declared license: "MIT" @@ -7760,7 +7760,7 @@ SOFTWARE. -## source-map-js@1.2.1 +## source-map-js@1.2.2 Declared license: "BSD-3-Clause" diff --git a/docs/GETTING_STARTED.md b/docs/GETTING_STARTED.md index 7697df8..56674e0 100644 --- a/docs/GETTING_STARTED.md +++ b/docs/GETTING_STARTED.md @@ -2,7 +2,7 @@ [Back to the README](../README.md) -This guide takes you from the downloadable archive to a searchable collection. Sift runs on your computer. The [public website](https://sift-docs.alx21.chatgpt.site/) hosts information and downloads. +This guide takes you from the downloadable archive to a searchable collection. Sift runs on your computer. The [public website](https://sift-docs.alx21.chatgpt.site/) provides information and earlier downloads. Use the pinned [GitHub release](https://github.com/agammann/sift/releases/tag/v1.0.0) for the current archive. ## Before you begin @@ -12,22 +12,22 @@ Install **Node.js 24.15 or later within Node 24** from the [official Node.js dow node --version ``` -Expect `v24.15.0` or a newer `v24.x.x`. Other major versions are not supported by Sift 0.1.2. With Node 24.19.0, clean package installation, CLI, HTTP interface and stdio MCP checks passed on Windows, Ubuntu 22.04 and macOS. The complete browser walkthrough ran on Windows. See the [verification record](verification-2026-10-02.md) for exact scope; the manual checksum commands below have not been executed on macOS/Linux. +Expect `v24.15.0` or a newer `v24.x.x`. Other major versions are not supported by Sift 1.0.0. The release workflow requires the exact archive to pass clean CLI, HTTP interface and stdio MCP installation checks on Windows, Ubuntu 22.04 and macOS with Node 24.19.0. See the [v1 checks](verification-v1.md) and [supported-source matrix](V1.md#supported-sources-and-delivery). ## Download and verify Download both files into the same new folder, such as a folder named `Sift` in your Documents directory: -1. [sift-local-0.1.2.tgz](https://sift-docs.alx21.chatgpt.site/downloads/sift-local-0.1.2.tgz) -2. [sift-local-0.1.2.tgz.sha256](https://sift-docs.alx21.chatgpt.site/downloads/sift-local-0.1.2.tgz.sha256) +1. [sift-local-1.0.0.tgz](https://github.com/agammann/sift/releases/download/v1.0.0/sift-local-1.0.0.tgz) +2. [sift-local-1.0.0.tgz.sha256](https://github.com/agammann/sift/releases/download/v1.0.0/sift-local-1.0.0.tgz.sha256) Keep their original filenames. Open a terminal in that folder. On Windows, open the folder in File Explorer, right click an empty area, and choose **Open in Terminal**. Use a fresh folder so extraction will not overwrite another application named `package`. In **PowerShell**, compare the download with its checksum: ```powershell -$siftExpectedHash = ((Get-Content .\sift-local-0.1.2.tgz.sha256 -Raw).Trim() -split '\s+')[0] -$siftActualHash = (Get-FileHash .\sift-local-0.1.2.tgz -Algorithm SHA256).Hash +$siftExpectedHash = ((Get-Content .\sift-local-1.0.0.tgz.sha256 -Raw).Trim() -split '\s+')[0] +$siftActualHash = (Get-FileHash .\sift-local-1.0.0.tgz -Algorithm SHA256).Hash if ($siftActualHash -ne $siftExpectedHash) { throw 'Checksum mismatch. Download the archive again.' } 'Checksum matches.' ``` @@ -35,13 +35,13 @@ if ($siftActualHash -ne $siftExpectedHash) { throw 'Checksum mismatch. Download On **macOS**, use: ```sh -shasum -a 256 -c sift-local-0.1.2.tgz.sha256 +shasum -a 256 -c sift-local-1.0.0.tgz.sha256 ``` On **Linux**, use: ```sh -sha256sum -c sift-local-0.1.2.tgz.sha256 +sha256sum -c sift-local-1.0.0.tgz.sha256 ``` Expect `Checksum matches.` in PowerShell, or a result ending in `OK` on macOS/Linux. A mismatch means the file does not match the published checksum; do not continue with that copy. @@ -51,7 +51,7 @@ Expect `Checksum matches.` in PowerShell, or a result ending in `OK` on macOS/Li From the same folder: ```sh -tar -xzf sift-local-0.1.2.tgz +tar -xzf sift-local-1.0.0.tgz node package/dist/cli.js version node package/dist/cli.js doctor node package/dist/cli.js start @@ -61,7 +61,7 @@ The archive creates a `package` folder. Keep it where you intend to run Sift, es | Step | Expected result | | --- | --- | -| `version` | `Sift 0.1.2` | +| `version` | `Sift 1.0.0` | | `doctor` | JSON containing `"ok": true`, `"integrity": "ok"`, and the database path | | `start` | A message containing `http://127.0.0.1:4587` and the data path | @@ -108,7 +108,7 @@ Version labels describe what you know about the source; Sift does not infer vers If Node's npm command is available, you can install the downloaded archive globally instead of using its extracted CLI: ```sh -npm install --global ./sift-local-0.1.2.tgz +npm install --global ./sift-local-1.0.0.tgz sift version sift doctor sift start diff --git a/docs/MCP.md b/docs/MCP.md index 32ce724..aa11b73 100644 --- a/docs/MCP.md +++ b/docs/MCP.md @@ -42,7 +42,7 @@ On October 2, 2026, Codex app-server 0.159.2 on Windows launched the extracted 0 | Built in test passes, assistant fails | The test uses Sift's own paths. Check the assistant's saved configuration, machine, and filesystem access. | | Terminal appears to wait after launching mcp | Stdio waits for a protocol client. Use the assistant or Test MCP connection instead of typing into it. | -Sift uses the official MCP TypeScript SDK 1.30.0. API references: [server](https://ts.sdk.modelcontextprotocol.io/server) and [client](https://ts.sdk.modelcontextprotocol.io/client). +Sift 1.0 uses the official MCP TypeScript SDK 1.31.0. API references: [server](https://ts.sdk.modelcontextprotocol.io/server) and [client](https://ts.sdk.modelcontextprotocol.io/client). ## Tool contract diff --git a/docs/OPERATIONS.md b/docs/OPERATIONS.md index 10c31cb..caef5a0 100644 --- a/docs/OPERATIONS.md +++ b/docs/OPERATIONS.md @@ -12,7 +12,7 @@ Run `sift doctor` while the management process is stopped to see the exact path and database health. For an extracted archive, replace `sift` in commands throughout this guide with `node package/dist/cli.js`, running from the extraction folder. -Set `SIFT_DATA_DIR` to a directory, or pass `--db ABSOLUTE_DATABASE_FILE` to use a custom workspace. Use the same database for management and MCP. In 0.1.1, **Connect → Copy configuration** includes the running executable and database paths automatically. +Set `SIFT_DATA_DIR` to a directory, or pass `--db ABSOLUTE_DATABASE_FILE` to use a custom workspace. Use the same database for management and MCP. In 1.0, **Connect → Copy configuration** includes the running executable and database paths automatically. ## Start and stop @@ -45,7 +45,7 @@ Only restore trusted Sift backups you control. SQLite file validation is not a s Current and previous successful revisions are retained by default, configurable per source up to 20. A failed refresh does not consume a successful revision slot. Confirmed 404/410 documents leave active search but keep retained revisions. An expired revision ID returns an explicit missing-content error. Change events retain their revision IDs even if the content later expires. -Source/job/outcome/change records remain until their source or collection is deleted. There is no automatic time-based pruning in 0.1.0; monitor disk usage for long-lived high-refresh workspaces. UI deletion prompts identify the affected scope and permanently delete its source content/history. Back up first if recovery is needed. Whole-workspace restore also has explicit confirmation and a recovery copy. +Source/job/outcome/change records remain until their source or collection is deleted. There is no automatic time-based pruning in 1.0; monitor disk usage for long-lived high-refresh workspaces. UI deletion prompts identify the affected scope and permanently delete its source content/history. Back up first if recovery is needed. Whole-workspace restore also has explicit confirmation and a recovery copy. ## Interrupted jobs diff --git a/docs/RELEASING.md b/docs/RELEASING.md index ccfee77..68acfaa 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -8,8 +8,10 @@ Use a clean source checkout, Node 24.15 or later within Node 24, and pnpm 11.19. 4. Run `pnpm run notices` to collect license declarations/text from the installed exact dependency graph; review notices. Old package versions left in pnpm's store are excluded. 5. Run `pnpm run release`. This creates `artifacts/sift-local-VERSION.tgz` and a SHA-256 file. The backend, UI, migration, docs, examples and notices are included. Runtime npm dependencies/install scripts are omitted because the runtime is bundled. 6. Run `pnpm run verify:package`. It installs the archive for the current package version in a fresh temporary project with scripts disabled, then checks version, doctor, the interface, a management mutation, and a real stdio MCP connection. Also extract the archive into a fresh folder and try the README path. Verify the checksum and archive contents before distribution. -7. Update the verification report with actual platform and test evidence. Review `git diff`, scan tracked files for data/secrets, commit the lockfile, and tag the verified commit. +7. Update the verification report with actual platform and test evidence. Review `git diff`, scan tracked files for data/secrets, commit the lockfile, and open a reviewed pull request. The main release workflow creates the exact checked tag; do not move an existing published tag. The lockfile pins the dependency graph; bit-for-bit build or archive reproducibility is not promised. Checksums apply to the produced artifact, not future rebuilds. Keep a published version's archive immutable and use a new patch version for executable changes. -No script publishes to npm, GitHub Releases, a web host or a paid service. Public source is maintained on GitHub and downloadable archives are served by the companion website. Publishing updated downloads is a separate step: upload the verified archive and matching checksum, update website links, and confirm the downloaded bytes match the local artifact. Preserve old download URLs. License selection and cross-platform validation remain explicit decisions. +The repository workflow verifies source installation, build, tests, audit and packaging on three platforms. It then installs the same Linux-produced archive on Windows, Ubuntu and macOS. Only a main push whose checks all pass may publish: the job downloads those exact assets, validates archive and combined checksums, uploads a complete draft, checks uploaded digests, creates the tag at the verified commit, and publishes last. Write permission is confined to that job. Manual/PR checks do not publish. Existing published versions stay unchanged; a failed draft may be retried only for the same commit. No script publishes to npm, a web host or a paid service. + +After publication, verify the release tag and asset digests, download the released archive into a fresh folder, and run the documented checksum/extraction workflow. The companion website’s older downloads remain intact; updating its download links is a separate hosted change. Sift’s own code is MIT licensed and the bundled dependency notices must be preserved. diff --git a/docs/V1.md b/docs/V1.md new file mode 100644 index 0000000..8625023 --- /dev/null +++ b/docs/V1.md @@ -0,0 +1,39 @@ +# Sift 1.0 contract + +[Back to the README](../README.md) + +Sift 1.0 is a local documentation collector for one user. A newcomer can extract the release, create a collection, refresh a supported public source, inspect its retained text, and search it. A developer can build the source and connect an MCP client to that same persistent workspace. No account or model key is required. + +## Supported sources and delivery + +| Input or environment | Support in 1.0 | +| ------------------------------------------------------- | --------------------------------------------------------------------------------------------- | +| Public UTF-8 HTML, rendered by the server | Supported within one source's exact origin and allowed path prefixes | +| Ordinary HTML documentation with article/main text | Preferred; source URL, retained revision and collection version label accompany retrieval | +| Simple HTML without an article/main region | Body fallback; inspect extraction warnings | +| HTTP redirects | Supported only when the destination remains within the configured public origin/path boundary | +| JavaScript-only applications, PDFs, authenticated pages | Unsupported; Sift does not render, parse or sign in for these | +| Private or reserved network destinations | Rejected; no production bypass switch | +| Windows, macOS and Linux | Node 24.15 or newer within Node 24; release automation verifies Node 24.19.0 | +| npm registry | No registry package; install the local release archive or use its extracted CLI | +| Remote/cloud MCP | Unsupported; five read-only tools over local stdio only | + +Collection limits are explicit: 1–500 pages, depth 0–6, at most two concurrent requests, and 2–20 successful revisions retained per source. Refresh is manual. Start with one page before increasing those budgets. There is no automatic time-based record pruning; monitor your local disk and back up collections you want to keep. + +## Stable interfaces + +The 1.x line preserves CLI command names (`start`, `mcp`, `doctor`, `backup`, `restore`, `version`), database schema compatibility, and the five documented [MCP tools](MCP.md#tool-contract). Collection IDs and persisted revision IDs remain usable across an additive upgrade. MCP keeps its `{ok:true,data:...}` / `{ok:false,error:{code,message}}` envelope and explicit collection allowlist. Additional optional output fields may be added; clients should read the fields they need rather than reject every unknown output property. + +The loopback management HTTP routes and UI component internals are application internals, not a public integration SDK. Use MCP for read-only integration. Documentation contents remain untrusted source material, not instructions for an assistant to execute. + +Search is lexical AND matching, not a model answer or a guarantee that a page is current. Compare the quoted passage with its source URL, collection version label, retained revision and check time. A failed refresh keeps prior successful content and records the failed check. Confirmed unavailable pages leave current search while retained revisions remain readable until retention expires. A source or collection deletion permanently removes its content/history; back up before deleting. + +## Upgrade from 0.1.2 + +No database schema change is introduced by 1.0. Stop management and MCP clients, create a verified [backup](OPERATIONS.md#backup-and-restore), and extract 1.0 into a new folder. Run the new CLI with the same explicit database path, check `version` and `doctor`, then reopen a collection and perform an MCP search. Copy a fresh Connect configuration when the executable path changes. Keep the old executable and verified backup until this check succeeds; do not overwrite a live database or manually change its schema version. + +Sift's own code and release package are MIT licensed. [Third party notices](../THIRD_PARTY_NOTICES.md) preserve the dependency licenses. A checksum proves the downloaded bytes match the released artifact; it is not a promise that future rebuilds have identical bytes. + +## Initial release stopping point + +The 1.0 release requires a clean frozen source installation and build; passing existing collection, refresh, recovery and stdio MCP checks; a fresh extracted/package consumer that does not depend on source-checkout modules; a browser first-collection walkthrough; and a published immutable archive/checksum tied to the checked commit. Cross-platform jobs must pass before publication. Dependency maintenance, new source formats and additional assistant hosts continue after this bounded release. diff --git a/docs/verification-v1.md b/docs/verification-v1.md new file mode 100644 index 0000000..8ea8a14 --- /dev/null +++ b/docs/verification-v1.md @@ -0,0 +1,13 @@ +# Sift 1.0 verification scope + +The Windows candidate was checked with Node 24.19.0 and pnpm 11.19.0 on October 6, 2026. Fresh frozen installation, TypeScript/backend/UI build and all 18 existing tests passed after updating MCP SDK to 1.31.0 and the compatible proxy-addr/source-map-js patches. The complete dependency audit reported zero known advisories at that time. + +The existing tests exercise deterministic HTML collection and independently expected retrieval passages, changed/unchanged/failed/unavailable refreshes, retained revisions, backup/restore and migration rollback, writer exclusion and interruption recovery, real stdio MCP discovery/retrieval with an explicit collection allowlist, and copied Connect configuration launched without PATH. The retrieval fixture checks five expected passages in the top three; this is a fixture result rather than a comparative model-quality claim. + +Release automation requires fresh source checks on Windows, Ubuntu 22.04 and macOS, then installs the exact Linux-produced archive on all three platforms. It checks matching SHA256 sidecars, version/MIT metadata, included license, zero runtime dependencies/install scripts, doctor, management interface and mutation, and actual stdio MCP. These are required publication gates; see the release’s Actions run for their exact commit and outcome. + +The local extracted-archive walkthrough used Chromium 153.0.8010.12 on Windows at 1280×800 and 390×844. Create collection, add/refresh a one-page Example Domain source, search, inspect the retained revision, Connect diagnostics, reload and mobile layout passed without console or page errors. A separate stdio client launched the exact displayed executable/database configuration and called all five tools. Retrieved paragraph text matched an independent fetch of the public HTML; the no-match result was explicit. Example Domain is only a one-time onboarding smoke example, not a service to monitor or a promise about future network availability. + +The mobile walkthrough exposed the destructive footer action running directly into the explanatory text. The mobile footer now stacks those elements with spacing; the same rendered check was repeated after the change. A separate Codex assistant-host call was recorded for 0.1.2 on October 2 and remains dated historical evidence; it has not been repeated for every assistant host against 1.0. The 1.0 integration promise is the documented local stdio contract, not universal host compatibility. + +Supported sources and remaining limitations are in the [v1 contract](V1.md). No capacity, hostile-database sandbox or JavaScript/PDF/authenticated-page support is claimed. Dependency audit results are time-bound; ongoing maintenance follows the initial release. diff --git a/package.json b/package.json index 28a79ca..385a6d9 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,10 @@ { "name": "sift-local", - "version": "0.1.2", + "version": "1.0.0", "description": "Clean documentation. Clear sources. Local documentation collections and read-only MCP access.", "type": "module", "private": true, - "license": "UNLICENSED", + "license": "MIT", "engines": { "node": ">=24.15.0 <25" }, @@ -20,7 +20,8 @@ "migrations", "examples", "CHANGELOG.md", - "THIRD_PARTY_NOTICES.md" + "THIRD_PARTY_NOTICES.md", + "LICENSE" ], "scripts": { "build": "tsc --noEmit && node scripts/build.mjs", @@ -32,7 +33,7 @@ "verify:package": "node scripts/install-check.mjs" }, "dependencies": { - "@modelcontextprotocol/sdk": "1.30.0", + "@modelcontextprotocol/sdk": "1.31.0", "cheerio": "1.2.0", "ipaddr.js": "2.5.0", "react": "19.2.8", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6e9e751..8f7b3be 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,13 +4,17 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +overrides: + source-map-js: 1.2.2 + proxy-addr: 2.0.8 + importers: .: dependencies: '@modelcontextprotocol/sdk': - specifier: 1.30.0 - version: 1.30.0(zod@4.5.4) + specifier: 1.31.0 + version: 1.31.0(zod@4.5.4) cheerio: specifier: 1.2.0 version: 1.2.0 @@ -237,8 +241,8 @@ packages: '@mixmark-io/domino@2.2.0': resolution: {integrity: sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==} - '@modelcontextprotocol/sdk@1.30.0': - resolution: {integrity: sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==} + '@modelcontextprotocol/sdk@1.31.0': + resolution: {integrity: sha512-UvTMgnNlnIBO/22ob2RcVGDlcvOslQs8T59+FTGdA0L27a39fdGF/EDETNtDVK4DZGpwomlsYpRdA8UXcVL/pw==} engines: {node: '>=18'} peerDependencies: '@cfworker/json-schema': ^4.1.1 @@ -1165,8 +1169,8 @@ packages: property-information@7.2.0: resolution: {integrity: sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==} - proxy-addr@2.0.7: - resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + proxy-addr@2.0.8: + resolution: {integrity: sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==} engines: {node: '>= 0.10'} qs@6.16.0: @@ -1266,8 +1270,8 @@ packages: resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} engines: {node: '>= 0.4'} - source-map-js@1.2.1: - resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + source-map-js@1.2.2: + resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==} engines: {node: '>=0.10.0'} space-separated-tokens@2.0.2: @@ -1517,7 +1521,7 @@ snapshots: '@mixmark-io/domino@2.2.0': {} - '@modelcontextprotocol/sdk@1.30.0(zod@4.5.4)': + '@modelcontextprotocol/sdk@1.31.0(zod@4.5.4)': dependencies: '@hono/node-server': 2.1.1(hono@4.13.7) ajv: 8.20.0 @@ -1939,7 +1943,7 @@ snapshots: on-finished: 2.4.1 once: 1.4.0 parseurl: 1.3.3 - proxy-addr: 2.0.7 + proxy-addr: 2.0.8 qs: 6.16.0 range-parser: 1.3.0 router: 2.2.0 @@ -2563,13 +2567,13 @@ snapshots: dependencies: nanoid: 3.3.18 picocolors: 1.1.1 - source-map-js: 1.2.1 + source-map-js: 1.2.2 prettier@3.9.6: {} property-information@7.2.0: {} - proxy-addr@2.0.7: + proxy-addr@2.0.8: dependencies: forwarded: 0.2.0 ipaddr.js: 1.9.1 @@ -2747,7 +2751,7 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 - source-map-js@1.2.1: {} + source-map-js@1.2.2: {} space-separated-tokens@2.0.2: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 5ed0b5a..f08e2e7 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,2 +1,6 @@ allowBuilds: esbuild: true + +overrides: + source-map-js: 1.2.2 + proxy-addr: 2.0.8 diff --git a/scripts/install-check.mjs b/scripts/install-check.mjs index ba00695..1b1c17a 100644 --- a/scripts/install-check.mjs +++ b/scripts/install-check.mjs @@ -1,9 +1,10 @@ import { mkdtemp, writeFile, readFile, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; -import { join, resolve } from "node:path"; +import { join, resolve, dirname, basename } from "node:path"; import { spawnSync, spawn } from "node:child_process"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; +import { createHash } from "node:crypto"; const root = await mkdtemp(join(tmpdir(), "sift-clean-install-")); const manifest = JSON.parse(await readFile("package.json", "utf8")); const packageManager = process.env.npm_execpath; @@ -19,6 +20,15 @@ function command(args) { return r.stdout; } try { + const archive = resolve(`artifacts/${manifest.name}-${manifest.version}.tgz`); + const expectedChecksum = `${createHash("sha256") + .update(await readFile(archive)) + .digest("hex")} ${manifest.name}-${manifest.version}.tgz\n`; + if ( + (await readFile(archive + ".sha256", "utf8")) !== expectedChecksum || + (await readFile("artifacts/SHA256SUMS", "utf8")) !== expectedChecksum + ) + throw new Error("Release archive checksum does not match its sidecars"); await writeFile( join(root, "package.json"), '{"name":"sift-install-verification","private":true}', @@ -31,6 +41,27 @@ try { ]); const cli = join(root, "node_modules", "sift-local", "dist", "cli.js"), db = join(root, "data", "sift.sqlite"); + const installed = JSON.parse( + await readFile( + join(root, "node_modules", "sift-local", "package.json"), + "utf8", + ), + ); + if ( + installed.version !== manifest.version || + installed.license !== "MIT" || + Object.keys(installed.dependencies || {}).length || + Object.keys(installed.scripts || {}).length || + !( + await readFile( + join(root, "node_modules", "sift-local", "LICENSE"), + "utf8", + ) + ).includes("MIT License") + ) + throw new Error( + "Installed release metadata, license or bundled runtime contract differs", + ); if (command([cli, "version"]).trim() !== `Sift ${manifest.version}`) throw new Error("Wrong installed version"); const health = JSON.parse(command([cli, "doctor", "--db", db])); @@ -109,6 +140,11 @@ try { server.kill(); await exited; } + if ( + dirname(resolve(root)) !== resolve(tmpdir()) || + !basename(root).startsWith("sift-clean-install-") + ) + throw new Error("Unexpected temporary consumer path; cleanup stopped"); await rm(root, { recursive: true, force: true, diff --git a/scripts/publish-release.cjs b/scripts/publish-release.cjs new file mode 100644 index 0000000..9680a31 --- /dev/null +++ b/scripts/publish-release.cjs @@ -0,0 +1,288 @@ +const fs = require("node:fs"); +const path = require("node:path"); +const crypto = require("node:crypto"); + +module.exports = async ({ github, context, core }) => { + if ( + context.eventName !== "push" || + context.ref !== "refs/heads/main" || + !/^[0-9a-f]{40}$/.test(context.sha) + ) { + throw new Error( + "Publishing requires a main push with an exact commit SHA.", + ); + } + const manifest = JSON.parse(fs.readFileSync("package.json", "utf8")); + const version = manifest.version; + if (!/^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$/.test(version)) { + throw new Error("Expected one stable package version."); + } + const tag = "v" + version; + const { owner, repo } = context.repo; + const requireCurrentHead = async () => { + const branch = ( + await github.rest.repos.getBranch({ owner, repo, branch: "main" }) + ).data; + if (branch.commit.sha !== context.sha) + throw new Error( + "main moved beyond the checked release commit; publication stopped", + ); + }; + const releases = await github.paginate(github.rest.repos.listReleases, { + owner, + repo, + per_page: 100, + }); + const matching = releases.filter((release) => release.tag_name === tag); + if (matching.length > 1) throw new Error("Multiple releases use " + tag); + let release = matching[0]; + if (release && !release.draft) { + core.info( + "Release " + tag + " is already published; leaving it unchanged.", + ); + return; + } + if ( + release && + (release.target_commitish !== context.sha || release.prerelease) + ) { + throw new Error( + "Existing draft does not belong to this exact release commit.", + ); + } + + const tagCommit = async () => { + let object; + try { + object = ( + await github.rest.git.getRef({ owner, repo, ref: "tags/" + tag }) + ).data.object; + } catch (error) { + if (error.status === 404) return null; + throw error; + } + for (let depth = 0; depth < 10; depth++) { + if (object.type === "commit") return object.sha; + if (object.type !== "tag") + throw new Error("Release tag does not point to a commit."); + object = ( + await github.rest.git.getTag({ + owner, + repo, + tag_sha: object.sha, + }) + ).data.object; + } + throw new Error("Release tag has too many annotated tag layers."); + }; + const requireCorrectTag = async () => { + const commit = await tagCommit(); + if (commit !== null && commit !== context.sha) { + throw new Error("Existing release tag points to a different commit."); + } + return commit; + }; + await requireCorrectTag(); + + const directory = "release-artifacts"; + const filenames = fs.readdirSync(directory).sort(); + const archives = ["sift-local-" + version + ".tgz"]; + const expectedInputs = [ + archives[0], + archives[0] + ".sha256", + "SHA256SUMS", + ].sort(); + if (JSON.stringify(filenames) !== JSON.stringify(expectedInputs)) { + throw new Error( + "Downloaded artifacts contain unexpected or missing files.", + ); + } + const payloads = []; + const checksumLines = []; + const readFile = (filename) => { + const location = path.join(directory, filename); + if (!fs.lstatSync(location).isFile()) + throw new Error("Artifact is not a regular file."); + return fs.readFileSync(location); + }; + const digest = (data) => + "sha256:" + crypto.createHash("sha256").update(data).digest("hex"); + for (const filename of archives) { + const data = readFile(filename); + const checksum = readFile(filename + ".sha256"); + const expected = digest(data).slice(7) + " " + filename + "\n"; + if (checksum.toString("utf8") !== expected) { + throw new Error("Artifact checksum mismatch: " + filename); + } + checksumLines.push(expected); + payloads.push( + { name: filename, data }, + { name: filename + ".sha256", data: checksum }, + ); + } + const combined = Buffer.from(checksumLines.join(""), "utf8"); + if (!readFile("SHA256SUMS").equals(combined)) + throw new Error("Combined checksums differ."); + payloads.push({ name: "SHA256SUMS", data: combined }); + for (const payload of payloads) payload.digest = digest(payload.data); + + const changelog = fs + .readFileSync("CHANGELOG.md", "utf8") + .replaceAll("\r\n", "\n"); + const heading = "## " + version + "\n"; + if (!changelog.includes(heading)) + throw new Error("Changelog has no release section."); + const notes = changelog.split(heading)[1].split("\n## ")[0].trim(); + const body = + notes + + "\n\nDownload the tested local archive and verify its SHA256 checksum before extracting it. The archive includes the built interface, bundled backend, MIT license and third-party notices; Node 24.15 or newer within Node 24 is required. No npm runtime dependencies or install scripts are needed. This release does not publish a registry package.\n\n" + + "[Installation and upgrade instructions](https://github.com/" + + owner + + "/" + + repo + + "/blob/" + + tag + + "/README.md)."; + if (!release) { + await requireCurrentHead(); + release = ( + await github.rest.repos.createRelease({ + owner, + repo, + tag_name: tag, + target_commitish: context.sha, + name: "Sift " + version, + body, + draft: true, + prerelease: false, + }) + ).data; + } else { + release = ( + await github.rest.repos.updateRelease({ + owner, + repo, + release_id: release.id, + name: "Sift " + version, + body, + draft: true, + prerelease: false, + }) + ).data; + } + const existing = await github.paginate(github.rest.repos.listReleaseAssets, { + owner, + repo, + release_id: release.id, + per_page: 100, + }); + const wanted = new Map(payloads.map((payload) => [payload.name, payload])); + const existingNames = new Set(); + for (const asset of existing) { + if (!wanted.has(asset.name) || existingNames.has(asset.name)) { + throw new Error("Draft contains an unexpected or duplicate asset."); + } + existingNames.add(asset.name); + } + for (const payload of payloads) { + const previous = existing.find((asset) => asset.name === payload.name); + if ( + previous && + previous.digest === payload.digest && + previous.size === payload.data.length && + previous.state === "uploaded" + ) { + continue; + } + if (previous) { + await github.rest.repos.deleteReleaseAsset({ + owner, + repo, + asset_id: previous.id, + }); + } + const uploaded = ( + await github.rest.repos.uploadReleaseAsset({ + owner, + repo, + release_id: release.id, + name: payload.name, + data: payload.data, + headers: { + "content-type": "application/octet-stream", + "content-length": payload.data.length, + }, + }) + ).data; + if ( + uploaded.digest !== payload.digest || + uploaded.size !== payload.data.length || + uploaded.state !== "uploaded" + ) { + throw new Error("Uploaded asset verification failed: " + payload.name); + } + core.info("Verified uploaded asset: " + payload.name); + } + const finalAssets = await github.paginate( + github.rest.repos.listReleaseAssets, + { + owner, + repo, + release_id: release.id, + per_page: 100, + }, + ); + if ( + finalAssets.length !== payloads.length || + new Set(finalAssets.map((asset) => asset.name)).size !== payloads.length || + finalAssets.some((asset) => { + const expected = wanted.get(asset.name); + return ( + !expected || + asset.digest !== expected.digest || + asset.size !== expected.data.length || + asset.state !== "uploaded" + ); + }) + ) { + throw new Error( + "Draft assets do not match the complete verified package set.", + ); + } + const latest = ( + await github.rest.repos.getRelease({ + owner, + repo, + release_id: release.id, + }) + ).data; + if ( + !latest.draft || + latest.target_commitish !== context.sha || + latest.tag_name !== tag + ) { + throw new Error("Draft changed during upload; publication stopped."); + } + // Create the tag at the checked commit explicitly; target_commitish cannot move an existing tag. + await requireCurrentHead(); + if ((await requireCorrectTag()) === null) { + await github.rest.git.createRef({ + owner, + repo, + ref: "refs/tags/" + tag, + sha: context.sha, + }); + } + if ((await requireCorrectTag()) !== context.sha) { + throw new Error("Release tag is missing before publication."); + } + await requireCurrentHead(); + await github.rest.repos.updateRelease({ + owner, + repo, + release_id: release.id, + draft: false, + make_latest: "true", + }); + core.info("Published Sift " + version + " at " + context.sha); +}; diff --git a/scripts/release.mjs b/scripts/release.mjs index d38821e..aef3b72 100644 --- a/scripts/release.mjs +++ b/scripts/release.mjs @@ -1,6 +1,6 @@ import { mkdtemp, cp, readFile, writeFile, mkdir, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; -import { join, resolve } from "node:path"; +import { join, resolve, dirname, basename } from "node:path"; import { spawnSync } from "node:child_process"; import { createHash } from "node:crypto"; const p = JSON.parse(await readFile("package.json", "utf8")); @@ -17,6 +17,7 @@ try { "examples", "CHANGELOG.md", "THIRD_PARTY_NOTICES.md", + "LICENSE", ]) await cp(file, join(root, file), { recursive: true }); // Bundled runtime has zero npm dependencies and requires no install scripts. @@ -51,8 +52,17 @@ try { `${artifact}.sha256`, `${hash} ${p.name}-${p.version}.tgz\n`, ); + await writeFile( + resolve("artifacts", "SHA256SUMS"), + `${hash} ${p.name}-${p.version}.tgz\n`, + ); console.log(artifact); console.log(`SHA-256 ${hash}`); } finally { + if ( + dirname(resolve(stage)) !== resolve(tmpdir()) || + !basename(stage).startsWith("sift-release-") + ) + throw new Error("Unexpected temporary release path; cleanup stopped"); await rm(stage, { recursive: true, force: true }); } diff --git a/src/model.ts b/src/model.ts index 9cd5848..513ad0d 100644 --- a/src/model.ts +++ b/src/model.ts @@ -1,7 +1,7 @@ import { z } from "zod"; import { randomUUID } from "node:crypto"; -export const VERSION = "0.1.2"; +export const VERSION = "1.0.0"; export const id = () => randomUUID(); export const now = () => new Date().toISOString(); export class SiftError extends Error { diff --git a/ui/style.css b/ui/style.css index 90083df..87d168f 100644 --- a/ui/style.css +++ b/ui/style.css @@ -613,7 +613,9 @@ footer { flex-wrap: wrap; } footer { - display: block; + flex-direction: column; + align-items: flex-start; + gap: 10px; } .row-title { display: block;