diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index eff05d92..fd35598a 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -16,13 +16,42 @@ jobs:
timeout-minutes: 25 # backstop: a hung step fails fast instead of running for hours
steps:
- uses: actions/checkout@v4
- - name: Check out pro (paid features) into ./pro
+ # Check out pro on the branch that MATCHES this PR/push (so a coordinated
+ # core+pro change is tested together). If pro has no such branch (most PRs),
+ # this step's checkout fails quietly and the fallback below pulls pro `main`.
+ # Without this, CI silently tested the core branch against pro `main`, so
+ # pro's new test files were absent while pro source was still measured →
+ # coverage cratered (60% vs 97% local). See PR #46.
+ - name: Check out pro (matching branch) into ./pro
+ id: pro_branch
uses: actions/checkout@v4
continue-on-error: true
with:
repository: off-grid-ai/desktop-pro
token: ${{ secrets.CI_CROSS_REPO_TOKEN }}
path: pro
+ ref: ${{ github.head_ref || github.ref_name }}
+ # Don't leave the cross-repo PAT in ./pro/.git/config where a later
+ # PR-controlled step could reuse it — we only need it for the clone.
+ persist-credentials: false
+ - name: Fall back to pro main if no matching branch
+ if: ${{ steps.pro_branch.outcome != 'success' || hashFiles('pro/tsconfig.json') == '' }}
+ continue-on-error: true
+ uses: actions/checkout@v4
+ with:
+ repository: off-grid-ai/desktop-pro
+ token: ${{ secrets.CI_CROSS_REPO_TOKEN }}
+ path: pro
+ persist-credentials: false
+ # pro MUST be present: without it the guarded typecheck/coverage path is skipped and
+ # CI would pass core-only with cratered/misleading coverage (the failure the
+ # matching-branch checkout above was added to fix). Fail loudly instead of silently.
+ - name: Require pro checkout to have succeeded
+ run: |
+ if [ ! -f pro/tsconfig.json ]; then
+ echo "::error::pro (desktop-pro) was not checked out — cannot validate the open-core build. Check CI_CROSS_REPO_TOKEN / repo access."
+ exit 1
+ fi
- uses: actions/setup-node@v4
with:
node-version: '24' # node:sqlite (used by integration tests) is available unflagged
@@ -35,9 +64,22 @@ jobs:
if: ${{ hashFiles('pro/tsconfig.json') != '' }}
timeout-minutes: 6
run: cd pro && npx tsc --noEmit -p tsconfig.json
- - name: Test
+ # Runs the full vitest suite AND enforces the coverage ratchet floor
+ # (vitest.config.ts `thresholds`) — the same gate as the pre-push hook, so a
+ # coverage regression fails CI, not just local pushes. The pro/** threshold
+ # group applies only when pro was checked out above (guarded in the config).
+ - name: Test + coverage thresholds
timeout-minutes: 10
- run: npm test
+ run: npm run test:coverage
+ # Architecture-boundary gate (hygiene §A/§H): walks the import graph and fails
+ # on a forbidden edge — core->pro (open-core), a pure-logic module gaining an
+ # IO import, renderer->main, circular deps. Fast (graph-only, ~2s), so unlike
+ # eslint it can BLOCK without wedging CI. Rules verified clean on the tree.
+ - name: Dependency boundaries
+ timeout-minutes: 4
+ run: npm run depcruise
+ # NOTE: SonarCloud runs via Automatic Analysis (SonarCloud clones this repo and
+ # analyzes core on each push/PR — no CI step, token, or coverage upload here).
# Advisory: lint is enforced locally; bounded + non-blocking here so an
# eslint slowdown over the full tree (incl. checked-out pro/) can't wedge CI.
- name: Lint
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index c08f58f1..92fc11b2 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -89,9 +89,10 @@ jobs:
with:
ref: ${{ needs.version.outputs.sha }} # the exact bumped commit
fetch-depth: 0
- lfs: true # resources/bin/* (llama-server, sd-cli, whisper, ffmpeg, dylibs)
- # are Git LFS — without this CI bundles pointer stubs and every
- # runtime spawn fails with ENOEXEC.
+ lfs:
+ true # resources/bin/* (llama-server, sd-cli, whisper, ffmpeg, dylibs)
+ # are Git LFS — without this CI bundles pointer stubs and every
+ # runtime spawn fails with ENOEXEC.
- name: Pull LFS binaries
run: git lfs pull
# Rebuild the chat engine from source with a PINNED macOS deployment target.
diff --git a/.github/workflows/windows-build.yml b/.github/workflows/windows-build.yml
index 197d401c..49d4c14d 100644
--- a/.github/workflows/windows-build.yml
+++ b/.github/workflows/windows-build.yml
@@ -30,9 +30,10 @@ jobs:
- uses: actions/checkout@v4
with:
persist-credentials: false # don't leave the token in .git/config for npm postinstall scripts
- lfs: false # the repo's LFS binaries are macOS-only; Windows runtimes
- # come from fetch-win-binaries.ps1, so don't pull ~235MB of
- # dylibs we won't ship.
+ lfs:
+ false # the repo's LFS binaries are macOS-only; Windows runtimes
+ # come from fetch-win-binaries.ps1, so don't pull ~235MB of
+ # dylibs we won't ship.
# Private pro source into pro/ → __OFFGRID_PRO__ true, so the exe bundles the
# Pro layer (mirrors release.yml). Needed to exercise the Pro "coming soon"
diff --git a/.prettierignore b/.prettierignore
index 9c6b791d..95aa0f89 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -4,3 +4,15 @@ pnpm-lock.yaml
LICENSE.md
tsconfig.json
tsconfig.*.json
+
+# Agent worktrees + local Claude config: copies of the repo / ephemeral state, never ours to format.
+.claude
+
+# Vendored, minified third-party libraries served verbatim — reformatting them is meaningless and bloats the diff.
+resources/artifacts
+**/*.min.js
+**/*.min.css
+
+# Local, non-source scratch files.
+.offgrid
+pr-targets.local.md
diff --git a/CLAUDE.md b/CLAUDE.md
index 4f5f1403..b8dc022a 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -10,7 +10,7 @@ Full design doc: **`docs/DESIGN.md`**. The essentials, which OVERRIDE any mobile
- **Typeface: Menlo** (monospace) everywhere — terminal/brutalist.
- **Accent: emerald** — `#34D399` (dark) / `#059669` (light). THE accent for primary actions, active states, links, success. (Tailwind `green-500/400` is an acceptable stand-in but prefer the exact tokens.)
- **Base:** black / `#0A0A0A` + white; neutral grays for surfaces/borders/text tiers. Flat, sharp, dense.
-- Tokens: `@offgrid/design`. Brand canon: `mobile/docs/design/DESIGN_PHILOSOPHY_SYSTEM.md` (brand only — desktop *layout* follows `docs/DESIGN.md`, desktop-first).
+- Tokens: `@offgrid/design`. Brand canon: `mobile/docs/design/DESIGN_PHILOSOPHY_SYSTEM.md` (brand only — desktop _layout_ follows `docs/DESIGN.md`, desktop-first).
- Real brand logos (Simple Icons), no decorative tiles behind them; no gradients; no emojis in the UI.
### Use the screen real estate — desktop density rules
@@ -19,7 +19,7 @@ The window is WIDE. A list of cards/rows stretched edge-to-edge in a single colu
- **Multi-column responsive grids for collections.** Any list of comparable items (models, connectors, entities, meetings) is a grid that fills the width: `grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4`, not one full-width row each. A card's controls stay next to its content, never flung across empty space.
- **Tight, consistent spacing on a 4/8/12px scale.** Dense data UIs use narrow gutters (8-12px) and small padding, NOT the 16-24px editorial spacing. Body text ~12-14px, compact line-height. Flat and sharp, per the brand.
-- **Group, then separate.** Reduce gaps *within* a group (rows in a section) but keep clear separation *between* functional groups (filters vs data, "On this device" vs "Available"). Section headers over a wall of identical rows.
+- **Group, then separate.** Reduce gaps _within_ a group (rows in a section) but keep clear separation _between_ functional groups (filters vs data, "On this device" vs "Available"). Section headers over a wall of identical rows.
- **Progressive disclosure.** Secondary info and rarely-used controls go behind a detail panel / "…" / hover affordance — don't lay everything flat. Master list stays scannable; depth lives in the side panel or slide-over.
- **Sticky context.** Fix headers, tabs, filter bars, and column labels while the body scrolls, so context never scrolls away.
- **Finesse the interactions.** Every click gets a small micro-interaction — `transition-all duration-150`, `active:scale-95` on buttons, slide+fade (not abrupt mount) for panels/slide-overs. State changes animate; nothing pops in or out hard.
@@ -56,13 +56,42 @@ Two process rules, learned from the same incident — they matter as much as the
- Main-process changes need an app restart; renderer changes hot-reload.
- Don't over-restart — it interrupts capture.
-## Pending hygiene adoption — READ BEFORE EVERY PUSH
-
-**TODO (deferred to its own PR — do NOT bundle into an unrelated PR):** adopt the wednesday-solutions
-gold-standard code hygiene. The pre-push hook echoes this reminder so it isn't forgotten. Two parts:
-
-1. **Prettier — repo-wide reformat.** Adopt `.prettierrc`: `{ printWidth: 120, tabWidth: 2, useTabs: false, singleQuote: true, trailingComma: 'none' }`. Desktop has no `.prettierrc` today (prettier defaults), so applying this reformats the whole tree — a huge diff. Do it ALONE, in its own PR/commit, so it never swamps a feature/refactor diff.
-2. **ESLint — tighten to the gold-standard rules** (adapted to desktop's flat config + React web, not RN/redux): `curly: all`, `no-console: [allow error,warn]`, `no-else-return`, `no-empty`, `prefer-template`, `max-params: 3`, `complexity` (gold standard is 5 — start looser, e.g. 15-20, and ratchet), `max-lines-per-function: 250`, `max-lines: 350`, `@typescript-eslint/no-shadow`. Many current files violate the structural caps (MemoryChat 2601, ipc.ts 1707, etc.), so introduce them with a **ratchet** like the coverage floor: set to `warn` (or grandfather the known-large files) and tighten to `error` as the god-files get decomposed — never lower a threshold to pass. Gold-standard source: github.com/wednesday-solutions/react-template (`.eslintrc.js`, `.prettierrc`).
+## Commit incrementally — never batch a session's work into one commit
+
+A long agent session WILL hit a context/session limit; anything uncommitted is lost. Protect
+progress by committing continuously, not at the end:
+
+- **One logical change = one commit, landed as soon as it's green** (tsc + its tests pass). A bug
+ fix, a single consolidation/extraction, a doc update — each is its own small, self-contained,
+ well-described commit. Do NOT accumulate 5 unrelated edits and commit them together "later".
+- **Commit the moment a unit is verified**, before starting the next one — so a session cut-off at
+ any point leaves every finished unit already saved. Treat "done and green" as "commit now".
+- **Push regularly** (at least whenever you'd be sad to lose what's landed) so progress survives even
+ a lost local worktree; the pre-push gate (tsc + tests + coverage ratchet) still runs each time.
+ If a pre-push hook is blocked by an unrelated environment issue (e.g. a running app holding a
+ port), use the documented CI-equivalent workaround (see the gateway-dead-port note) — never
+ `--no-verify`.
+- **Small commits are the record** (merge, never squash — see the workspace multi-agent model), so
+ each step stays reviewable and revertible. A giant end-of-session commit is a defect.
+
+## Code hygiene (adopted)
+
+The wednesday-solutions gold-standard hygiene is **adopted** (landed on the quality-hardening
+release branch, not a separate PR). Two parts:
+
+1. **Prettier — applied repo-wide.** Config: `.prettierrc.yaml` (`singleQuote`, `semi: false`,
+ `printWidth: 100`, `trailingComma: none`) — the repo settled on 100/semi-false rather than the
+ 120 originally sketched. The whole tree is formatted to it; `eslintConfigPrettier` runs
+ `prettier/prettier` as a lint rule so drift is caught. `.prettierignore` (core + `pro/`) excludes
+ vendored/ephemeral trees (`.claude` worktrees, `resources/artifacts` + `**/*.min.*`, tsconfig
+ jsonc, local scratch). Re-run with `npm run format`.
+2. **ESLint — gold-standard rules as a warn RATCHET** (`eslint.config.mjs`, `goldStandardRatchet`):
+ `curly: all`, `no-console: [allow error,warn]`, `no-else-return`, `no-empty`, `prefer-template`,
+ `max-params: 3`, `complexity: 15`, `max-lines-per-function: 250`, `max-lines: 350`,
+ `@typescript-eslint/no-shadow` — all at `warn` because the god-files (MemoryChat ~2.6k, ipc.ts
+ ~1.7k) exceed the structural caps. **Tighten toward `error` (and `complexity` toward the gold 5)
+ as those files decompose — never loosen to pass.** This is the same ratchet discipline as the
+ coverage floor. Source: github.com/wednesday-solutions/react-template.
## Testing — test every approved behavior change in the same pass
@@ -80,7 +109,7 @@ When iterating (a request, a fix, a tweak the user just confirmed), add a test t
E2E and screenshot/video capture (including the Provit capture harness) run the app on a **fresh temp `OFFGRID_USER_DATA` profile** and must use **synthetic demo data only — never a real profile, never upload real user data.**
-- **Seed with the demo script — BOTH seeders.** A blank profile is EMPTY, so any flow that *generates* (chat, especially the "All memory" scope) will error with **"Sorry, something went wrong…"** — a **profile/RAG gap, not a bug**: no seeded memory store means the memory path throws before streaming. There are TWO independent seeders and a flow may need both: **`OFFGRID_SEED=force`** → core `seedDemo` (`src/main/index.ts` → `dev-seed.ts`) seeds chats / knowledge / RAG memory (this is what "All memory" chat queries); **`OFFGRID_SEED_PRO=force`** → pro `seedProDemo` (`pro/main/index.ts` → `pro/main/dev-seed.ts`) seeds observations / entities / clipboard / replay frames. `npm run demo` sets both — use it (or set both env vars) for any capture that exercises chat/generation.
+- **Seed with the demo script — BOTH seeders.** A blank profile is EMPTY, so any flow that _generates_ (chat, especially the "All memory" scope) will error with **"Sorry, something went wrong…"** — a **profile/RAG gap, not a bug**: no seeded memory store means the memory path throws before streaming. There are TWO independent seeders and a flow may need both: **`OFFGRID_SEED=force`** → core `seedDemo` (`src/main/index.ts` → `dev-seed.ts`) seeds chats / knowledge / RAG memory (this is what "All memory" chat queries); **`OFFGRID_SEED_PRO=force`** → pro `seedProDemo` (`pro/main/index.ts` → `pro/main/dev-seed.ts`) seeds observations / entities / clipboard / replay frames. `npm run demo` sets both — use it (or set both env vars) for any capture that exercises chat/generation.
- **Model ports are single-owner.** Only one app instance can bind the model engine ports (`:7878` gateway, `:8439` llama-server, `:7879`). A running `npm run dev` will block a second capture instance's engine → generation errors that look like a bug but aren't. Free the ports (stop the dev app) before an e2e capture, or the recording exercises the error path only.
- **Never upload private data.** Screenshots/video from real-profile runs (real chats, memories) must not be published. Capture runs must be synthetic-seeded so anything that lands in a PR or the public showcase is demo data.
@@ -123,8 +152,9 @@ The `pro/` directory is a **git submodule** pointing at the private `desktop-pro
Design to abstractions, not concrete types. When implementations are interchangeable (model backends, TTS/STT engines, image/diffusion runtimes, connectors), the rest of the app depends on one service/interface — never branch on a concrete type in UI/stores (`if (engine === 'kokoro')`, `instanceof X`). Push the decision behind the abstraction; adding an implementation should need zero changes to callers. Normalize capability gaps inside the service, not the UI.
**Before every code edit, stop and ask three questions — out loud, in the response:**
+
1. **Is there enough here to abstract?** Two or more concrete cases handled by the same caller (text vs vision vs image models, Slack vs Mail surfaces, kokoro vs piper TTS) means there's a seam. One case, used once, is not — don't abstract speculatively (YAGNI).
-2. **Can we apply SOLID here?** Mainly: does one thing own one responsibility (SRP), and do callers depend on an interface rather than the concretes (DSP)? A `kind === 'x'` / `instanceof` / per-type `switch` in a caller — *especially in the renderer* — is the tell that the decision belongs behind a service.
+2. **Can we apply SOLID here?** Mainly: does one thing own one responsibility (SRP), and do callers depend on an interface rather than the concretes (DSP)? A `kind === 'x'` / `instanceof` / per-type `switch` in a caller — _especially in the renderer_ — is the tell that the decision belongs behind a service.
3. **Are we actually using it?** A mapping or rule must be defined ONCE and reused. If the same kind→modality map, the same routing `if`, or the same capability check appears in two layers (e.g. main process AND renderer), that's duplication, not abstraction — collapse it to a single source of truth and have both sides call it.
If the answer to 1 is "no", say so and write the simple version. If "yes", build the seam before piling on the second concrete branch — retrofitting after drift is the expensive path.
diff --git a/README.md b/README.md
index b8ad081e..11b6774e 100644
--- a/README.md
+++ b/README.md
@@ -28,6 +28,17 @@
+
+
+
+
+
+
+
+
+
Off Grid AI Mobile — the same on-device AI, on your phone ·
@@ -51,8 +62,8 @@ Three things in one app:
Claude/LM-Studio/Ollama with everything on-device.
2. **A gateway** — one local OpenAI-compatible API (`http://127.0.0.1:7878/v1`, no key)
for chat, vision, image, audio, and embeddings. Run it headless as just the gateway.
-3. **Off Grid Pro** — an always-on private layer that *sees* your work (screen → OCR),
- *remembers* it, helps you *reflect*, and *acts* with your approval. On-device, opt-in.
+3. **Off Grid Pro** — an always-on private layer that _sees_ your work (screen → OCR),
+ _remembers_ it, helps you _reflect_, and _acts_ with your approval. On-device, opt-in.
## A look inside
@@ -107,14 +118,14 @@ A full breakdown is in [docs/FEATURES.md](docs/FEATURES.md).
One local server (`http://127.0.0.1:7878`) speaks the OpenAI API:
-| Capability | Endpoint |
-|---|---|
-| Chat (text + vision) | `POST /v1/chat/completions` |
-| Text → Image | `POST /v1/images` (`/generations`, `/edits`) |
-| Speech → Text | `POST /v1/audio/transcriptions` |
-| Text → Speech | `POST /v1/audio/speech` |
-| Embeddings | `POST /v1/embeddings` |
-| Models | `GET /v1/models` |
+| Capability | Endpoint |
+| -------------------- | -------------------------------------------- |
+| Chat (text + vision) | `POST /v1/chat/completions` |
+| Text → Image | `POST /v1/images` (`/generations`, `/edits`) |
+| Speech → Text | `POST /v1/audio/transcriptions` |
+| Text → Speech | `POST /v1/audio/speech` |
+| Embeddings | `POST /v1/embeddings` |
+| Models | `GET /v1/models` |
```bash
curl http://127.0.0.1:7878/v1/chat/completions \
@@ -146,14 +157,14 @@ OFFGRID_SERVER_ONLY=1 npm run gateway
It's **self-sufficient** — manage models over HTTP, no UI required:
-| Action | Endpoint |
-|---|---|
-| List the catalog | `GET /v1/models/catalog` |
-| List installed | `GET /v1/models/installed` |
-| Active model per modality | `GET /v1/models/active` |
-| **Pull** a model | `POST /v1/models/pull` `{ "id": "…" }` → poll `GET /v1/models/pull/status?id=…` |
-| **Activate** a model | `POST /v1/models/activate` `{ "id": "…", "kind"?: "image\|speech\|transcription" }` |
-| **Delete** a model | `POST /v1/models/delete` `{ "id": "…" }` |
+| Action | Endpoint |
+| ------------------------- | ----------------------------------------------------------------------------------- |
+| List the catalog | `GET /v1/models/catalog` |
+| List installed | `GET /v1/models/installed` |
+| Active model per modality | `GET /v1/models/active` |
+| **Pull** a model | `POST /v1/models/pull` `{ "id": "…" }` → poll `GET /v1/models/pull/status?id=…` |
+| **Activate** a model | `POST /v1/models/activate` `{ "id": "…", "kind"?: "image\|speech\|transcription" }` |
+| **Delete** a model | `POST /v1/models/delete` `{ "id": "…" }` |
```bash
# pull a model into a headless gateway, then chat
diff --git a/ROADMAP_DESKTOP.md b/ROADMAP_DESKTOP.md
index 3f5ff739..b8069d17 100644
--- a/ROADMAP_DESKTOP.md
+++ b/ROADMAP_DESKTOP.md
@@ -2,19 +2,21 @@
The desktop product view of the plan. The shared, package-oriented plan lives in `../shared/ROADMAP.md`; this is the same arc re-cut around the **desktop app**, with honest current status. Sources folded in: `shared/ROADMAP.md`, root `CLAUDE.md`, `website/vision.md`.
-**North star:** a private, **local-first** layer for knowledge workers that **sees** your work, **remembers** it, helps you **reflect**, and **acts** on your behalf *with approval* — data stays on the device, intelligence runs on-device. Mission: *democratize intelligence for knowledge workers* (the broader vision: a proactive, private Personal AI OS for everyone — `website/vision.md`).
+**North star:** a private, **local-first** layer for knowledge workers that **sees** your work, **remembers** it, helps you **reflect**, and **acts** on your behalf _with approval_ — data stays on the device, intelligence runs on-device. Mission: _democratize intelligence for knowledge workers_ (the broader vision: a proactive, private Personal AI OS for everyone — `website/vision.md`).
Legend: ✅ done · 🟡 partial / in progress · ⬜ not started
---
## Phase 0 — Foundation ✅
+
- ✅ App shell, Off Grid design (Menlo / black / emerald), unified `userData` path, dock + tray icons
- ✅ Bundled local runtimes: `llama-server` (gemma-4 vision), `whisper.cpp`, `ffmpeg`, `sharp`
- ✅ Local LLM plumbing: grammar-constrained JSON, `enable_thinking:false`, single-flight init, port 8439
- 🟡 Full design pass on every screen (ongoing polish)
## Phase 1 — Capture spine ✅
+
- ✅ Focus loop via `get-windows` (no fragile per-helper TCC)
- ✅ Active-screen capture, **multi-monitor correct** (display under the focused window)
- ✅ Window-bounds crop → OCR → gemma distill → observations + entities
@@ -23,6 +25,7 @@ Legend: ✅ done · 🟡 partial / in progress · ⬜ not started
- ✅ Menu-bar tray (pause/recalibrate)
## Phase 2 — See & Remember surfaces 🟡
+
- ✅ **Day** — persisted journal (keep-old-while-updating) + time blocks
- ✅ **Entities** — CRM-for-everything (merge/hide/reassign/hierarchy, synthesis summaries)
- ✅ **Replay** — "movie of your day" (scrub/play/speed, busiest-day default, blanks filtered)
@@ -32,55 +35,66 @@ Legend: ✅ done · 🟡 partial / in progress · ⬜ not started
- ⬜ Replay polish (filmstrip, jump-to-entity, collapse gaps)
## Phase 3 — Meeting recorder 🟡 (building now)
+
- 🟡 **Meeting recorder** — **Google Meet + Zoom** for now (user has access to those two). Records **screen video + system/speaker audio (Electron loopback) + mic**, mixed into one webm; transcribes locally with bundled whisper; LLM title/summary/people; folds a summary into memory (surface=Meeting) so it hits Day/Reflect. Explicit start/stop + recording indicator (consent). FIRST CUT: `main/meetings.ts` + `MeetingsScreen.tsx` + `setDisplayMediaRequestHandler` loopback. Next: auto-arm on detecting a Meet/Zoom window, diarization, align transcript to the frame timeline, Teams later.
- ⬜ Encryption at rest for the memory DB
## Parked: remaining connectors (later)
+
3 connectors verified (Notion, Linear, Jira/Confluence). Parked for now, revisit after meetings:
+
- **Per-connector arg/prereq generalization** — generalize the cloudId resolver to any required id (teamId/workspaceId/orgId) so Vercel/Asana-style `list_*` tools work (Vercel `list_projects` needs `teamId`; partial code in `ingest.ts`).
- **Verify/finish**: Sentry, Vercel, Attio, ClickUp, Trello, GitHub (PAT), GitLab — onboard one-at-a-time with the disabled-by-default rule.
- **Slack** — skipped (bot must be invited per-channel; capture covers it). Adapter built but parked.
- **Google (Gmail/Cal/Drive)** — needs a GCP OAuth client (Testing mode); parked until the client exists. Capture covers it meanwhile.
## Phase 4 — The "Act" pillar ⬜ (foundation-first) ← NEXT
+
**Foundation (build first — everything authorized hangs off these):**
+
- ✅ **Identity anchor** — who the user is (name + email); Settings → "You"; `isMe()` for ownership (`main/identity.ts`)
- ✅ **Secure secret storage** — Electron `safeStorage`/Keychain; encrypted at rest, values never reach the renderer (`main/secrets.ts`)
- 🟡 **Consent / permission model** — per-connector enable/disable + the local-processing guarantee (connectors fetch, local model reasons). Full per-source consent UI + revoke still to deepen
- ✅ **Approval queue + audit log** — proposed → approve/reject → execute → logged; nothing acts without a logged approval (`crm/approvals.ts`, Approvals screen)
**Sources & connectors (MCP):**
+
- ✅ **Integrations / Connectors page** — add/enable/test MCP connectors (stdio + HTTP), discover tools, status (`main/mcp.ts` via `@modelcontextprotocol/sdk`, `ConnectorsScreen.tsx`). Approved tool calls execute through `callConnectorTool`
- ⬜ **Connector auth rule** — MCP connectors only for clean local-friendly auth: **DCR-OAuth** (Notion✓, Linear, Atlassian, Sentry…) or **token** (Slack, Airtable, Postgres…). **No central OAuth client.**
- ⬜ **Google (Gmail/Calendar) via SCREEN CAPTURE, not an OAuth client** (decided June 2026, fully-offline). Google MCP has no DCR → would need a registered GCP client = anti-offline; rejected. We already OCR Gmail/Calendar on screen — zero setup, nothing leaves the device.
-- ⬜ **Capture ↔ connector intelligence (source-of-truth priority).** Be smart: capture is the universal SIGNAL of interest ("you're on a Notion page / a Linear issue / company XYZ"); when a connector exists for what you're looking at, **pull the authoritative data from the connector (source of truth) instead of leaning on OCR/AX.** Connector data **takes priority** over captured/OCR data in synthesis + dedup, and lets us **throttle/skip OCR** for those apps (cheaper, cleaner). Capture tells us *what you care about*; the connector gives the *correct* version.
+- ⬜ **Capture ↔ connector intelligence (source-of-truth priority).** Be smart: capture is the universal SIGNAL of interest ("you're on a Notion page / a Linear issue / company XYZ"); when a connector exists for what you're looking at, **pull the authoritative data from the connector (source of truth) instead of leaning on OCR/AX.** Connector data **takes priority** over captured/OCR data in synthesis + dedup, and lets us **throttle/skip OCR** for those apps (cheaper, cleaner). Capture tells us _what you care about_; the connector gives the _correct_ version.
- ⬜ **Cross-source synthesis** — join email ↔ calendar event ↔ person ↔ project ↔ ticket into one entity, across capture + connectors
-- ⬜ **File system access (local file catalogue + auto-retrieval).** Index opt-in, **scoped folders** on-device — file metadata (path / name / type / size / mtime) + content embeddings into the RAG store (Phase 5) — so Off Grid knows *what files exist* and *what's in them*. Then **fetch the right file at the right time instead of making the user attach it**: capture/context signals *what you're working on* → the catalogue surfaces or auto-attaches the relevant document (meeting prep pulls the deck; a chat about project X pulls its docs; "the contract we discussed" resolves to the file). Local-only, per-folder enable/revoke under the consent model, incremental re-index via an fs watcher. The local-first peer of the source-of-truth-priority rule above: **capture tells us what you care about; the file system gives the actual document — no manual attach.**
+- ⬜ **File system access (local file catalogue + auto-retrieval).** Index opt-in, **scoped folders** on-device — file metadata (path / name / type / size / mtime) + content embeddings into the RAG store (Phase 5) — so Off Grid knows _what files exist_ and _what's in them_. Then **fetch the right file at the right time instead of making the user attach it**: capture/context signals _what you're working on_ → the catalogue surfaces or auto-attaches the relevant document (meeting prep pulls the deck; a chat about project X pulls its docs; "the contract we discussed" resolves to the file). Local-only, per-folder enable/revoke under the consent model, incremental re-index via an fs watcher. The local-first peer of the source-of-truth-priority rule above: **capture tells us what you care about; the file system gives the actual document — no manual attach.**
**Skills & action:**
+
- ⬜ **Skills framework** (`@offgrid/skills`) — trigger → action, on-device, model-driven
-- 🟡 **Intent → tool bridge** — action-item *detection* shipped (`crm/actions.ts`); next: propose a connector tool call (args from context) → approval queue
-- ⬜ **Day flips from retrospective → prospective** (KEY shift, user-confirmed June 2026). Today "Day" = what happened / what you did (a rear-view mirror). Once Calendar + tasks + email flow, Day gains an **"Ahead"** half = *what your day SHOULD look like*:
+- 🟡 **Intent → tool bridge** — action-item _detection_ shipped (`crm/actions.ts`); next: propose a connector tool call (args from context) → approval queue
+- ⬜ **Day flips from retrospective → prospective** (KEY shift, user-confirmed June 2026). Today "Day" = what happened / what you did (a rear-view mirror). Once Calendar + tasks + email flow, Day gains an **"Ahead"** half = _what your day SHOULD look like_:
- meetings as the skeleton (Calendar) → **prep per meeting** (past conversations + open items + docs about the attendees, from memory/connectors)
- **priorities** — action items + connector to-dos slotted into the day
- **protected deep-work blocks** + **overcommitment nudges**, informed by the Reflect/attention signal
- The "Behind" half (current Day) stays as the feedback loop that makes "Ahead" smarter.
- This is the tool→assistant pivot and the vision.md promise ("your devices already know your day; the briefing is ready, you didn't ask for it").
+ This is the tool→assistant pivot and the vision.md promise ("your devices already know your day; the briefing is ready, you didn't ask for it").
- ⬜ **Proactive delivery** — morning briefing, meeting prep ~20 min before, right person/right time (notifications), tuned by Reflect. Needs the connectors flowing (Phase 4 sources).
## Phase 5 — Intelligence depth 🟡
+
- ✅ **Projects + RAG + chat** over all memory + ingested docs — file upload (txt/md/PDF/DOCX/image/audio/video) → MiniLM embeddings + better-sqlite3 vector store; cited sources; "include captured memory" toggle spans uploads + everything captured (`@offgrid/rag`, `rag/index.ts`, `ProjectsScreen.tsx`)
- ⬜ Unified search
- ✅ **Models** — HF browser / provider abstraction / download manager (`@offgrid/models`, `ModelsScreen.tsx`)
- ⬜ **Expose a local model server** — surface the on-device runtimes (the bundled `llama-server` on 8439, whisper, embeddings) as a **local, OpenAI-compatible API endpoint** so other apps on the machine — and, over the mesh, other paired devices — can use Off Grid AI Desktop as their private inference backend. Off Grid Desktop becomes the household's on-device model server (no cloud, no API keys). Auth-gated + opt-in (off by default); ties into the consent model and the cross-device mesh (Phase 6).
### Phase 5b — The Off Grid chat as a local AI Studio (in progress, June 2026)
+
The Off Grid chat (`MemoryChat.tsx`) is becoming a full local-first studio — like Claude/LM Studio/Ollama, but everything on-device. Brand: brutalist/terminal (Menlo, emerald, flat). Done + planned:
+
- ✅ **Chat redesign** — brutalist composer, clickable example prompts, mode segmented control; light + dark.
- ✅ **On-device image generation** — `stable-diffusion.cpp` (`sd-cli`, Metal) in `resources/bin/sd`; txt2img + img2img; per-model size/steps/seed/negative-prompt; **live per-step preview + progress bar + ETA + Stop/cancel**; **lightbox** (zoom/download/delete); **artifacts gallery** of every generated image (`main/imagegen.ts`, `MemoryChat.tsx`).
- ✅ **Image models** — SDXL, **SDXL-Lightning** (few-step, default-fast), SD 1.5/2.1; per-model step/cfg/sampler auto-tuning; catalog curated toward latest models.
- ✅ **Crash/freeze fix (Apple Silicon)** — unified-memory overflow froze the machine when LLM + image model were both resident; now **free the LLM before image gen** + `--diffusion-fa` + memory guard + thread cap.
- ✅ **STT (voice input)** — mic → bundled whisper → text in the composer.
+- ⬜ **Screen-aware dictation (strong fit — we already ship the primitives)** — dictation that reads the ACTIVE window (our capture → OCR primitive) as context, so a spoken command acts on what's on screen: "reply to this email", "summarize this doc", "make this a styled message". We already have the three pieces — PTT dictation (whisper), screen capture → OCR → text, and the local LLM — so the work is just wiring the current-window OCR into the dictation → LLM prompt, fully on-device. Competitive signal (2026-07): HeyClicky (YC-backed) launched Mac screen-aware dictation — ~450ms latency, Fn+Ctrl hotkey, free tier + $20/mo Pro; Wispr Flow is similar. Our edge is the privacy question they can't answer: screen frames never leave the device (runs in the Mac's RAM). See mobile cross-app capture asset (sentinel `feat/cross-app-intelligence`) for the mobile analog.
- ✅ **TTS (voice output)** — **Kokoro-82M** (open-weight, multilingual) via `kokoro-js`; per-message Speak + auto-speak voice mode (`main/tts.ts`).
- ✅ **Add chat to a project** — header picker scopes a chat to a project's KB (+ inline project create); Projects tab lists a project's chats (Claude-style, no composer there) and opens them in the chat (`App.tsx` `chatTarget`).
- 🟡 **Z-Image-Turbo (2026 flagship image model)** — Alibaba Tongyi, Apache-2.0, ~8-step turbo, 1024px, bilingual text. 3-file stack (diffusion + Qwen3-4B `--llm` encoder + FLUX `--vae`), `--offload-to-cpu`; wired in `imagegen.ts`, set as default. (verified sd.cpp supports it; verifying first generation)
@@ -99,36 +113,42 @@ The Off Grid chat (`MemoryChat.tsx`) is becoming a full local-first studio — l
- 🟡 **Tool-calling loop + connectors in chat** — agentic loop done for **built-in local tools** (calculator, datetime), isolated + opt-in via the composer "+" menu; the model calls them mid-chat with results shown inline (`main/tools.ts`). Next: web search, read-URL, KB search, and **MCP connectors** in the picker.
- ✅ **Composer redesign** — Claude/ChatGPT-style: "+" menu (add image / generate / add-to-project / tools), Gemini-style **image style presets** (Sketch/Cinematic/Anime/…), centered welcome + example chips.
- ⬜ **Thinking controls** — toggle `enable_thinking` on the local reasoning model + collapsible reasoning blocks in messages.
-- ⬜ **Project cross-chat memory** — chats live *inside* a project; a chat can **reference information from other chats in the same project** (project-scoped retrieval across its conversations), while "All memory" remains available.
+- ⬜ **Project cross-chat memory** — chats live _inside_ a project; a chat can **reference information from other chats in the same project** (project-scoped retrieval across its conversations), while "All memory" remains available.
- ✅ **Model-settings control in chat** — **temperature** (per-request) + **context window** (re-spawns `llama-server` with new `--ctx-size`), persisted, surfaced in a chat-header settings popover (`llm.ts` getSettings/setSettings). Next: top_p / max-tokens.
## Phase 6 — Cross-device (Personal AI OS) 🟡
+
- ✅ Engine exists: `@offgrid/sync` + `@offgrid/memory` (pairing, anti-entropy op-log)
- ⬜ Carry the new memory (observations/actions/entities/reflect) across the mesh
- ⬜ Embed sync + memory + clipboard into Desktop; desktop↔desktop converge; universal clipboard
- ⬜ **One brain across devices** — laptop (work) + phone (life) unify into a single working model, syncing over the home network, no cloud relay (`vision.md`)
## Phase 7 — Org / B2B distribution ⬜
-- ⬜ Team/org identity + roles; **scoped-sharing model** (share *intelligence*, never raw frames); right-person-right-time distribution across a team. The layer neither screenpipe nor Littlebird has
+
+- ⬜ Team/org identity + roles; **scoped-sharing model** (share _intelligence_, never raw frames); right-person-right-time distribution across a team. The layer neither screenpipe nor Littlebird has
## Phase 8 — Productization ⬜
-- ⬜ Onboarding permission ladder (screen → Google OAuth → MCP) — *deferred, not now*
+
+- ⬜ Onboarding permission ladder (screen → Google OAuth → MCP) — _deferred, not now_
- ⬜ Settings consolidation; ✅ theme toggle wiring
- ⬜ Packaging: signed/notarized DMG + auto-update
-- ⬜ Licensing: AGPL + CLA + open-core; device cap (2 free / 3+ paid) — *deferred, not now*
+- ⬜ Licensing: AGPL + CLA + open-core; device cap (2 free / 3+ paid) — _deferred, not now_
---
## Parked (explicitly deferred — not now, per product call June 2026)
+
- **Storage & retention** (cleanup/budget/auto-prune). Revisit when running all-day for weeks becomes the norm.
- **Continuous ScreenCaptureKit → H.264 video** (smooth DVR vs current PNG frames). Current per-tick screenshots are good enough for now.
-- **Replay: scene grouping below the scrubber.** Group the timeline's frames into visible scenes/sessions (the `session.ts` windowing we built for entity carry-over) shown as labelled bands under the time slider, so it's not one flat strip of frames — helps people understand what a stretch of time *was*. (Parked 2026-06-29.)
+- **Replay: scene grouping below the scrubber.** Group the timeline's frames into visible scenes/sessions (the `session.ts` windowing we built for entity carry-over) shown as labelled bands under the time slider, so it's not one flat strip of frames — helps people understand what a stretch of time _was_. (Parked 2026-06-29.)
- **Entity → scene replay.** From an entity record, pull up the full scene(s) involving that entity and play them back — "show me everything around Nowshad" reconstructs and replays the relevant captured scene. Builds on scene detection + entity links. (Parked 2026-06-29.)
## Engineering track (parallel)
+
Desktop implements capture/reflect/act **inline** today. For mobile reuse, extract into `@offgrid/*` packages (`capture`, `memory`, `skills`, `models`, `imagegen`, `ui`) once proven in-app. **Mobile is built last.**
## Status (June 2026)
+
Phases 0–2 largely done — the full **see → remember → reflect → act(detect)** loop runs on one machine. Frontier: **Phase 4 (authorized sources + approved actions)**, starting with the **foundation** (identity → secrets → consent → approval queue), then **Gmail/Calendar via MCP**.
## Later phase — UI standardization audit (design philosophy)
diff --git a/docs/API.md b/docs/API.md
index 976fa30f..96b576ba 100644
--- a/docs/API.md
+++ b/docs/API.md
@@ -45,18 +45,18 @@ speech are always ready (models download on first use). Image generation/edit re
## Capabilities at a glance
-| Modality | Endpoint | Method | Backend |
-|---|---|---|---|
-| Text → Text | `/v1/chat/completions` | POST | llama-server (bundled VLM) |
-| Image → Text (vision) | `/v1/chat/completions` | POST | llama-server (VLM + mmproj) |
-| Text completion (legacy) | `/v1/completions` | POST | llama-server |
-| Embeddings | `/v1/embeddings` | POST | llama-server |
-| List models | `/v1/models` | GET | llama-server |
-| Speech → Text (STT) | `/v1/audio/transcriptions` | POST | whisper.cpp |
-| Text → Speech (TTS) | `/v1/audio/speech` | POST | Kokoro-82M (ONNX) |
-| List TTS voices | `/v1/audio/voices` | GET | Kokoro-82M |
-| Text → Image | `/v1/images` (or `/v1/images/generations`) | POST | stable-diffusion.cpp / Core ML |
-| Image → Image | `/v1/images` (or `/v1/images/edits`) | POST | stable-diffusion.cpp |
+| Modality | Endpoint | Method | Backend |
+| ------------------------ | ------------------------------------------ | ------ | ------------------------------ |
+| Text → Text | `/v1/chat/completions` | POST | llama-server (bundled VLM) |
+| Image → Text (vision) | `/v1/chat/completions` | POST | llama-server (VLM + mmproj) |
+| Text completion (legacy) | `/v1/completions` | POST | llama-server |
+| Embeddings | `/v1/embeddings` | POST | llama-server |
+| List models | `/v1/models` | GET | llama-server |
+| Speech → Text (STT) | `/v1/audio/transcriptions` | POST | whisper.cpp |
+| Text → Speech (TTS) | `/v1/audio/speech` | POST | Kokoro-82M (ONNX) |
+| List TTS voices | `/v1/audio/voices` | GET | Kokoro-82M |
+| Text → Image | `/v1/images` (or `/v1/images/generations`) | POST | stable-diffusion.cpp / Core ML |
+| Image → Image | `/v1/images` (or `/v1/images/edits`) | POST | stable-diffusion.cpp |
This surface follows the [OpenRouter multimodal](https://openrouter.ai/docs/guides/overview/multimodal/overview)
conventions: images go in as `image_url` content parts (data URLs **or** `http(s)://` /
@@ -92,7 +92,14 @@ grammar-constrained `response_format` and disable thinking:
```json
{
"messages": [{ "role": "user", "content": "..." }],
- "response_format": { "type": "json_schema", "json_schema": { "schema": { /* ... */ } } },
+ "response_format": {
+ "type": "json_schema",
+ "json_schema": {
+ "schema": {
+ /* ... */
+ }
+ }
+ },
"chat_template_kwargs": { "enable_thinking": false }
}
```
@@ -108,12 +115,12 @@ print(r.choices[0].message.content)
```
```javascript
-import OpenAI from "openai";
-const client = new OpenAI({ baseURL: "http://127.0.0.1:7878/v1", apiKey: "not-needed" });
+import OpenAI from 'openai'
+const client = new OpenAI({ baseURL: 'http://127.0.0.1:7878/v1', apiKey: 'not-needed' })
const r = await client.chat.completions.create({
- model: "local",
- messages: [{ role: "user", content: "hello" }],
-});
+ model: 'local',
+ messages: [{ role: 'user', content: 'hello' }]
+})
```
---
@@ -196,11 +203,11 @@ converted to 16 kHz mono via ffmpeg and transcribed with auto language detection
**Form fields**
-| Field | Required | Notes |
-|---|---|---|
-| `file` | yes | Audio file (wav, mp3, m4a, …). Max 200 MB. |
-| `response_format` | no | `json` (default) or `text`. |
-| `model` | no | Accepted for compatibility; the installed whisper model is used. |
+| Field | Required | Notes |
+| ----------------- | -------- | ---------------------------------------------------------------- |
+| `file` | yes | Audio file (wav, mp3, m4a, …). Max 200 MB. |
+| `response_format` | no | `json` (default) or `text`. |
+| `model` | no | Accepted for compatibility; the installed whisper model is used. |
```bash
curl http://127.0.0.1:7878/v1/audio/transcriptions \
@@ -223,12 +230,12 @@ onnxruntime. **Returns raw `audio/wav` bytes** by default (like OpenAI).
**JSON body**
-| Field | Required | Notes |
-|---|---|---|
-| `input` | yes | Text to speak. Capped at ~2000 chars per call. (`text` also accepted.) |
-| `voice` | no | Voice id, default `af_heart`. See `/v1/audio/voices`. |
-| `response_format` | no | Omit/`wav` → raw WAV bytes. `json`/`b64_json` → `{ "audio": "data:audio/wav;base64,…", "format": "wav" }`. |
-| `model` | no | Accepted for compatibility. |
+| Field | Required | Notes |
+| ----------------- | -------- | ---------------------------------------------------------------------------------------------------------- |
+| `input` | yes | Text to speak. Capped at ~2000 chars per call. (`text` also accepted.) |
+| `voice` | no | Voice id, default `af_heart`. See `/v1/audio/voices`. |
+| `response_format` | no | Omit/`wav` → raw WAV bytes. `json`/`b64_json` → `{ "audio": "data:audio/wav;base64,…", "format": "wav" }`. |
+| `model` | no | Accepted for compatibility. |
```bash
# Raw WAV to a file
@@ -250,7 +257,7 @@ curl http://127.0.0.1:7878/v1/audio/voices
---
-## 6. Text → Image & Image → Image (`/v1/images`)
+## 6. Text → Image & Image → Image (`/v1/images`)
`POST /v1/images` — one JSON endpoint for both text-to-image and image-to-image, matching
OpenRouter's image API. On-device diffusion via stable-diffusion.cpp (GGUF / safetensors)
@@ -258,21 +265,21 @@ or the Core ML ANE helper. Returns base64 PNG.
**JSON body**
-| Field | Required | Notes |
-|---|---|---|
-| `prompt` | yes | Text prompt. |
-| `input_references` | no | Array — **presence makes it image-to-image.** Each item is `{ "type": "image_url", "image_url": { "url": "…" } }` (or a bare URL string). The `url` may be a data URL, `http(s)://`, or `file://`. The first reference is used as the init image. |
-| `strength` | no | img2img only. 0–1, how far from the init image (default ~0.75). Lower = closer to original. |
-| `aspect_ratio` | no | e.g. `"16:9"`, `"1:1"`. Combined with `resolution`. |
-| `resolution` | no | `"1K"` (default), `"2K"`, or `"512"` — sets the long edge. |
-| `size` | no | OpenAI-style `"WIDTHxHEIGHT"`, e.g. `"1024x1024"`. |
-| `width` / `height` | no | Explicit dimensions (numbers); override the above. |
-| `negative_prompt` | no | Things to avoid. A sensible default is applied if omitted. |
-| `steps` | no | Sampling steps. Per-model default (few-step turbo/lightning models use ~4–8). |
-| `seed` | no | Integer seed; omit or `-1` for random. The resolved seed is returned. |
-| `cfg_scale` | no | Guidance scale. Per-model default. |
-| `model` | no | Model filename in the models dir; otherwise the preferred installed model. |
-| `response_format` | no | `b64_json` (default) → base64 PNG. `url` → `file://` path on disk. |
+| Field | Required | Notes |
+| ------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `prompt` | yes | Text prompt. |
+| `input_references` | no | Array — **presence makes it image-to-image.** Each item is `{ "type": "image_url", "image_url": { "url": "…" } }` (or a bare URL string). The `url` may be a data URL, `http(s)://`, or `file://`. The first reference is used as the init image. |
+| `strength` | no | img2img only. 0–1, how far from the init image (default ~0.75). Lower = closer to original. |
+| `aspect_ratio` | no | e.g. `"16:9"`, `"1:1"`. Combined with `resolution`. |
+| `resolution` | no | `"1K"` (default), `"2K"`, or `"512"` — sets the long edge. |
+| `size` | no | OpenAI-style `"WIDTHxHEIGHT"`, e.g. `"1024x1024"`. |
+| `width` / `height` | no | Explicit dimensions (numbers); override the above. |
+| `negative_prompt` | no | Things to avoid. A sensible default is applied if omitted. |
+| `steps` | no | Sampling steps. Per-model default (few-step turbo/lightning models use ~4–8). |
+| `seed` | no | Integer seed; omit or `-1` for random. The resolved seed is returned. |
+| `cfg_scale` | no | Guidance scale. Per-model default. |
+| `model` | no | Model filename in the models dir; otherwise the preferred installed model. |
+| `response_format` | no | `b64_json` (default) → base64 PNG. `url` → `file://` path on disk. |
**Text-to-image:**
@@ -301,9 +308,7 @@ curl http://127.0.0.1:7878/v1/images \
```json
{
"created": 1750000000,
- "data": [
- { "b64_json": "iVBORw0KGgo...", "seed": 42, "model": "z-image-turbo.gguf" }
- ],
+ "data": [{ "b64_json": "iVBORw0KGgo...", "seed": 42, "model": "z-image-turbo.gguf" }],
"usage": { "prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0 }
}
```
@@ -344,13 +349,13 @@ Errors use the OpenAI shape:
{ "error": { "message": "…", "type": "invalid_request_error" } }
```
-| Status | Meaning |
-|---|---|
-| `400` | Bad request (missing field, wrong content type, invalid JSON). |
-| `413` | Upload exceeds the 200 MB cap. |
-| `500` | The model run failed (see `message`). |
-| `501` | Modality not installed (e.g. no diffusion model, transcription runtime missing). |
-| `502` | llama-server not ready (chat/embeddings upstream unavailable). |
+| Status | Meaning |
+| ------ | -------------------------------------------------------------------------------- |
+| `400` | Bad request (missing field, wrong content type, invalid JSON). |
+| `413` | Upload exceeds the 200 MB cap. |
+| `500` | The model run failed (see `message`). |
+| `501` | Modality not installed (e.g. no diffusion model, transcription runtime missing). |
+| `502` | llama-server not ready (chat/embeddings upstream unavailable). |
---
@@ -392,7 +397,7 @@ curl http://127.0.0.1:7878/v1/requests/cee1c78f-…
{
"request_id": "cee1c78f-…",
"kind": "image",
- "status": "completed", // queued | running | completed | failed
+ "status": "completed", // queued | running | completed | failed
"created_at": 1782191813736,
"updated_at": 1782191840000,
"result": { "created": 1782191840, "data": [{ "b64_json": "…" }] }
@@ -423,12 +428,12 @@ memory carefully (Apple Silicon shares RAM between CPU/GPU/ANE):
## Backends & ports
-| Service | Where | Used by |
-|---|---|---|
-| `llama-server` | `127.0.0.1:8439` (long-running) | chat, vision, completions, embeddings, models |
-| `whisper.cpp` (`whisper-cli`) | one-shot subprocess | transcription |
-| Kokoro-82M via `kokoro-js` | in-process (onnxruntime) | speech, voices |
-| `sd-cli` / `coreml-sd` | one-shot subprocess | image generation & edits |
+| Service | Where | Used by |
+| ----------------------------- | ------------------------------- | --------------------------------------------- |
+| `llama-server` | `127.0.0.1:8439` (long-running) | chat, vision, completions, embeddings, models |
+| `whisper.cpp` (`whisper-cli`) | one-shot subprocess | transcription |
+| Kokoro-82M via `kokoro-js` | in-process (onnxruntime) | speech, voices |
+| `sd-cli` / `coreml-sd` | one-shot subprocess | image generation & edits |
The gateway (`src/main/model-server.ts`) is the only port you call (`7878`); it proxies
or invokes the right backend per route. Models live in the app's `userData/models`
diff --git a/docs/CHAT_UX_SPEC.md b/docs/CHAT_UX_SPEC.md
index 38b34771..bd0c0b2e 100644
--- a/docs/CHAT_UX_SPEC.md
+++ b/docs/CHAT_UX_SPEC.md
@@ -24,7 +24,7 @@ appears at the right time, and nothing feels like a tool you have to operate.)
2. **The model picks the modality at the right point.** It emits a typed block and the
UI renders it seamlessly inline:
- `\`\`\`image` → on-device generation, rendered as it completes
- - `\`\`\`html` / `\`\`\`svg` / `\`\`\`mermaid` → artifact in the side canvas
+ - `\`\`\`html`/`\`\`\`svg`/`\`\`\`mermaid` → artifact in the side canvas
- `\`\`\`ask` (JSON) → inline clickable multiple-choice
- otherwise → streamed text
3. **Everything streams.** Text types in; images show a tasteful generating state then
diff --git a/docs/CODE_AUDIT.md b/docs/CODE_AUDIT.md
new file mode 100644
index 00000000..4ba23eee
--- /dev/null
+++ b/docs/CODE_AUDIT.md
@@ -0,0 +1,64 @@
+# Code-quality backlog — what's NOT done yet
+
+Remaining SOLID/DRY/cleanup work, after the `chore/quality-hardening` branch landed the
+4 real bugs, the safe DRY consolidations, the coverage campaign, and the quality-tooling
+spine. Everything below is deliberately deferred: each either rewrites an engine contract
+on a coverage-excluded I/O path (can't be verified live headless) or needs visual/on-device
+verification. Land each as its own reviewed change with real verification — not a blind sweep
+(the merge gate forbids shipping an unverified "done").
+
+## Structural (prevents a whole bug class)
+
+- **Renderer has no store layer** (`src/renderer/src/stores/` doesn't exist). Every screen
+ re-fetches + holds its own `useState` copy — the root of the "local copy drifts" bug class
+ (image composer, ProjectsScreen doc-toggle, …). A thin per-domain store (owns the fetch +
+ write-through) prevents it structurally instead of fixing it screen-by-screen.
+
+## Core — DIP / SRP (engine contracts; need live verification)
+
+- **`ImageRuntime` interface** — `imagegen.runImageGen` chooses among 4 interchangeable runtimes
+ (mflux/coreml/sd-server/sd-cli) via a predicate cascade in one ~350-line fn; a 5th needs edits
+ in ≥3 places. Fold `models-manager` `runtime==='mflux'` install/delete/isCached into the same
+ abstraction. Needs live image-gen to verify.
+- **`imagegen.ts` SRP split** — listing/delete/LoRA/GGUF-sniff/resolve-policy/orchestration in one
+ file. Split model-resolve / gguf-inspect / loras.
+- **`ipc.ts rag:chat` god-handler split** — `retrieveContext()` + `ftsBlock()` (5 near-identical
+ FTS blocks remain; the app-name filter is already unified via `appNameLikeClause`).
+- **`database.ts` split (1339 lines)** — key mgmt + cosine + DDL/migrations + 6 domains' queries →
+ connection/schema/per-domain repos + `vector-math.ts`. Native-DB path; behavior-risk.
+
+## Core — correctness (needs live image-gen)
+
+- **Z-Image guard-vs-run regex** — the memory guard sizes a _different_ match than the run loads,
+ so residency estimates can be wrong for the Z-Image stack.
+
+## Core — brand (needs screenshot verification)
+
+- **`@tabler/icons-react` → Phosphor** in ModelsScreen / StoragePanel / ConnectorsScreen
+ (CLAUDE.md mandates Phosphor-only). Many-icon swap; each glyph + weight verified visually.
+
+## Pro — DIP / SRP
+
+- **`ConnectorIngestor` interface** — `ingest.ts` dispatches on connector identity (URL substring +
+ tool-name sniff); adding a connector edits ≥4 fns. Per-connector object declares
+ category/buildQuery/pickTool; dispatcher picks first `matches()`. Needs live MCP connectors.
+- **SRP splits** (sequence AFTER the seams): `agent.proposeActions`,
+ `extract.extractObservationFromScreen`, `vault-service` unlock/recovery (inject a VaultStore).
+
+## Lint backlog surfaced by the new gates (warn-ratchet — grind down, tighten to error as areas clear)
+
+- **`@typescript-eslint/no-unnecessary-condition`**: ~289 dead-branch findings (190 core, 99 pro),
+ concentrated in the god-files (MemoryChat 42, ipc 12, App 12). Triage — some are dead branches to
+ delete, some are guards at untyped boundaries where the fix is to correct the TYPE.
+- **sonarjs on pro/**: ~295 findings (mostly cyclomatic/cognitive complexity on the CRM god-files +
+ duplicate strings). Decompose hotspot-first, test-covered.
+- **knip**: 27 unused dependencies, 84 unused exports, 30 unused exported types. Triage carefully —
+ a "dead" dep may be used in a build/runtime path knip can't trace; never blind-`npm remove`.
+
+## Evaluated and intentionally NOT changing (don't re-flag)
+
+`isMe` (token-overlap, DB-sourced) vs `isSelfName` (substring-position, injected list) — different
+matchers, not a dup. `markdownComponents` maps — intentionally styled per surface. `dayKey` (string)
+vs `dayKeyOf` (epoch-sec number) — different functions. dictation `buildSinks` — it's the factory
+itself (the delivery loop is already polymorphic over `OutputSink`); a SinkFactory is speculative
+(YAGNI) with two sinks.
diff --git a/docs/CONSOLE_PLAN.md b/docs/CONSOLE_PLAN.md
index 9aff0ba0..ec1143d8 100644
--- a/docs/CONSOLE_PLAN.md
+++ b/docs/CONSOLE_PLAN.md
@@ -7,8 +7,8 @@ the "app that connects to all the nodes" (Off Grid Desktop/Mobile). Next.js.
This is a **new, separate product** from Off Grid Desktop. The nodes already carry the
gateway and enforce policy locally (see `ENTERPRISE_BUILD_PLAN.md`). The Console does **not**
run the intelligence or enforce policy — it **defines and observes**: provisions policy +
-knowledge + config *down* to the fleet, aggregates audit + telemetry + distilled learnings
-*up*.
+knowledge + config _down_ to the fleet, aggregates audit + telemetry + distilled learnings
+_up_.
---
@@ -46,6 +46,7 @@ customer takes any subset and never the whole ecosystem to use one part:
- **All of it** — the full common control plane.
Baked into the build:
+
- **API-first.** Every module exposes its function over a documented API; the Console UI is
one consumer of that same API. "API only" is free — it's the contract the UI uses.
- **Independent modules.** Each plane/service (Gateway, Brain, Agents, Data/ingest, Fleet,
@@ -88,15 +89,15 @@ below, then wiring real nodes once Stage 1 ships.
Define this API first — both sides build against it. Pull-based, versioned, mTLS or
device-token auth.
-| Direction | Endpoint (Console side) | Purpose |
-|---|---|---|
-| Enroll | `POST /v1/devices/enroll` (with admin-issued enrollment token) | Node registers; Console issues a device identity/token (C4) |
-| Policy down | `GET /v1/devices/{id}/policy` | Node pulls current policy bundle (guardrails, egress rules, RBAC, AI-use policy) |
-| Config + knowledge down | `GET /v1/devices/{id}/provision` | Intelligence config + SOPs/KB refs the node's role gets (from Brain) |
-| Audit up | `POST /v1/devices/{id}/audit` | Node pushes audit batches (calls, what-left-device, tool use) |
-| Telemetry up | `POST /v1/devices/{id}/telemetry` | Tokens, latency, eval results, drift signals |
-| Learnings up | `POST /v1/devices/{id}/learnings` | Distilled SOPs/patterns (never raw capture) → Brain |
-| Commands | `GET /v1/devices/{id}/commands` (+ optional SSE) | Kill switch, re-provision, revoke — node polls/streams |
+| Direction | Endpoint (Console side) | Purpose |
+| ----------------------- | -------------------------------------------------------------- | -------------------------------------------------------------------------------- |
+| Enroll | `POST /v1/devices/enroll` (with admin-issued enrollment token) | Node registers; Console issues a device identity/token (C4) |
+| Policy down | `GET /v1/devices/{id}/policy` | Node pulls current policy bundle (guardrails, egress rules, RBAC, AI-use policy) |
+| Config + knowledge down | `GET /v1/devices/{id}/provision` | Intelligence config + SOPs/KB refs the node's role gets (from Brain) |
+| Audit up | `POST /v1/devices/{id}/audit` | Node pushes audit batches (calls, what-left-device, tool use) |
+| Telemetry up | `POST /v1/devices/{id}/telemetry` | Tokens, latency, eval results, drift signals |
+| Learnings up | `POST /v1/devices/{id}/learnings` | Distilled SOPs/patterns (never raw capture) → Brain |
+| Commands | `GET /v1/devices/{id}/commands` (+ optional SSE) | Kill switch, re-provision, revoke — node polls/streams |
---
@@ -105,29 +106,30 @@ device-token auth.
Navigation mirrors the planes (and the `ENTERPRISE_BUILD_PLAN.md` component map):
- **Fleet** — device inventory, enrollment, groups/roles, per-role policy + intelligence
- assignment, kill switch. *(C4, C9c, provisioning.)*
+ assignment, kill switch. _(C4, C9c, provisioning.)_
- **Control plane** — gateway config (model routing, leashed cloud), guardrail rules
(input/output), **egress rules**, **audit log explorer**, observability dashboards, RBAC
- authoring. *(C1, C2, C3, C5, C7, C8, C16.)*
+ authoring. _(C1, C2, C3, C5, C7, C8, C16.)_
- **Data plane** — connectors to DBs/warehouses/SaaS, ingest jobs + status, PII/masking
- rules, data catalog, retention/erasure (DSAR). *(A1, A3, A5, A7, A9, A11, A12a.)*
+ rules, data catalog, retention/erasure (DSAR). _(A1, A3, A5, A7, A9, A11, A12a.)_
- **AI plane (Brain)** — KB/SOP management (review, edit, publish "what good is"), model
- registry, retrieval config, **eval + drift** dashboards. *(B2a, B3, B5a, B16, C9, C9a.)*
+ registry, retrieval config, **eval + drift** dashboards. _(B2a, B3, B5a, B16, C9, C9a.)_
- **Regulatory** — the **DPO single view**: compliance status, framework→control mapping,
- one-click audit/DPIA export, AI-use-policy authoring. *(E1, E2, E6, C7 rollup.)*
+ one-click audit/DPIA export, AI-use-policy authoring. _(E1, E2, E6, C7 rollup.)_
---
## Standards (decision locked)
We follow the Wednesday **Standards Kit** for engineering and component sourcing, and the
-**Off Grid brutalist brand** (`docs/DESIGN.md`) for visual identity. Where the kit's *visual*
+**Off Grid brutalist brand** (`docs/DESIGN.md`) for visual identity. Where the kit's _visual_
identity conflicts with Off Grid (it uses green→teal gradients, Instrument Serif, DM Sans,
shimmer, card-lift, rich animation), **Off Grid wins** — the Console is one product family
with the Desktop/Mobile nodes it manages, and a dense compliance/audit tool suits the flat,
information-first look.
**Engineering standards (from the kit — adopted as-is):**
+
- Cyclomatic complexity **< 8** (no exceptions); refactor over nesting.
- Naming: **PascalCase** (components/types), **camelCase** (logic).
- Strict **import ordering**: React → Next → state → UI → alias → relative.
@@ -136,6 +138,7 @@ information-first look.
`aria-label` on icons, `alt` on images; 4.5:1 contrast.
**Visual identity (Off Grid `docs/DESIGN.md` — overrides the kit):**
+
- Menlo mono everywhere; single emerald accent (`#34D399`/`#059669`), **no gradients**.
- Flat 8px radius, hairline borders, no shadow/lift; hierarchy via size+opacity, not color.
- **No decorative animation** — loading spinner only. (So the kit's animation-timing table
@@ -147,22 +150,24 @@ information-first look.
`wednesday-solutions/component-library-animations` is a **catalog** — an index of the ~399
components that exist across the ecosystem (shadcn, aceternity, animate-ui, cult-ui,
eldora-ui, magic-ui, motion-primitives), with **example implementations**. Use the catalog to
-find the *right* component for each need; then bring that component in **from its real source
+find the _right_ component for each need; then bring that component in **from its real source
library**, and re-theme it. The repo's `Button` is an example — we don't import it; we use
the actual library's component.
**Setup (one-time):**
+
1. Clone `wednesday-solutions/component-library-animations` **inside** the console repo as a
**reference catalog**.
2. **`.gitignore` that clone** — it's for discovery, not a committed dependency. Checked out
fresh per environment.
**Workflow (every UI need):**
+
1. **Discover** — search `skills/component-library-index.md` (indexed **by use case**) +
`src/componentRegistry.tsx`. Find the right component and note **which library it's from**.
2. **Source it from that library** — install/add the real component from its upstream
(shadcn via its CLI, aceternity/magic-ui/cult-ui from their source). The catalog's file is
- the *example*; the real library is the *source of truth*.
+ the _example_; the real library is the _source of truth_.
3. **Re-theme to `docs/DESIGN.md`** — the brand overrides the library's defaults.
If a need isn't in the catalog, find the **closest catalog component** and adapt it — still
@@ -177,7 +182,7 @@ gradients**, **single emerald accent**, Menlo mono, **no decorative animation**)
admin console is dense and information-first, not a landing page.
- **Skip the decorative pieces** (AuroraBackground, NeonGradientCard, RainbowButton,
ShinyText, Meteors, GlowingStars…) — they fight the brand.
-- **Re-theme on use:** Menlo `font-mono`, emerald `#34D399/#059669` as the *only* accent,
+- **Re-theme on use:** Menlo `font-mono`, emerald `#34D399/#059669` as the _only_ accent,
flat 8px radius, hairline borders, hierarchy via size+opacity not color. Animation only
where functional (loading), never decorative.
@@ -208,11 +213,11 @@ Sequenced so the Console is demoable early (against mocks) and useful as soon as
the component-library-animations repo as a **discovery catalog**; source components from
their real libraries per the workflow above (no custom, no vendored repo copies); apply
`@offgrid/design` / `docs/DESIGN.md` theme; OIDC login, RBAC, Postgres schema (devices,
- policies, audit, users). Mocked node API. *Demoable console with a fake fleet, brand-
- matched, components sourced from the library.*
+ policies, audit, users). Mocked node API. _Demoable console with a fake fleet, brand-
+ matched, components sourced from the library._
2. **M1 — Fleet foundation.** Enrollment flow, device inventory, push a policy bundle,
ingest audit, the audit log explorer, kill switch. Wire to **real nodes** once node-side
- Stage 1 ships. *Done when an admin governs a real device from the console.*
+ Stage 1 ships. _Done when an admin governs a real device from the console._
- ✅ **Contract API + OpenAPI/Scalar docs shipped** — the full node↔console lifecycle
(enroll · pull policy · push audit · poll commands · admin token/policy/kill · fleet
audit) on a swappable in-memory store; OpenAPI 3.1 at `/openapi.json`, interactive
@@ -223,15 +228,15 @@ Sequenced so the Console is demoable early (against mocks) and useful as soon as
3. **M2 — Control plane UI.** ✅ Policy editor (egress toggle + editable guardrails + allowed
models, published as a **versioned** policy), **policy history**, **RBAC** (users + role
change, validated), and the audit log. Gateway section reads the live node gateway
- (`:7878`). *Observability dashboards deferred to the Analytics module.*
+ (`:7878`). _Observability dashboards deferred to the Analytics module._
4. **M3 — Data plane UI.** ✅ Connectors (add/sync/delete), ingest jobs, PII/masking rules
(add/toggle), data catalog with classification, and retention/erasure (DSAR). Real
- Postgres tables + admin APIs. *Next: wire ingest into Brain (M4).*
+ Postgres tables + admin APIs. _Next: wire ingest into Brain (M4)._
5. **M4 — Brain UI.** ✅ LanceDB ingestion→retrieval (RAG): KB/SOP list + add-document
(embed+index), semantic search with scored citations. Embeddings via the gateway
- `/v1/embeddings` (deterministic fallback). *Next: eval/drift + model registry.*
+ `/v1/embeddings` (deterministic fallback). _Next: eval/drift + model registry._
6. **M5 — Regulatory / DPO.** Compliance view, framework mapping, one-click audit/DPIA
- export, AI-use-policy authoring. *Done when a DPO can export a defensible pack.*
+ export, AI-use-policy authoring. _Done when a DPO can export a defensible pack._
7. **M6 — Self-host packaging.** Docker Compose bundle, install docs, backup/restore.
---
@@ -268,7 +273,7 @@ diagrams + OSS map). All on real Postgres + LanceDB + SSO + the live `:7878` gat
1. **Desktop grounding of the story** — survey `../desktop` so Fleet Control, auto-SOP
creation, and org-knowledge sync messaging matches what the app actually does
- (capture → synthesize → SOPs; memory + `@offgrid/sync`). *(in progress)*
+ (capture → synthesize → SOPs; memory + `@offgrid/sync`). _(in progress)_
2. **Multi-tenant Admin module + ABAC/RBAC** — tenants/orgs, provisioning (who the console
is for + their access), ABAC (tenant/purpose/data-class) layered on existing RBAC. Do now
to avoid a retrofit. Single interface (ours) — no white-labeling underlying tools.
diff --git a/docs/CONSOLIDATION_PLAN.md b/docs/CONSOLIDATION_PLAN.md
index af87ae0b..1a2df7f4 100644
--- a/docs/CONSOLIDATION_PLAN.md
+++ b/docs/CONSOLIDATION_PLAN.md
@@ -30,7 +30,6 @@ NOT DONE (deferred, deliberate):
forked-pipeline. NOT done - needs its own PR. See C7 below.
-->
-
**Scope:** 76 raw findings from parallel audits, deduped to **29 surviving actions** across 5 patterns. **9 findings dropped** as false positives or already-single-source (listed at the end). Four items are **live disagreements shipping today** (P0). Two audit claims were factually wrong on inspection and are corrected inline.
---
@@ -40,30 +39,35 @@ NOT DONE (deferred, deliberate):
These are not "risk of drift"; the two sides already disagree. Fix first, each with a regression test asserting the shared constant.
### P0.1 — `RagConversation.project_id` missing on the preload/renderer boundary
+
- **Pattern:** duplicate-config-or-type · **Principle:** DRY · **Severity:** high
- **Locations:** `src/main/database.ts:1129` (has `project_id?: string | null`), `src/preload/index.d.ts:46` (omits it), `src/renderer/src/env.d.ts:76` (omits it) — **verified**.
- **Fix:** Export `RagConversation` from `database.ts`; import in preload + renderer. Do not redeclare.
- **Drift risk (already real):** project-scoped conversations lose `project_id` at the preload type boundary — the frontend never sees it. Project chat routing silently degrades. This is a live bug, not hypothetical.
### P0.2 — `saveArtifact` kind union diverges preload vs renderer
+
- **Pattern:** duplicate-config-or-type · **Principle:** DRY · **Severity:** high
- **Locations:** `src/preload/index.ts:248` allows `'html'|'svg'|'mermaid'|'react'`; `src/renderer/src/env.d.ts:187` allows those **plus `'text'|'image'`** — **verified**.
- **Fix:** Use the canonical `ArtifactKind` (`src/main/artifacts.ts:33`) — better, the shared `@offgrid/artifacts` type (see D2) — everywhere. Align preload to the full union.
- **Drift risk (already real):** renderer can call `saveArtifact({kind:'text'})`, which the preload type rejects — a compile-time contract split; `text`/`image` artifacts hit an untyped path.
### P0.3 — `ctxSize` default: backend 16384 vs UI 32768
+
- **Pattern:** duplicate-config-or-type · **Principle:** DRY · **Severity:** medium (raised to P0: values disagree now)
- **Locations:** `src/main/llm.ts:70` (`ctxSize = 16384`), `src/renderer/src/components/SettingsPanel.tsx:151` (`s.ctxSize ?? 32768`, and the option list labels `65536` as "(default)") — **verified, three-way disagreement**.
- **Fix:** `src/shared/llm-defaults.ts` exporting `DEFAULT_CTX_SIZE`; import in both. Reconcile which value is truly the default (the option-list label says 65536 — a third number, decide deliberately).
- **Drift risk (already real):** a user with no stored setting sees 32768 in the UI while inference runs at 16384; "reset to defaults" changes actual behavior.
### P0.4 — Advanced sampler defaults: backend `undefined` vs UI hardcoded
+
- **Pattern:** duplicate-config-or-type · **Principle:** DRY · **Severity:** high
- **Locations:** `src/main/llm.ts:77-80` (`topP/topK/minP/repeatPenalty` = `undefined`, intentionally "let llama.cpp default" — confirmed by the source comment), `src/renderer/src/components/SettingsPanel.tsx:20` (`DEFAULTS` hardcodes `topP:0.95, topK:40, minP:0.05, repeatPenalty:1.1`) — **verified**.
- **Fix:** Put the decision in `src/shared/llm-defaults.ts` **once**. Either (a) initialize the backend fields from it, or (b) drop them from UI `DEFAULTS` so `undefined` stays `undefined`. Pick one; the backend comment says the intent is "let llama.cpp decide," so (b) is likely correct.
- **Drift risk (already real):** UI "Reset to defaults" pushes sampler overrides the backend never intended — fresh instances and post-reset instances infer differently.
### P0.5 — Two incompatible `Modality` types with the same name
+
- **Pattern:** duplicate-config-or-type · **Principle:** DRY · **Severity:** high
- **Locations:** `src/main/active-models.ts:10` = `'image'|'speech'|'transcription'`; `src/main/runtime-residency.ts:18` = `'llm'|'image'|'stt'|'tts'` — **verified**. Same name, different members, `speech`≠`tts`, `transcription`≠`stt`.
- **Fix:** One `Modality` in a shared module. Reconcile the vocabulary (`speech`/`tts`, `transcription`/`stt`) to a single set; `active-models` already owns the canonical `modalityForKind()` dispatch (`:18`), so anchor there and let residency extend it with the `llm` tier explicitly.
@@ -74,13 +78,16 @@ These are not "risk of drift"; the two sides already disagree. Fix first, each w
## Foundational shared modules (do these next — everything else imports them)
### F1 — `src/main/constants.ts`: engine ports
+
- **Pattern:** duplicate-config-or-type · **Principle:** DRY · **Severity:** high
- **`LLAMA_SERVER_PORT = 8439`** — `llm.ts:51`, `tools.ts:14`, `model-server.ts:39`, `setup.ts:44` — **verified (4 sites)**.
- **`GATEWAY_PORT = 7878`** — `GatewayScreen.tsx:6`, `setup.ts:45`, `model-server.ts:919` — **verified (3 sites)**. Renderer needs it too, so this constant (or a preload-exposed copy) must be reachable from the renderer.
- **Fix:** Single `src/shared/ports.ts`; import in main + expose to renderer. **Drift risk:** a port change misses one site → gateway UI snippets point at the wrong port; upstream proxy breaks.
### F2 — `src/shared/llm-defaults.ts`: sampling + timeouts + mode presets
+
Merges four findings into one config module:
+
- Sampler/ctx defaults (P0.3, P0.4).
- `temperature 0.7`, `maxTokens 2048`, tool `temperature 0.3 / max_tokens 1024`, `timeoutMs 300000` — `llm.ts:69,81,554-555`, `tools.ts:303`.
- `LLAMA_RELOAD_TIMEOUT_MS = 45_000` — `model-server.ts:569,576`.
@@ -88,6 +95,7 @@ Merges four findings into one config module:
- **Principle:** DRY · **Severity:** high (aggregate). **Fix:** one `LLAMA_DEFAULTS` object + `MODE_PRESETS`; backend fields and UI both import. **Drift risk:** tool-chat's deliberate 0.3 temp is an invisible magic number; mode-preset ctx and RAM-budget fractions drift apart across `llm.ts`/`setup.ts`.
### F3 — `src/main/types/model-kinds.ts`: model-kind / capability source of truth
+
- **Pattern:** branch-on-concrete-type · **Principle:** OCP · **Severity:** medium
- **Locations:** `model-server.ts:638` (`'vision'`/`'chat'` string literals), `setup.ts:183` (`performanceMode` triple-check), `models-manager.ts:301,305` (`kind === 'image'|'speech'|'transcription'` dispatch), `ModelsScreen.tsx:88,231,296` (`m.kind === 'vision'`).
- **Fix:** const-object `MODEL_KINDS` + `isVisionKind()`/capability guards; **route all modality dispatch through the existing `modalityForKind()`** (`active-models.ts:18`) — models-manager should call it, not re-branch. Renderer asks "supports vision?" via a capability check, not `kind ===`.
@@ -98,6 +106,7 @@ Merges four findings into one config module:
## Group A — duplicate-config-or-type (remaining)
### A1 — Cross-boundary type triplication (UserProfile / RagMessage / AppSettings / PermissionStatus / ReprocessProgress)
+
- **Principle:** DRY · **Severity:** high
- **UserProfile:** `database.ts:1090`, `preload/index.d.ts:3`, `renderer/env.d.ts:3`
- **RagMessage:** `database.ts:1138`, `preload/index.d.ts:54`, `renderer/env.d.ts:84`
@@ -108,22 +117,26 @@ Merges four findings into one config module:
- **Coupling:** touches `preload/index.d.ts` and `renderer/env.d.ts` — the SAME two files as P0.1, P0.2. **Sequence these; do not parallelize.**
### A2 — Artifact kind→label/icon/runtime maps duplicated
+
- **Principle:** DRY · **Severity:** medium
- **Locations:** `artifacts.ts:37` (runtime if-chain), `ArtifactCanvas.tsx:13`, `ProjectsScreen.tsx:29`, plus the wider renderer `KIND_LABEL/KIND_ICON` spread in `ModelsScreen.tsx:110`, `StoragePanel.tsx:30-32`, `SetupPanel.tsx:15-20`, `CommandPalette.tsx:10`.
- **Fix:** one `ARTIFACT_CONFIG` (kind → {label, icon, runtime}); ideally sourced from `@offgrid/artifacts` (see D2). Renderer kind-maps → `src/renderer/src/constants/kinds.ts`. **Drift risk:** Projects label ≠ Canvas label; new kind labeled in one place only.
- **Coupling:** overlaps D2 (both edit `artifacts.ts` + `ArtifactCanvas.tsx`) — do D2's import swap and A2's map extraction together.
### A3 — Notification type descriptors duplicated across switch/union/array
+
- **Principle:** DRY / OCP · **Severity:** high
- **Locations:** `useNotifications.tsx:5` (union), `NotificationList.tsx:13,15-25,46-53,69,227`.
- **Fix:** `NOTIFICATION_TYPE_DESCRIPTORS` (type → {label, icon}) in a shared `notificationUtil.ts`; derive `FilterType` and `filterOptions` via `keyof`. Mirror VaultScreen's `TYPES` pattern (which the audit correctly flags as the good example). **Drift risk:** new type → missing icon/label, raw enum in badge (`:227`).
### A4 — App route ↔ viewMode maps defined twice (with real gaps)
+
- **Principle:** DRY · **Severity:** medium
- **Locations:** `App.tsx:221-241` (path→view) and `App.tsx:266-288` (view→path). Audit reports `/clipboard` + `/vault` missing from the forward map.
- **Fix:** one `ROUTES` catalog; generate both maps from it. **Drift risk:** deep links reach a route that never activates the view. **Coupling:** same file as A9 (App.tsx core-screen chain).
### A5 — Strictness settings + prompt-key strings fetched ad hoc
+
- **Principle:** DRY · **Severity:** low/medium
- **Locations:** strictness `ipc.ts:361,418`; prompt keys `ipc.ts:23,38,82,113,130,363,422,480,511,933`.
- **Fix:** `getStrictness(category)` helper + `PROMPT_KEYS` constants. **Drift risk:** mistyped prompt key silently falls back to empty instructions; strictness default fixed in one call site only. **Note:** low priority — bundle into the ipc.ts refactor (A8) since it touches the same file.
@@ -133,16 +146,19 @@ Merges four findings into one config module:
## Group B — branch-on-concrete-type (remaining)
### B1 — Artifact kind dispatch in `artifacts.ts` + gallery
+
- **Principle:** OCP · **Severity:** low/medium
- **Locations:** `artifacts.ts:37,38,67,71` (`artifactRuntime`/`deriveTitle` parallel chains); `MemoryChat.tsx:2587-2598` (gallery onClick + thumbnail by kind).
- **Fix:** the `ARTIFACT_CONFIG` map from A2 + an `openArtifactPanel(artifact, handlers)` helper. Folds into A2/D2.
### B2 — Attachment kind branching scattered in MemoryChat
+
- **Principle:** OCP · **Severity:** medium
- **Locations:** `MemoryChat.tsx:666,1220,1607-1625,2182-2210`.
- **Fix:** `renderAttachmentPreview(att)` + viewer map keyed by kind. **Note:** lands inside the MemoryChat decomposition (C3) — do together, same file.
### B3 — Transcription engine guards (`entry.engine === 'parakeet'`)
+
- **Principle:** OCP · **Severity:** medium
- **Locations:** `transcription/whisper-cli.ts:61`, `transcription/parakeet-cli.ts:113`.
- **Fix:** `modelsByEngine(engine)` pure fn in `select.ts`; both CLIs call it. Pairs with C1 (transcription dispatcher).
@@ -152,23 +168,27 @@ Merges four findings into one config module:
## Group C — god-module / forked-pipeline
### C1 — Transcription selection fork (`select.ts`) + engine dispatch
+
- **Pattern:** forked-pipeline · **Principle:** OCP · **Severity:** medium
- **Locations:** `select.ts:32-45,77-80,85-87,94-100` (`pickTranscription` + 4 callers re-deciding), plus B3's engine guards.
- **Fix:** one dispatcher `resolveTranscription(engine, mode?, services?) → {service, engine, fellBack}`; all callers invoke it. **Drift risk:** whisper-resident→whisper fallback priority spread across 4 fns.
### C2 — Image-runtime god-path in `imagegen.ts`
+
- **Pattern:** god-module + branch-on-concrete-type · **Principle:** SRP/OCP · **Severity:** high
- **Locations:** `imagegen.ts:40-48` (`isCoreMLModelDir`), `72-79`, `330` (`mfluxAvailable`), `416` (`isMfluxModelId`), `477`, `485-509`, `537-644`, `602-714`. `runImageGen` is 500+ LOC interleaving mflux/sd-cli/Core ML/Z-Image memory guards + arg building + preview parsing.
- **Fix:** `ImageRuntime` interface (`generate/validateMemory/buildArgs/parseProgress`) with `ImageRuntimeMflux/SdCli/CoreML`; dispatch once at the top of `runImageGen`. Move `isCoreMLModelDir` to a shared `runtime-model-detect.ts`.
- **Coupling with C5 (models-manager runtime dispatch):** both introduce a per-runtime handler abstraction — design ONE `RuntimeHandler`/`ImageRuntime` shape and reuse across `imagegen.ts` and `models-manager.ts` so we don't build two parallel registries. Sequence C5 after C2's interface lands.
### C3 — `MemoryChat.tsx` god component (2600 LOC, 63 useState, 286-LOC `sendMessage`)
+
- **Pattern:** god-module · **Principle:** SRP · **Severity:** high
- **Locations:** `MemoryChat.tsx:200-330` (state), `652-937` (`sendMessage`), plus B2, and the citation/source dispatch (C6) and RagContext render (A-adjacent).
- **Fix:** extract hooks — `useMessageSender`, `useImageGeneration`, `useVoiceInput`, `useGallery`, `useComposerPrefs`, `useAttachments`, `useStreamingChat`. MemoryChat becomes a coordinator.
- **Largest deliberate refactor here.** Do LAST, after the renderer shared helpers (timeAgo D3, SearchHit handler C6, attachment map B2) exist so the extracted hooks import them instead of carrying the duplication forward.
### C4 — `ipc.ts` `rag:chat` god-handler + parallel SQL filters
+
- **Pattern:** god-module + forked-pipeline · **Principle:** SRP/DRY · **Severity:** high
- **Locations:** `ipc.ts:656-970` (314-line handler, 5 intent paths); parallel `source_app`/`app_name LIKE` filters `ipc.ts:547-548,780-781,797-798,814-815,830-831,844-852,868-869` and the 5-query block `774-802,805-818,821-834,837-856,859-872`; also `database.ts:395-396,701-702,722-723,754-755` and `search.ts:150-157`.
- **Fix (two layers, do the cheap one first):**
@@ -177,16 +197,19 @@ Merges four findings into one config module:
- **Coupling:** A5 (strictness/prompt keys), A6-embeddings, and B/broadcast all live in `ipc.ts`. **One owner for `ipc.ts` per round.**
### C5 — `models-manager.ts` per-runtime `=== 'mflux'` branching ×3
+
- **Pattern:** duplicate-config-or-type/OCP · **Severity:** high
- **Locations:** `models-manager.ts:55,111-123,212-226` (list/download/delete each branch on runtime).
- **Fix:** `RuntimeHandler {isCached, download, delete}` map — the SAME abstraction as C2. **Drift risk:** third runtime → edits in 3 functions.
### C6 — SearchHit / unified-source navigation dispatch duplicated
+
- **Pattern:** copy-pasted-helper + branch-on-concrete-type · **Severity:** high
- **Locations:** `App.tsx:440-452`, `MemoryChat.tsx:432-438`, `MemoryChat.tsx:1859-1864`.
- **Fix:** `useSearchHitHandler({onEntity,onMemory,onMeeting,onReplay})` in renderer nav utils; call from all three. **Drift risk:** one site adds screen-replay, another doesn't. Prerequisite helper for C3.
### C7 — `toolChat` is a SECOND generation pipeline forked from `ragChat`/`streamAnswer` (MISSED BY THE ORIGINAL AUDIT)
+
- **Pattern:** forked-pipeline · **Principle:** SRP/DRY · **Severity:** high · **Status:** NOT done (deferred - behavioral change)
- **This is the fork that started the whole effort** (the "chat doesn't stream / no thinking bubble" bug). The audit scanned for behavior-preserving mechanical dedup and bucketed this as a "product fix," so it never became a plan item - a real gap. Recording it here as the canonical forked-pipeline.
- **Locations:** `src/main/tools.ts` `toolChat()` runs its OWN blocking `fetch` loop to `/v1/chat/completions` (its own port const, `max_tokens:1024`, `temperature:0.3`, no streaming, no thinking) - parallel to `ipc.ts` `streamAnswer()` -> `llm.chatStream()` (streams tokens + reasoning over `rag:stream`, honors `thinking`, abortable). Renderer forks at `MemoryChat.tsx:811`: `if (toolsOn || connectorsOn) -> window.api.toolChat(...)` (blocking) else the streaming path.
@@ -195,23 +218,27 @@ Merges four findings into one config module:
- **Why deferred from this PR:** every other consolidation item is behavior-preserving; this one changes what the tool path DOES (starts streaming + thinking), so it needs its own PR + on-device verification. It is the highest-value remaining forked-pipeline.
### C8 — `rag:chat` runs TWO retrieval pipelines on the same query (found by the C7 follow-up sweep)
+
- **Pattern:** forked-pipeline · **Principle:** DRY · **Severity:** high · **Status:** NOT done (behavioral)
- **Locations:** `ipc.ts:771-802` (inline SQL vector search on memories, threshold >=0.2) + `ipc.ts:804-872` (inline FTS on messages/summaries/entities/facts) AND then `ipc.ts:904-905` `universalSearch()` (search.ts - hybrid FTS + LanceDB semantic + RRF fusion + recency boost) on the SAME query.
- **Drift/risk:** the same memories/entities are retrieved twice with DIFFERENT ranking (BM25/cosine + threshold vs RRF, no threshold). The CONTEXT_BLOCK (from inline SQL) and the SOURCES cards (from universalSearch) can disagree; an item below the SQL 0.2 threshold is dropped from context but shown as a source. Project mode (`ipc.ts:737` `ragService.searchProject`) is yet a third retrieval path that misses the hybrid fusion.
- **Fix:** make `universalSearch()` the single retrieval engine for all three chat modes; delete the inline SQL block; pass a `sources`/appName filter param. (Overlaps C4 - same handler.)
### C9 — image-generation intent decided in 3 places that can disagree
+
- **Pattern:** forked-pipeline + duplicated-decision-rule · **Principle:** DRY/SRP · **Severity:** medium · **Status:** NOT done (behavioral)
- **Locations:** renderer regex `looksLikeImageRequest` (`src/renderer/src/lib/image-intent.ts`) auto-switches to image mode at `MemoryChat.tsx:746`; main LLM classifier `classifyIntent` (`ipc.ts:254-280`) decides `intent==='image'`; the model itself can emit a ` ```image ` fenced block parsed at `MemoryChat.tsx:863`. The ` ```image ` format is PRODUCED in `ipc.ts:668` and PARSED by a separate regex in the renderer (not via `parseArtifact`).
- **Drift/risk:** renderer regex and main LLM can disagree on "is this an image request" (e.g. "make a dashboard" - regex no, LLM maybe); the fenced-block format has no single producer/parser definition.
- **Fix:** one intent decision (renderer asks main via an intent IPC, or shares one rule module); define the ` ```image ` fence once and parse it through `parseArtifact`.
### C10 — `maxTokens` default duplicated (same class as P0.3, but MECHANICAL/behavior-preserving)
+
- **Pattern:** duplicate-config-or-type · **Principle:** DRY · **Severity:** low · **Status:** NOT done (cheap, safe)
- **Locations:** `llm.ts` `private maxTokens = 2048` and `SettingsPanel.tsx` DEFAULTS `maxTokens: 2048`. Currently AGREE, but there is no shared constant (unlike ctxSize, which we already moved to `shared/llm-defaults.ts` as `DEFAULT_CTX_SIZE`).
- **Fix:** add `DEFAULT_MAX_TOKENS = 2048` to `shared/llm-defaults.ts`, import in both. This one IS behavior-preserving - could fold into this PR or a trivial follow-up. (The audit also flagged tool-chat's `max_tokens:1024`/`temperature:0.3` magic numbers - fold those into the shared defaults too.)
### C11 — summarization/entity extraction is 4+ independent LLM calls with independently-set strictness
+
- **Pattern:** forked-pipeline · **Principle:** SRP · **Severity:** medium · **Status:** NOT done (behavioral)
- **Locations:** `ipc.ts:371` (memory-eval, applies `memoryStrictness`), `ipc.ts:515` (session summary), `ipc.ts:426` (entity extraction, applies `entityStrictness`), `ipc.ts:488` (per-entity fact summary), plus `updateMasterMemoryIncremental`. Each is its own prompt + LLM call.
- **Drift/risk:** memory-eval strictness and entity strictness are set independently and can disagree on what is worth keeping; no dedup across repeated summarizations. Not a correctness bug today, but a coherence/cost fork.
@@ -224,43 +251,51 @@ Merges four findings into one config module:
## Group D — copy-pasted-helper / forked shared packages
### D1 — `extractJson` duplicated across 10+ pro modules, 3 fallback variants
+
- **Pattern:** copy-pasted-helper · **Principle:** DRY · **Severity:** high
- **Locations (verified, larger than reported):** `pro/main/meetings.ts:71`, `ingest.ts:39`, `crm/agent.ts:66`, `crm/extract.ts:68`, `crm/preferences.ts:92`, `crm/actions.ts:135`, `dictation/sinks/memory-ingest.ts:16`, **plus** `crm/calendar.ts:58` (`[`/`]` array variant), `crm/layout.ts:99`, `crm/organize.ts:73`.
- **Fix:** one helper in a shared pro util (`pro/main/crm/json.ts` or a `@offgrid/core` util) with `mode: 'object'|'array'` and configurable fallback. **Drift risk (real today):** `preferences.ts` returns `s` on failure, others return `'{}'`, array variants return `']'`-slice — inconsistent silent-fail behavior across every LLM JSON parse.
### D2 — Desktop reimplements `@offgrid/artifacts` and `@offgrid/rag` extraction
+
- **Pattern:** forked-pipeline/duplicate-config-or-type · **Principle:** DRY · **Severity:** high
- **Locations:** `src/main/artifacts.ts:33-40` + `ArtifactCanvas.tsx:11-18,85-130` reimplement `ArtifactKind`/`isLiveKind`/`artifactTitle`/`buildSrcDoc` — **verified those exist in `../shared/packages/artifacts/src/index.ts:31,52,95` and desktop does NOT depend on `@offgrid/artifacts` at all**. Also `src/main/files.ts:13-76` re-routes file-kind detection that `@offgrid/rag` `extract.ts` owns.
- **Correction to the audit:** the claim that `files.ts` `AUDIO_EXT` is "missing `oga` and `aiff`" is **false** — `files.ts:14` includes both. The routing duplication is real; that specific drift-example is not.
- **Fix:** add `@offgrid/artifacts` dep; import `isLiveKind/artifactTitle/buildSrcDoc` and the canonical `ArtifactKind`. Reuse `@offgrid/rag`'s `extractContent` in `files.ts`, wrapping with desktop temp-file/userData persistence. **Coupling:** anchors A2, B1, P0.2 — do the import swap first, then those maps reference the shared type.
### D3 — `timeAgo` duplicated (renderer) + `formatTimestamp` (pro)
+
- **Pattern:** copy-pasted-helper · **Severity:** medium/low
- **Locations:** `MemoryChat.tsx:183-195`, `ProjectsScreen.tsx:104-114`, and pro `NotificationList.tsx:27-44` vs `clipboard/clipboardUtil.ts:113-122`.
- **Fix:** `src/renderer/src/lib/time.ts` `timeAgo(input: string|number|Date)`; pro imports (or a pro `timeUtils.ts`). **Drift risk:** timezone/format fix applied to one copy only.
### D4 — Pro CRM helper trio: `today()`, `hasColumn()`, `notify()`
+
- **Pattern:** copy-pasted-helper · **Severity:** medium
- **Locations:** `today()` `crm/skills-engine.ts:30-32` + `crm/proactive.ts:18-20`; `hasColumn()` `crm/preferences.ts:38-40` + `crm/actions.ts:74-76`; `notify()` `crm/skills-engine.ts:19-28` + `crm/proactive.ts:24-33` (**with variance** — skills truncates body to 240, proactive doesn't).
- **Fix:** `pro/main/crm/utils.ts` (or `time.ts`/`schema.ts`/`notify.ts`); `notify(opts, {truncate?})`. **Drift risk:** inconsistent notification truncation UX today.
### D5 — Image-data-URL + message-construction + thinking-payload helpers
+
- **Pattern:** copy-pasted-helper · **Severity:** medium/low
- **Locations:** data-URL build `llm.ts:576-586,652-656`, `model-server.ts:263-265`, `tools.ts:284-286`; message-array build `llm.ts:569-594` vs `651-663`; thinking payload `llm.ts:675-680`.
- **Caveat (verified):** `tools.ts` already imports `buildUserContent` from `tool-content.ts` — a partial seam exists. **Check `tool-content.ts` first** and extend it rather than making a fourth copy. Extract `imageToDataUrl()`, `buildMessages()`, `buildThinkingPayload()` around it. **Drift risk:** webp/MIME support added to one path, missed in another.
### D6 — HTTP retry-with-deadline duplicated (`model-server.ts`)
+
- **Pattern:** copy-pasted-helper · **Severity:** medium
- **Locations:** `model-server.ts:187-217` (`proxyToLlama`) + `504-543` (`callLlamaJson`).
- **Fix:** `retryWithDeadline(deadline, attempt)`. **Drift risk:** off-by-one on the deadline check affects only one of streaming/buffered paths.
### D7 — Embeddings serialization + service coupling + BrowserWindow broadcast
+
- **Pattern:** copy-pasted-helper · **Principle:** DRY/DIP · **Severity:** medium
- **Locations:** serialize/parse `ipc.ts:318-319,565-566,591-592,771-772`, `database.ts:50-72`, `search.ts:335-336`, rag/store parseEmbedding; broadcast idiom `ipc.ts:338-344,465-473,1328-1335`; `embed()` wrapper `embeddings.ts:29`.
- **Fix:** `serializeVector`/`deserializeVector` + `embed(text)` in `embeddings.ts`; `broadcast(channel, payload)` module helper. **Drift risk:** format/caching change misses a call site → incompatible stored vectors, score=0 results vanish. **Note:** deferred embedding dimension validation is a nice-to-have, not dedup — keep it out of scope unless bundled.
- **Coupling:** all in `ipc.ts` — bundle with C4 under one owner.
### D8 — `existing()` binary-resolver + pro `TypeIcon` component
+
- **Pattern:** copy-pasted-helper · **Severity:** low/high
- **Locations:** `existing()` `whisper-cli.ts:19-28` + `parakeet-cli.ts:24-33` → `transcription/bin-resolution.ts`. `TypeIcon` `pro/renderer/ClipboardPopup.tsx:14-19` + `ClipboardScreen.tsx:74-79` → `clipboardUtil.ts` (alongside existing `CONTENT_TYPE_FILTERS`/`typeLabel`). Pro notification filter/count `NotificationList.tsx:59-61,117-119` → `notificationMatches()` (folds into A3).
@@ -298,9 +333,9 @@ Merges four findings into one config module:
3. **`whisper/parakeet isAvailable()`** — correct polymorphism; each service owns its own check. Audit itself marked it intended.
4. **`mfluxAvailable()` "duplicated"** — actually defined once in `mflux.ts` and imported; already DRY.
5. **Skill trigger branching (`skills-engine.ts:122`)** — single centralized dispatch; exemplary, not a violation.
-6. **ModelsScreen vision-kind checks as "single boundary"** (finding #57) — the *renderer-boundary* framing is right, but the SAME lines appear in the branch-on-concrete finding (#38); folded into F3, not double-counted. The "no fix needed" duplicate is dropped.
+6. **ModelsScreen vision-kind checks as "single boundary"** (finding #57) — the _renderer-boundary_ framing is right, but the SAME lines appear in the branch-on-concrete finding (#38); folded into F3, not double-counted. The "no fix needed" duplicate is dropped.
7. **`VaultScreen TYPES` "only used locally"** — correct single-source pattern; speculative export (YAGNI). Keep as-is.
-8. **`ArtifactCanvas` kind branching for rendering** — a legitimate single rendering boundary; not scattered. Only the *gallery caller* (B1) is the real finding.
+8. **`ArtifactCanvas` kind branching for rendering** — a legitimate single rendering boundary; not scattered. Only the _gallery caller_ (B1) is the real finding.
9. **Embedding dimension schema validation** (part of finding #28) — a correctness/robustness feature, not deduplication; out of scope for this plan (note it in the gaps backlog instead).
-**Corrections to audit claims:** `files.ts:14` AUDIO_EXT is NOT missing `oga`/`aiff` (both present) — that drift example is wrong, routing dup is still valid (D2). `extractJson` is 10+ sites with `[`/`]` variants, not 7 (D1). `tools.ts` already uses `buildUserContent` — D5 must extend that seam, not add a fourth copy.
\ No newline at end of file
+**Corrections to audit claims:** `files.ts:14` AUDIO_EXT is NOT missing `oga`/`aiff` (both present) — that drift example is wrong, routing dup is still valid (D2). `extractJson` is 10+ sites with `[`/`]` variants, not 7 (D1). `tools.ts` already uses `buildUserContent` — D5 must extend that seam, not add a fourth copy.
diff --git a/docs/DESIGN.md b/docs/DESIGN.md
index 68a93373..ce4eb735 100644
--- a/docs/DESIGN.md
+++ b/docs/DESIGN.md
@@ -1,6 +1,6 @@
# Off Grid AI Desktop — Design
-The desktop adaptation of the Off Grid design philosophy. The brand canon is the mobile docs (`../../mobile/docs/design/DESIGN_PHILOSOPHY_SYSTEM.md` + `VISUAL_HIERARCHY_STANDARD.md`); **this doc keeps the same soul and adapts it for a desktop app.** Where this conflicts with the mobile docs on *layout/interaction*, desktop wins; where it conflicts on *brand* (font, color, voice), the brand wins.
+The desktop adaptation of the Off Grid design philosophy. The brand canon is the mobile docs (`../../mobile/docs/design/DESIGN_PHILOSOPHY_SYSTEM.md` + `VISUAL_HIERARCHY_STANDARD.md`); **this doc keeps the same soul and adapts it for a desktop app.** Where this conflicts with the mobile docs on _layout/interaction_, desktop wins; where it conflicts on _brand_ (font, color, voice), the brand wins.
---
@@ -9,7 +9,7 @@ The desktop adaptation of the Off Grid design philosophy. The brand canon is the
**Brutalist, minimal, terminal-inspired.** Functionality over decoration. Clarity, density, respect for attention. Silence over noise. Remove before adding.
- **Typeface: Menlo (monospace) everywhere.** No sans UI font, no mixed families.
-- **Single accent: emerald** — `#34D399` (dark) / `#059669` (light). Used *sparingly* — active states, focus, primary actions, links, success. Everything else is monochrome.
+- **Single accent: emerald** — `#34D399` (dark) / `#059669` (light). Used _sparingly_ — active states, focus, primary actions, links, success. Everything else is monochrome.
- **Base:** pure black `#0A0A0A` (dark) / white `#FFFFFF` (light). Three surface tiers: `background → surface → surfaceLight`.
- **Hierarchy through size + weight + opacity, never color.** Weights stay light (≤ medium); avoid bold for emphasis.
- **Flat & sharp:** 8px radius, hairline borders, no gradients, no heavy shadows, no emojis, no decorative animation.
@@ -20,15 +20,15 @@ The desktop adaptation of the Off Grid design philosophy. The brand canon is the
Desktop is a **wide, mouse-driven, multi-window** canvas. Adapt accordingly:
-| Concern | Mobile | **Desktop** |
-|---|---|---|
-| Canvas | one narrow column, vertical scroll | **wide** — multi-column grids, master-detail, side panels, dense tables. Use the width; don't center a phone-width strip. |
-| Navigation | bottom tabs | **persistent left icon-rail sidebar** (active = emerald). |
-| Pointer | touch, ≥44px targets, `hitSlop` | **mouse** — precise targets fine; **hover is a first-class state** (reveal actions, brighten borders/text on hover). |
-| Density | compact | **denser still** — desktop shows more at once (5-col galleries, 3-col dashboards, long lists). |
-| Input | on-screen | **keyboard** — shortcuts (space/arrows in Replay, Cmd+[ / Cmd+] nav), Enter-to-submit. |
-| Chrome | full-screen flows | **window + menu-bar tray** (pause/recalibrate), title is always "Off Grid AI Desktop". |
-| Detail | push a screen | **detail screens / side panels** (e.g. click a connector row → its own detail view). |
+| Concern | Mobile | **Desktop** |
+| ---------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
+| Canvas | one narrow column, vertical scroll | **wide** — multi-column grids, master-detail, side panels, dense tables. Use the width; don't center a phone-width strip. |
+| Navigation | bottom tabs | **persistent left icon-rail sidebar** (active = emerald). |
+| Pointer | touch, ≥44px targets, `hitSlop` | **mouse** — precise targets fine; **hover is a first-class state** (reveal actions, brighten borders/text on hover). |
+| Density | compact | **denser still** — desktop shows more at once (5-col galleries, 3-col dashboards, long lists). |
+| Input | on-screen | **keyboard** — shortcuts (space/arrows in Replay, Cmd+[ / Cmd+] nav), Enter-to-submit. |
+| Chrome | full-screen flows | **window + menu-bar tray** (pause/recalibrate), title is always "Off Grid AI Desktop". |
+| Detail | push a screen | **detail screens / side panels** (e.g. click a connector row → its own detail view). |
---
@@ -53,12 +53,12 @@ Font font-mono everywhere
Same 5 roles as mobile, scaled for a monitor:
-| Role | Tailwind | Use |
-|---|---|---|
-| **TITLE** | `text-lg tracking-tight text-white` | one per screen (page title) |
-| **BODY** | `text-sm text-neutral-200/300` | primary content, list items, inputs, buttons |
-| **SUBTITLE** | `text-sm text-white` / section `` | section/card/modal titles |
-| **DESCRIPTION** | `text-xs text-neutral-500` | explanatory text under a title |
+| Role | Tailwind | Use |
+| ---------------- | ------------------------------------------------------------------------------------- | ------------------------------------------------------ |
+| **TITLE** | `text-lg tracking-tight text-white` | one per screen (page title) |
+| **BODY** | `text-sm text-neutral-200/300` | primary content, list items, inputs, buttons |
+| **SUBTITLE** | `text-sm text-white` / section `` | section/card/modal titles |
+| **DESCRIPTION** | `text-xs text-neutral-500` | explanatory text under a title |
| **META / LABEL** | `text-[11px]` or `text-[10px]`, labels `uppercase tracking-wide text-neutral-500/600` | timestamps, counts, tags, section markers ("whispers") |
Rules: hierarchy from size+opacity (not color); section labels are tiny, **uppercase**, widely tracked, muted; metadata whispers.
@@ -101,7 +101,7 @@ Rules: hierarchy from size+opacity (not color); section labels are tiny, **upper
## Checklist
- [ ] Menlo (`font-mono`) everywhere; weights light.
-- [ ] Emerald is the *only* accent, used sparingly; rest monochrome.
+- [ ] Emerald is the _only_ accent, used sparingly; rest monochrome.
- [ ] Uses the full width — multi-column / master-detail where it helps.
- [ ] Hierarchy from size + opacity, not color; labels tiny/uppercase/muted.
- [ ] Hover states reveal/brighten; focus is emerald.
diff --git a/docs/DEVICE_TEST_LOG.md b/docs/DEVICE_TEST_LOG.md
new file mode 100644
index 00000000..a866e0d8
--- /dev/null
+++ b/docs/DEVICE_TEST_LOG.md
@@ -0,0 +1,88 @@
+# Device test log — QA bug hunt (feat/consolidation-and-coverage)
+
+Adversarial QA sweep of the desktop app: 6 read-only agents hunting to PROVE THE
+CODE WRONG across download, chat (streaming / tools / MCP / modality), projects,
+model settings, and the Settings surface. Every item below is **code-traced to a
+real path + a user-visible symptom** — but **NONE is yet verified on-device**.
+
+This is the fix-then-verify list: each bug gets a red-first regression test (assert
+the TERMINAL artifact — the persisted row / rendered bubble / file on disk — across
+the untested intersection), the fix, then a manual on-device check ticked here.
+
+Status: `[ ]` open (found, not fixed) · `🔁` fixed, needs on-device recheck · `[x]` ✅ verified on device.
+Severity in **CAPS**. `§A` = data-layer/presentation drift · `ARCH` = SOLID/DRY/SoC/open-core.
+
+All 35 were found in UNTESTED intersections — the default-config trap (suite sits at
+single-conversation / happy-path / one-model / non-empty-profile), so the other axis
+value had zero coverage.
+
+---
+
+## Download & storage
+
+- 🔁 **D1 CRASH** — FIXED (needs on-device confirm). Download write errors (disk full / EIO) emitted an unhandled `'error'` → whole-app crash, and the finish-wait hung. Fix: extracted `pumpToFile` (models/download-pump.ts) owns the error path → rejects → status 'failed'. Test: `download-pump.test.ts` (real write stream; ENOENT → graceful reject; naive version times out = the bug, falsified). **On-device:** fill the disk (or point models dir at a tiny volume), start a large download → it shows Failed, the app stays up (no crash), chat/capture keep working. `models-manager.ts` + `models/download-pump.ts`
+- 🔁 **D2 DATA-LOSS** — FIXED (needs on-device confirm). A truncated/corrupt download was promoted to installed (no verify before rename) → activating it → "Chat model Down". Fix: `downloadIntegrityError` gates the rename (byte-count vs content-length + GGUF magic/size); throws → status 'failed'. Test: `download-verify.test.ts` (truncated/corrupt/too-small rejected; falsified). **On-device:** interrupt a large model download near the end (kill wifi) → it shows Failed, not installed; the model isn't offered as activatable. `models/download-verify.ts`
+- 🔁 **D3 CORRUPT** — FIXED (needs on-device confirm). Double-download of the same id raced two writers into one .part (corrupt) + overwrote the controller. Fix: downloadModel no-ops if that id is already in flight. Test: `download-reentrancy.test.ts` (source guard; red on HEAD). **On-device:** double-click Download on a model → one download starts, Cancel works. `models-manager.ts`
+- ❌ **D4 NOT-REACHABLE-ON-MACOS** (investigated, dismissed). The race is real (clearDownload rmSyncs the `.part` while pumpToFile's write stream may be open), but on macOS/POSIX, `unlink` of an open file removes the dir entry while the inode lives until the fd closes — remaining buffered writes go to that unlinked inode (no new dir entry) and it's freed on `out.end()`. No orphan `.part` reappears, and the download isn't re-invoked (the D3 re-entrancy guard prevents a fresh createWriteStream), so no new file is created. The loop's own post-abort `fs.rmSync(partPath)` is then a no-op. The "orphan recreated, leaking GBs" premise needs a platform where unlink-open recreates the entry — not macOS, the only target. Recorded for honesty.
+- ❌ **D5 BENIGN** (investigated, dismissed). Claim: setState on the unmounted StoragePanel leaks. On React 19.2 (this app) setState-after-unmount is a silent no-op — the warning was removed — and the effect cleanup DOES `clearInterval` + unsubscribe `onModelProgress` (`StoragePanel.tsx:53`). The only residue is one in-flight `getStorageInfo` resolving to a no-op setState (trivial wasted work, no leak/warning/crash). Not worth a guard; recorded for honesty.
+- 🔁 **D6 WRONG-STATE** — FIXED (needs on-device confirm). Deleting the active IMAGE model didn't clear its selection (stored by filename, delete compared id) → dangling pointer. Fix: `modalSelectionMatches` matches id OR primary filename; deleteModel passes primaryFileName. Test: `catalog-logic.test.ts` (filename-match case). **On-device:** activate an image model, delete it → no dangling active pointer; a later image gen picks a valid model / prompts to choose. `models-manager.ts`, `catalog-logic.ts`
+- ❌ **D7 NOT-A-BUG** (investigated, dismissed). Claim was that the `/health` result is computed then ignored. FALSE — the QA agent only read to setup.ts:279 and missed the return: `setup.ts:306 return { success: ok, … }` and the done message at :302 both gate on `ok`. A model that fails to load → `/health` never 200s → `ok=false` → `success=false` + "still warming up". No fix needed; recorded for honesty. (A deeper "verify by actually generating, not just /health" is a possible future enhancement, not this bug.)
+
+## Chat — streaming & send/resend lifecycle
+
+- 🔁 **D8 WRONG-CONVERSATION** — FIXED (needs on-device confirm). Send history was built from the ACTIVE tab's `messages`, not the target conversation → a drained-queue/background send got another conversation's context. Fix: `buildSendHistory(messagesByConv[convId], …)` (extracted, pure). Test: `chat-history.test.ts` (rule + source guard; red on HEAD). **On-device:** in conversation A ask something long; while it streams, queue a follow-up in A, switch to B; when A's queued send fires, A's reply must reference A's thread (not B's). `lib/chat-history.ts`, `MemoryChat.tsx`
+- 🔁 **D9 CROSS-CONVERSATION** — FIXED (needs on-device confirm). Image-gen UI (`generatingImage`/`imgProgress`) was global → an image forming in A showed its spinner + a Stop in whatever tab you switched to; and stop cleared globals unconditionally. Fix: track owning conversation (`imageGenConv`), derive `generatingImage = imageGenConv === activeConversationId`, and stop cancels the image job only when the conversation owns it. Test: `memorychat-image-gen-scope.test.ts` (source-contract guard; red on HEAD). **On-device (needs multi-tab harness in-process):** start an image gen in conversation A; while it forms, switch to conversation B → B shows NO image spinner/Stop and A's gen keeps running; switch back to A → progress still there. `MemoryChat.tsx`
+- 🔁 **D10 TRUNCATION** — FIXED (needs on-device confirm). `chatStream` ignored the caller's `maxTokens` (`this.maxTokens || maxTokens`) → capped at the setting. Fix: shared `resolveMaxTokens` (`requested ?? setting`) across chat/chatStream/streamChat. Test: `llm/__tests__/gen-params.test.ts` (rule + source-contract guard; red on HEAD). **On-device:** raise "max response length" → a long answer streams past the old 2048 cap. `llm/gen-params.ts`
+- 🔁 **D11 STUCK** — FIXED (needs on-device confirm). Stop during the pre-token classify didn't abort the blocking `llm.chat` → model stayed busy, next send blocked. Fix: `postCompletionOnce`/`httpPost`/`chat` take an AbortSignal; `classifyIntent` registers a controller under the turn's streamId so rag:cancel aborts it. Test: `http-post.integration.test.ts` (real abort → 'aborted', red-vs-'timed out'). **On-device:** hit Stop during "Searching your memory…" → the next message sends immediately (no multi-second wait behind the cancelled classify). NOTE: the image-prompt `llm.chat` (ipc.ts:502) is still un-aborted — minor (200-token call); logged as a follow-up. `llm/http-post.ts`, `ipc.ts`
+- 🔁 **D12 LOST-CONTENT** — FIXED (needs on-device confirm). A tool turn's cancelled image gen dropped the already-shown text answer (never persisted) → gone on reload. Fix: always persist the text answer in the image-gen catch; only the image is lost. Test: `MemoryChat.tool-image-cancel.test.tsx` (persisted-row; falsified). **On-device:** tools on, ask for something that draws an image, cancel the image mid-gen → the text answer stays and is still there after reopening the chat. `MemoryChat.tsx`
+- ❌ **D13 NOT-A-BUG** (investigated, dismissed). Claim: the cancel-partial finalize doesn't clear `activity`, stranding the label. FALSE — the activity/thinking block renders only while `message.role === 'assistant' && message.streaming` (`MemoryChat.tsx:1721`); every finalize sets `streaming: false`, so the stale `activity` field is never rendered. A test that "passed" did so because `streaming:false` hid the label, not the field-clear — a false-green, backed out per doctrine. Kept one genuine find: added `aria-label="Stop generating"` to the Stop button (it had no accessible name).
+- ⚠️ **D14 NEEDS-RUNTIME-REPRO** (partially investigated). The SQL memory path (`ipc.ts:615-649`) returns `[]` on empty tables (no throw) and falls back to FTS on vector failure, so a REAL fresh install appears to degrade gracefully (chat answers with no context). The known "All memory" error is the E2E blank-profile / RAG-seed gap (CLAUDE.md — needs `OFFGRID_SEED`), a demo-harness concern. **On-device:** on a genuine fresh install (no captures), ask in "All memory" scope → if it errors, capture the stderr and reopen this with the real throw site; if it answers, D14 is a demo-only artifact. Not fixed speculatively.
+
+## Chat — tools / MCP / modality
+
+- 🔁 **D15 ACTION-AFTER-CANCEL** — FIXED (needs on-device confirm). Stop mid-round still ran the assembled tool (incl. MCP writes — send message/create event), invisibly. Fix: `toolChat` returns immediately after a round if `signal.aborted`, before `runTool`. Test: `tools-abort.test.ts` (fake MCP tool's side effect never fires after abort; red on HEAD → green). **On-device:** connect an MCP write connector (Slack/Calendar), ask something that triggers its tool, hit Stop as it "thinks" → confirm NO message/event is actually created. `tools.ts:374+`
+- 🔁 **D16 WRONG/EMPTY** — FIXED (needs on-device confirm). Image attached to a text-only model was embedded as `image_url` unconditionally (no main-side guard). Fix: gate embed on `llm.hasVision()` in main (single source of truth). Test: `tools-vision-guard.test.ts` (crosses both vision values; no-vision → no image_url at the engine boundary; red on HEAD). **On-device:** with a text-only chat model active + Tools on, attach an image → the model answers text-only without erroring (no vision garbage). `tools.ts:346`
+- 🔁 **D17 SILENT-DROP** — FIXED (needs on-device confirm). A connector whose token expired / server is down silently lost its tools with no status change. Fix: the loader marks it `error` via `setConnectorStatus`. Test: `mcp-connector-status.dbtest.ts` (real DB; failed load → status 'error'; red on HEAD). **On-device:** connect a connector, revoke/expire its token, chat → the connector shows an error/reconnect state in Integrations (not a phantom "connected"). `mcpConnectorToolExtension.ts`, `mcp.ts`
+- 🔁 **D18 SLOW/STUCK** — FIXED (needs on-device confirm). Per-turn connector loads were serial + unbounded → one dead connector hung every turn. Fix: `fetchTools` races an 8s timeout; the loader fetches all connectors concurrently. **On-device:** with several connectors (one unreachable), a chat turn still starts promptly (≤~8s worst case, not a hang). `mcp.ts` (fetchTools), `mcpConnectorToolExtension.ts`
+- ⚠️ **D19 MINOR / SELF-RECOVERING** (noted, not fixed). The pre-job guard (`MemoryChat.tsx:937` `!cancelledRef.has(convId)`) skips the image job if cancel lands BEFORE it; only a cancel DURING the already-started image job leaves the LLM evicted → one re-warm on the next turn (a few seconds, self-recovering). QA rated it low-severity. Fixing needs coordinating the cancel with the IMAGE_JOB eviction — not worth the complexity for a rare, self-healing perf blip. Left open as a known minor.
+
+## Projects
+
+- 🔁 **D20 DATA-ORPHAN + BROKEN-PROMISE** — FIXED (needs on-device confirm). `deleteProject` swept the dead `project_threads` backend but never `rag_conversations` (where project chats live) nor artifacts → orphaned chats badged to a phantom project. Fix: `deleteProject` now also deletes `rag_conversations` + `rag_messages` (explicit — FKs are off) and calls `deleteArtifactsForProject`. Test: `project-delete-cascade.dbtest.ts` (chat+messages+artifact all gone; red on HEAD, falsified). **On-device:** make a project, chat in it (generate an artifact), delete the project → the chats vanish from the sidebar and the artifact is gone from the gallery. `store.ts:239`, `artifacts.ts`
+- 🔁 **D21 WRONG-ATTRIBUTION** — FIXED (needs on-device confirm). Send read live `activeProjectId` at each await → switching project mid-stream misattributed the artifact/image/RAG scope (cross-project leak). Fix: capture `projectId` at send-start (like convId), use it throughout the send body. Test: `memorychat-project-attribution.test.ts` (source guard; HEAD had 9 live reads → red). **On-device:** in project A, ask for something that generates an artifact; while it streams, switch the picker to project B → the artifact lands in A, not B. `MemoryChat.tsx`
+- 🔁 **D22 ARCH (SoC/DRY)** — FIXED (needs on-device confirm). Removed the dead `project_threads`/`project_messages` backend (projectChat + thread store fns + IPC handlers + 4 preload methods) — zero renderer callers; `getProjectChatHistory` no longer UNIONs the always-empty `project_messages`. One source of truth: `rag_conversations`. Verified: tsc×2 + production build + DB suite 65/65 + unit 2150 all green (behavior-neutral removal). **On-device:** project chat + project knowledge-base search still work (they always ran through rag_conversations); nothing referenced the removed path. `rag/store.ts`, `rag/index.ts`, `rag-ipc.ts`, `preload`, `database.ts`
+- 🔁 **D23 ORPHAN** — FIXED (needs on-device confirm). Deleting a conversation orphaned its rag_messages (FKs off) + artifacts. Fix: deleteRagConversation deletes rag_messages explicitly; handler calls deleteArtifactsForConversation. Test: `conversation-delete-cascade.dbtest.ts` (rows + artifact gone; falsified). **On-device:** in a chat, generate an artifact, then delete that conversation → the artifact is gone from the gallery. `database.ts`, `artifacts.ts`, `ipc.ts`
+- ❌ **D24 NOT-REACHABLE** (investigated, dismissed). The independent `useState` project snapshots are real, but the view router (`App.tsx:737`) renders ONE screen at a time via `viewMode === … ? : ` with `key={viewMode}` — so MemoryChat and ProjectsScreen never coexist, and switching UNMOUNTS the old + MOUNTS the target fresh (re-running `listProjects`). A stale snapshot can't survive a view switch (the only time the other screen's projects could change), so there's no user-visible cross-screen drift. Would become real only if the two were ever co-mounted. Recorded for honesty.
+- 🔁 **D25 LATENT (contract locked)** — getRagConversations tri-state (undefined=all / null=unscoped / id=scoped) had zero coverage. Not a live bug; locked with `rag-conversations-scope.dbtest.ts` (all 3 modes; falsified). No code change.
+
+## Model settings / activation / residency
+
+- 🔁 **D26 SILENT-LOST + DRY** — FIXED (needs on-device confirm). "Configure for me" passed `kind:'voice'` but `setActiveModalChoice` gated on `isModalKind` (only `'speech'`) → silently failed to activate TTS. Fix: `setActiveModalChoice` normalizes via `modalityForModel` (idempotent on `'speech'`), so `'voice'` and `'speech'` both work. Tests: `active-models.test.ts` (`modalityForKind('speech')==='speech'`, red on HEAD) + `set-active-modal-choice.test.ts` (no isModalKind gate). **On-device:** fresh profile → "Configure for me" → after it completes, Kokoro/TTS shows Active in Models and voice output works. `models-manager.ts`, `active-models.ts`
+- ❌ **D27 ALREADY-FIXED** (investigated, dismissed). Claim: an in-flight Steps edit is stomped by the model-change re-seed effect. Already addressed by T1b: `setStepsOverride` (`MemoryChat.tsx:572`) writes the value to `imgParamStore` ATOMICALLY with the local `setImgSteps`, and the re-seed effect reads `resolveImageParams(imgModel, imgParamStore)` which honors the per-model override — so a model switch preserves each model's steps and switching back restores them. No stomp. (The only residual is a negligible sub-second mount race: typing in the steps input before the initial getSettings load resolves; not worth a guard.) Verified against `lib/image-params.ts` + `image-params-wiring.test.ts`.
+- ❌ **D28 NOT-REACHABLE** (investigated, dismissed). The locked `llm` row's display uses `row.locked || modes[...]` (`Settings.tsx:146`), shows `row.locked ? 'in-memory (required)'` (:155), and is `disabled={row.locked}` (:162) — so it ALWAYS renders in-memory + disabled regardless of a stale `modes.llm`. No user-visible desync; the `row.locked` short-circuit IS the client-side lock. Would only matter if that short-circuit were removed (the QA flagged it latent). Recorded for honesty.
+
+## Settings surface
+
+- 🔁 **D29 DATA-LOSS / PRIVACY** — FIXED (needs on-device confirm). "Delete all my data" cleared only CHAT_TABLES + MEMORY_TABLES + user_profile, so the capture corpus (observations, frames, entity_aliases, secretary_prefs, action_items, clipboard_items, day_journals, voice_recordings…) + rag_documents/chunks survived. Fix: a `personalStores` registry (core) that pro extends via `registerProPersonalData()` in activateMain — single source of truth, open-core clean. Tests: `data-privacy-delete-all.dbtest.ts` (core: rag docs gone), `pro/main/__tests__/personal-data.integration.test.ts` (pro: observations gone). **On-device:** seed real captures/clips, "Delete all my data", confirm Day/Entities/Clipboard/Search all empty. `data-privacy.ts` + `pro/main/personal-data.ts`
+- 🔁 **D30 PRIVACY / SECURITY** — FIXED (needs on-device confirm). "Delete all" never cleared `secrets` (OAuth tokens) or `connectors` → live third-party credentials remained. Now both are in the core registry. Test: `data-privacy-delete-all.dbtest.ts` (connectors + secrets → 0 after wipe; red on HEAD, falsified). **On-device:** connect an OAuth connector, "Delete all my data", confirm the connector is gone and reconnect requires fresh auth. `data-privacy.ts`
+- ⏳ **D31 ARCH (CONFIRMED — deferred to a dedicated dual-build pass)** — Pro sections `ProactiveSection`/`SecretaryPrefs`/`ProPlanSection` (+ PlanInfo/PlanDevice, ~230 lines) are defined & rendered inline in CORE gated by `isPro`; `registerProSettings` is a stub and core never consumes `getRegisteredSettingsSections()`. Real open-core violation (pro logic in the public repo). All 3 use only `window.api`, so the MOVE is mechanical — but the correct wiring + verification is not, and getting open-core wrong is high-cost. PLAN: (1) new `pro/renderer/settings-sections.tsx` — the 3 components, each self-wrapping in `SettingsCard` (title+summary), exported as registerable sections with ids `proactive`/`secretary`/`pro-plan`; (2) `registerProSettings` registers them via `api.registerSettingsSection`; (3) core `Settings.tsx` deletes the 3 inline components + the `isPro ? : ` blocks, and instead for each pro SLOT renders the registered section if present else a `ProPlaceholder` (slot→registered-or-placeholder); (4) VERIFY BOTH: `OFFGRID_PRO=0` electron-vite build → placeholders, no pro import in core bundle; pro build → sections registered (check `registerProSettings` runs in the pro renderer bootstrap before Settings mounts) + render. Deferred because that dual-build/no-leak confidence bar isn't reachable in the current deep session — it deserves a fresh, dedicated pass.
+- ⚠️ **D32 MISSING-FEATURE (needs product decision)** — `memoryStrictness`/`entityStrictness` are plumbed, read (`ipc.ts:205,262`, default `balanced`) and persist correctly (guarded by `database-integration.dbtest.ts:344`), but no renderer control surfaces them → stuck at `balanced`. NOT a bug (the default works + saveSetting round-trips); it's an unexposed knob. Surfacing it is a feature-add needing a product/design decision (where in Settings, labels, whether to expose at all) — not a fix. Left open as a feature request.
+- ❌ **D33 NOT-A-BUG** (investigated, dismissed). Claim: `resetDefaults` diverges shown-vs-used because it sets `{...prev, ...DEFAULTS}` locally but persists `DEFAULTS` alone. FALSE — `llm.setSettings` is PATCH-MERGE (`llm.ts` applies each present field, leaves absent ones unchanged; that's why per-field `update()` works). So the DEFAULTS patch resets its 13 fields on the backend and leaves `performanceMode` (absent from DEFAULTS) unchanged — exactly matching the local `{...prev,...DEFAULTS}` which also keeps `prev.performanceMode`. No divergence. (Whether "Reset to defaults" _should_ also reset performanceMode is a separate minor product choice — it has its own mode-preset control.)
+- 🔁 **D34 PANEL-DESYNC §A** — FIXED (needs on-device confirm). Optimistic toggles (proactive/residency/auto-update/beta/tool-enable/TTS-voice) did setX + fire-and-forget persist → on a (rare) persist failure the control lied then flipped back on next mount. Fix: shared `persistToggle` reverts to prev if the persist rejects. Test: `persist-toggle.test.ts` (revert contract; falsified). **On-device:** hard to trigger (needs a persist failure); low-severity. `lib/persist-toggle.ts`
+- ⏳ **D35 RACE (low-severity, deferred with plan)** — delete-all/clearCategory don't pause the capture pipeline. Assessed: SQLite serializes writes (no mid-statement interleaving — capture just accrues NEW rows after the wipe, not "resurrected" ones), and an in-flight vector read holds its own resolved lancedb handle (resetVectors only nulls the cache; a new read reopens). The one real transient is a concurrent vector read during the ~ms `clearDirs(lancedb)` window → self-recovers on reopen. Low severity on a rare deliberate action. Correct fix is cross-cutting + open-core-sensitive: core `deleteAllData` can't reach pro's capture loop, so add a `callHook('data:before-wipe')`/`'data:after-wipe')` seam that pro registers to pause+resume capture around the wipe — needs live capture verification, so deferred (same confidence-bar reason as D31).
+
+---
+
+## Cross-cutting themes (for the fix pass)
+
+- **Cancel/Stop is not honored at the boundary** (D11, D12, D15, D19): abort sets a renderer flag but the main-process job keeps running — tools fire, models stay busy, content is lost. Fix at the seam: thread the AbortSignal all the way and check it before every side-effect.
+- **Global state where it should be per-conversation** (D8, D9, D13): history + loading/progress are read/written globally, so a second conversation corrupts the first. Fix: key them by `convId`, capture `activeProjectId` into the send closure like `convId` already is (D21).
+- **"UI keeps its own COPY of authoritative data" (§A)** (D24, D27, D28, D33, D34): re-seed effects and optimistic setX drift from the owning source. Fix: read reactively + write through; a per-render default is a pure function of the source, not a stored duplicate.
+- **DRY breaks across main/renderer or code/schema** (D22, D26, D28, D29): a rule/key/table-list defined twice drifts. Fix: one source of truth, imported by both sides (delete-all should derive from the schema, not a hand-listed subset).
+- **Data-privacy correctness** (D29, D30, D35): the single most user-critical class — a "wipe" that doesn't wipe, and leftover credentials. Fix + a test that asserts EVERY personal table/store is empty after delete-all.
+- **Open-core** (D31): pro logic in core + a dead registry seam.
+
+## Related
+
+- `docs/RELEASE_TEST_CHECKLIST.md` — the branch's intended-change manual checklist (separate from this broken-flow log).
+- `docs/GAPS_BACKLOG.md` — will get a linked entry per bug as it's picked up.
diff --git a/docs/ENTERPRISE_BUILD_PLAN.md b/docs/ENTERPRISE_BUILD_PLAN.md
index 41a0ddf8..c15697ae 100644
--- a/docs/ENTERPRISE_BUILD_PLAN.md
+++ b/docs/ENTERPRISE_BUILD_PLAN.md
@@ -6,15 +6,15 @@ This plan **inherits the entire 5-plane agentic architecture** from the stack na
Physical plane, is accounted for below and mapped to Off Grid.
The navigator was written for a multi-tenant regulated bank on cloud/on-prem. Off Grid is
-**local-first, on-device, single-org-per-deployment, on-prem.** That changes *how* each
-layer is realized, not *whether* it exists. Six rules drive every mapping:
+**local-first, on-device, single-org-per-deployment, on-prem.** That changes _how_ each
+layer is realized, not _whether_ it exists. Six rules drive every mapping:
1. **The work is the data source.** The "data plane" is capture (screen / messages / calls
on org time) + optional connectors — not a cloud data lake.
2. **The substrate is distributed.** Data lands per-device (SQLite) plus a thin org **Brain**
that holds only distilled knowledge — not one central lake.
3. **The AI plane came first.** On a laptop there's no model serving unless we build it, so
- we built it first (`model-server.ts`). The control plane *wraps* the AI plane we already
+ we built it first (`model-server.ts`). The control plane _wraps_ the AI plane we already
have — the inverse of the bank's "gateway first" order.
4. **Worker owns raw; org sees distilled.** Raw capture never leaves the worker's device by
default. Only distilled SOPs/patterns go up. This is the access-control spine **and** the
@@ -37,8 +37,8 @@ data never leaves your control.
**But the wedge is different, and it's ours.** Those players sell top-down into compliance,
back-office, document workflows. We land **bottom-up through a frontline/sales-productivity
-tool people actually want** — the on-device node that helps the worker *and* observes the
-work. The control plane rides in *with* the node. So we don't have to win a 12-month
+tool people actually want** — the on-device node that helps the worker _and_ observes the
+work. The control plane rides in _with_ the node. So we don't have to win a 12-month
compliance procurement to get installed; the productivity tool gets us in, and the common
control plane is what we already are once we're there. The frontline use case is the
distribution mechanism for the control-plane vision — not a separate bet.
@@ -47,14 +47,14 @@ distribution mechanism for the control-plane vision — not a separate bet.
## The six products (who owns what)
-| Product | Role | In navigator terms |
-|---|---|---|
-| **Personal AI** (Desktop + Mobile) | Each person's private copilot, memory, and capture. The worker's benefit. | Consumption (D) + local Data (A) + local AI plane (B) |
-| **Gateway** (`:7878`, per device) | Routes every model/tool call. Controls egress. Logs everything. | Control plane (C) + AI-plane serving (B15/B7) |
-| **Brain** (org) | The distilled org knowledge — SOPs, patterns, "what good is." Serves it back. | AI-plane substrate (B3/B5a/B11) + Data landing (A10) |
-| **Fleet Control** (on-prem admin **app**) | **MDM for AI.** Connects to the fleet of nodes. Enrolls them, provisions policy + knowledge + intelligence config down, pulls audit + telemetry + distilled learnings up, renders the DPO/compliance view. Does **not** run the intelligence or enforce policy itself — the nodes do. Never cloud. | Control plane (C) *define/observe* + Regulatory (E) |
-| **Sync** (EasyShare) | Desktop ↔ mobile, device ↔ org transport. | cross-cutting transport |
-| **Learning loop** (batch) | Observe → find patterns → write SOPs → grade quality. | Consumption flywheel (D8) + AI orchestration (D1) |
+| Product | Role | In navigator terms |
+| ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- |
+| **Personal AI** (Desktop + Mobile) | Each person's private copilot, memory, and capture. The worker's benefit. | Consumption (D) + local Data (A) + local AI plane (B) |
+| **Gateway** (`:7878`, per device) | Routes every model/tool call. Controls egress. Logs everything. | Control plane (C) + AI-plane serving (B15/B7) |
+| **Brain** (org) | The distilled org knowledge — SOPs, patterns, "what good is." Serves it back. | AI-plane substrate (B3/B5a/B11) + Data landing (A10) |
+| **Fleet Control** (on-prem admin **app**) | **MDM for AI.** Connects to the fleet of nodes. Enrolls them, provisions policy + knowledge + intelligence config down, pulls audit + telemetry + distilled learnings up, renders the DPO/compliance view. Does **not** run the intelligence or enforce policy itself — the nodes do. Never cloud. | Control plane (C) _define/observe_ + Regulatory (E) |
+| **Sync** (EasyShare) | Desktop ↔ mobile, device ↔ org transport. | cross-cutting transport |
+| **Learning loop** (batch) | Observe → find patterns → write SOPs → grade quality. | Consumption flywheel (D8) + AI orchestration (D1) |
---
@@ -65,18 +65,18 @@ the personal AI, capture, the learning loop, and **enforces its own policy — e
Fleet Control is the **app that connects to the fleet** to define and observe; it does not
run the intelligence or enforce policy itself. Every control has two halves:
-| Control | Node — *enforce / emit* | Fleet Control — *define / observe* |
-|---|---|---|
-| Gateway (C1) | routes & runs locally | — |
-| Input/output policy (C2/C3) | enforces locally | authors the rules |
-| Egress gate (C16) | blocks/redacts on-device | sets what's allowed out |
-| Audit (C7) | writes every call locally | aggregates + DPO view + export (E1/E6) |
-| Observability (C8) | emits traces | fleet dashboards |
-| RBAC (C5) | enforces on-device | authors who-sees-what |
-| Identity (C4) | holds its device token | issues tokens, enrolls devices |
-| Kill switch (C9c) | executes the halt | triggers it across the fleet |
-| Eval / drift (C9/C9a) | runs local checks | fleet-wide gates + rollup |
-| Intelligence config | runs the SOPs / models / agents it was given | **provisions which intelligence each role gets** (from Brain) |
+| Control | Node — _enforce / emit_ | Fleet Control — _define / observe_ |
+| --------------------------- | -------------------------------------------- | ------------------------------------------------------------- |
+| Gateway (C1) | routes & runs locally | — |
+| Input/output policy (C2/C3) | enforces locally | authors the rules |
+| Egress gate (C16) | blocks/redacts on-device | sets what's allowed out |
+| Audit (C7) | writes every call locally | aggregates + DPO view + export (E1/E6) |
+| Observability (C8) | emits traces | fleet dashboards |
+| RBAC (C5) | enforces on-device | authors who-sees-what |
+| Identity (C4) | holds its device token | issues tokens, enrolls devices |
+| Kill switch (C9c) | executes the halt | triggers it across the fleet |
+| Eval / drift (C9/C9a) | runs local checks | fleet-wide gates + rollup |
+| Intelligence config | runs the SOPs / models / agents it was given | **provisions which intelligence each role gets** (from Brain) |
**Fleet Control pushes down:** policy + knowledge (from Brain) + intelligence config.
**Fleet Control pulls up:** audit + telemetry + distilled learnings.
@@ -86,15 +86,15 @@ Enforcement is node-local by design — a node governs itself with or without a
The system **doubles as both** a work-observation layer and an org-data ingestion layer. Two
sources feed Brain, so the intelligence baked into nodes is grounded in **what people do
-*and* the org's real data** — not observation alone:
+_and_ the org's real data** — not observation alone:
-1. **Observed work (push, from nodes).** Capture on org time → distilled learnings flow *up*
- from devices. Worker-owns-raw; only the distilled layer leaves the device. *(A1 capture.)*
+1. **Observed work (push, from nodes).** Capture on org time → distilled learnings flow _up_
+ from devices. Worker-owns-raw; only the distilled layer leaves the device. _(A1 capture.)_
2. **Org digital data (pull, via connectors).** An **org-side ingest service** pulls from
databases, warehouses, SaaS, and document stores → ETL → PII mask → index into Brain.
This is org-owned data by definition; access is governed by A11/C5. Runs next to Brain on
- the org's infra (warehouses are org infra), behind the same policy. *(A1 connectors, A3
- CDC, A3a contracts, A7/A9 mask.)*
+ the org's infra (warehouses are org infra), behind the same policy. _(A1 connectors, A3
+ CDC, A3a contracts, A7/A9 mask.)_
```
Org systems (DBs · warehouses · SaaS · docs) Nodes (Desktop / Mobile)
@@ -113,18 +113,18 @@ sources feed Brain, so the intelligence baked into nodes is grounded in **what p
> Bank version: get data out of source systems, prep, govern, land. Off Grid version: the
> work itself is the source; it lands on-device and (distilled) in Brain.
-| # | Navigator component | Off Grid realization | Owner | Status |
-|---|---|---|---|---|
-| A1 | Source systems | **Two first-class sources.** (1) **Capture** (screen→OCR, messages, calls) — the work itself, on the node. (2) **Org digital data** — databases, warehouses, SaaS, document stores — via connectors on the org-side ingest service. | Personal AI capture · Brain-side connectors | ✅ capture (`watcher.ts`, `vision.ts`, `ocr.swift`, meeting recorder); ❌ enterprise connectors build |
-| A3 | CDC / ingestion | Node: continuous on-device ingest of capture. Org: CDC/connector pulls from DBs & warehouses → ETL → mask → Brain. | Personal AI ingest · Brain ingest service | ✅ `watcher.ts`, `ingest.ts`; ❌ org ingest service |
-| A3a | Schema registry / contracts | The observation/entity schema — stable contract for what capture emits. | Brain · ingest | ✅ `crm/schema.ts` |
-| A5 | Data catalog | Index of what's known — entities, sources, where each came from. | Brain | ⚠️ partial (`EntityGraph`, `database.ts`) |
-| A7 | PII discovery + classification | Detect & tag PII in captured content. **Critical** — capture sees everything on screen. On-device (Presidio-class). | Gateway input policy · ingest | ❌ build |
-| A7a | Consent management | Per-device, per-purpose opt-in; the **"on org time" boundary**; visible recording indicator. | Fleet Control policy · Personal AI | ⚠️ consumer opt-in pattern exists; org policy build |
-| A9 | PII masking + synthetic | Redact PII **before anything leaves the device**. Same engine the egress gate uses. | Gateway egress (C16) | ❌ build |
-| A10 | Data lake (zones) | No cloud lake. **Per-device SQLite** (raw, stays local) + **Brain** (distilled, org-shared). The zone boundary is the device edge. | Personal AI · Brain | ✅ SQLite; ❌ Brain build |
-| A11 | Fine-grained access | Who in the org sees which distilled knowledge. Enforces **worker-owns-raw / org-sees-distilled**. | Fleet Control · Brain | ❌ build |
-| A12a | Retention + erasure | "Delete this person" across device + Brain + memory. File-based markdown memory makes erasure `git revert`, not a vector rebuild (navigator's own note). | Fleet Control · Brain | ❌ build |
+| # | Navigator component | Off Grid realization | Owner | Status |
+| ---- | ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
+| A1 | Source systems | **Two first-class sources.** (1) **Capture** (screen→OCR, messages, calls) — the work itself, on the node. (2) **Org digital data** — databases, warehouses, SaaS, document stores — via connectors on the org-side ingest service. | Personal AI capture · Brain-side connectors | ✅ capture (`watcher.ts`, `vision.ts`, `ocr.swift`, meeting recorder); ❌ enterprise connectors build |
+| A3 | CDC / ingestion | Node: continuous on-device ingest of capture. Org: CDC/connector pulls from DBs & warehouses → ETL → mask → Brain. | Personal AI ingest · Brain ingest service | ✅ `watcher.ts`, `ingest.ts`; ❌ org ingest service |
+| A3a | Schema registry / contracts | The observation/entity schema — stable contract for what capture emits. | Brain · ingest | ✅ `crm/schema.ts` |
+| A5 | Data catalog | Index of what's known — entities, sources, where each came from. | Brain | ⚠️ partial (`EntityGraph`, `database.ts`) |
+| A7 | PII discovery + classification | Detect & tag PII in captured content. **Critical** — capture sees everything on screen. On-device (Presidio-class). | Gateway input policy · ingest | ❌ build |
+| A7a | Consent management | Per-device, per-purpose opt-in; the **"on org time" boundary**; visible recording indicator. | Fleet Control policy · Personal AI | ⚠️ consumer opt-in pattern exists; org policy build |
+| A9 | PII masking + synthetic | Redact PII **before anything leaves the device**. Same engine the egress gate uses. | Gateway egress (C16) | ❌ build |
+| A10 | Data lake (zones) | No cloud lake. **Per-device SQLite** (raw, stays local) + **Brain** (distilled, org-shared). The zone boundary is the device edge. | Personal AI · Brain | ✅ SQLite; ❌ Brain build |
+| A11 | Fine-grained access | Who in the org sees which distilled knowledge. Enforces **worker-owns-raw / org-sees-distilled**. | Fleet Control · Brain | ❌ build |
+| A12a | Retention + erasure | "Delete this person" across device + Brain + memory. File-based markdown memory makes erasure `git revert`, not a vector rebuild (navigator's own note). | Fleet Control · Brain | ❌ build |
---
@@ -132,17 +132,17 @@ sources feed Brain, so the intelligence baked into nodes is grounded in **what p
> The engine. This is largely **already built** — it's what `model-server.ts` is.
-| # | Navigator component | Off Grid realization | Owner | Status |
-|---|---|---|---|---|
-| B1 | Doc parsing + chunking | OCR + audio transcription + doc extractors. | Gateway (AI plane) | ✅ `rag/extractors`, whisper, vision |
-| B2a | Reranking + hybrid search | Retrieval over captured + Brain content; BM25 + vector + rerank. | Gateway | ⚠️ embeddings exist; hybrid + rerank build |
-| B3 | Vector store / KB index | On-device index (personal) + Brain index (org). | Personal AI · Brain | ⚠️ embeddings + SQLite; dedicated index build |
-| B5a | Provenance + citation | **Every SOP/answer traces to the captured source** (which screen, which call, when). The auditability beam. *(This is the "SANN-equivalent.")* | Brain · Gateway output policy | ❌ build |
-| B7 | Tool layer (MCP) | `/mcp` — on-device models + actions + org connectors as scoped, audited tools. | Gateway | ✅ `mcp-server.ts`, extend with scope+audit |
-| B9 | Sandboxed code execution | Agents run untrusted code in isolation (microVM), never the host. | Gateway | ❌ build (later) |
-| B11 | Memory (sidecar, 4 flavors) | Exactly Off Grid's memory: short-term, long-term vector, entity graph, file-based markdown. **Personal memory** + **org memory (Brain)**. | Personal AI · Brain | ✅ strong (`crm/*`, observations, memory) |
-| B15 | Model serving / inference | Bundled llama-server, whisper, TTS, diffusion — unified at `:7878`. | Gateway | ✅ strong (`model-server.ts`) |
-| B16 | Fine-tuning + privacy ML | Adapt the local SLM to the org's domain & SOPs (LoRA), on-device or in Brain. | Brain | ❌ build (later) |
+| # | Navigator component | Off Grid realization | Owner | Status |
+| --- | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- | --------------------------------------------- |
+| B1 | Doc parsing + chunking | OCR + audio transcription + doc extractors. | Gateway (AI plane) | ✅ `rag/extractors`, whisper, vision |
+| B2a | Reranking + hybrid search | Retrieval over captured + Brain content; BM25 + vector + rerank. | Gateway | ⚠️ embeddings exist; hybrid + rerank build |
+| B3 | Vector store / KB index | On-device index (personal) + Brain index (org). | Personal AI · Brain | ⚠️ embeddings + SQLite; dedicated index build |
+| B5a | Provenance + citation | **Every SOP/answer traces to the captured source** (which screen, which call, when). The auditability beam. _(This is the "SANN-equivalent.")_ | Brain · Gateway output policy | ❌ build |
+| B7 | Tool layer (MCP) | `/mcp` — on-device models + actions + org connectors as scoped, audited tools. | Gateway | ✅ `mcp-server.ts`, extend with scope+audit |
+| B9 | Sandboxed code execution | Agents run untrusted code in isolation (microVM), never the host. | Gateway | ❌ build (later) |
+| B11 | Memory (sidecar, 4 flavors) | Exactly Off Grid's memory: short-term, long-term vector, entity graph, file-based markdown. **Personal memory** + **org memory (Brain)**. | Personal AI · Brain | ✅ strong (`crm/*`, observations, memory) |
+| B15 | Model serving / inference | Bundled llama-server, whisper, TTS, diffusion — unified at `:7878`. | Gateway | ✅ strong (`model-server.ts`) |
+| B16 | Fine-tuning + privacy ML | Adapt the local SLM to the org's domain & SOPs (LoRA), on-device or in Brain. | Brain | ❌ build (later) |
---
@@ -151,21 +151,21 @@ sources feed Brain, so the intelligence baked into nodes is grounded in **what p
> The gateway spine. Wraps the AI plane we already have. **This is the bulk of the new
> build**, and where Fleet Control plugs in.
-| # | Navigator component | Off Grid realization | Owner | Status |
-|---|---|---|---|---|
-| C1 | AI gateway | `:7878` becomes a real chokepoint: routing local + **leashed cloud** (rule 5). | Gateway | ⚠️ started — single local model, add routing |
-| C2 | Input policy / guardrails | Injection scan — **especially of captured content** (hostile indirect input). | Gateway pre-hook | ❌ build |
-| C3 | Output policy + grounding | No ungrounded SOP ships — must cite a real observation (B5a) or it's blocked/flagged. | Gateway post-hook | ❌ build |
-| C4 | Identity + token issuance | Per-device identity (collapses for one user, **re-emerges for the fleet**). | Fleet Control | ❌ build |
-| C5 | RBAC / ABAC | Who sees which distilled knowledge; **enforces worker-owns-raw** (rule 4). | Fleet Control · Gateway | ❌ build |
-| C7 | Audit log + lineage | Every model/tool call + **what left the device**. The DPO's single evidence stream. | Gateway → Fleet Control | ❌ build — **START HERE** |
-| C8 | Observability + tracing | Tokens, latency, full trace per person/feature; replay any answer. | Gateway → Fleet Control | ❌ build |
-| C9 | Eval + red teaming | Quality gate on the local model **and** on SOP quality. Drift checks. | Fleet Control · Brain | ❌ build |
-| C9a | Bias + fairness | For consequential frontline decisions. | Fleet Control | ❌ build (later) |
-| C9c | Incident response + runbooks | **Kill switch** ("stop all AI / pause capture") + runbooks. | Fleet Control | ❌ build — kill switch early |
-| C14 | FinOps + cost | Per-device compute/battery budget; $ only matters once cloud fallback is on. | Fleet Control | ❌ build (light) |
-| C16 | DLP + exfil prevention | **The egress gate** — what may leave to a cloud model / connector / other device. The privacy guarantee (rule 5). | Gateway | ❌ build — **START HERE** |
-| C21 | Durable execution | Long agent runs (a batch SOP-mining job over a week of capture) resume, not restart. | Gateway / runtime | ❌ build (later) |
+| # | Navigator component | Off Grid realization | Owner | Status |
+| --- | ---------------------------- | ----------------------------------------------------------------------------------------------------------------- | ----------------------- | -------------------------------------------- |
+| C1 | AI gateway | `:7878` becomes a real chokepoint: routing local + **leashed cloud** (rule 5). | Gateway | ⚠️ started — single local model, add routing |
+| C2 | Input policy / guardrails | Injection scan — **especially of captured content** (hostile indirect input). | Gateway pre-hook | ❌ build |
+| C3 | Output policy + grounding | No ungrounded SOP ships — must cite a real observation (B5a) or it's blocked/flagged. | Gateway post-hook | ❌ build |
+| C4 | Identity + token issuance | Per-device identity (collapses for one user, **re-emerges for the fleet**). | Fleet Control | ❌ build |
+| C5 | RBAC / ABAC | Who sees which distilled knowledge; **enforces worker-owns-raw** (rule 4). | Fleet Control · Gateway | ❌ build |
+| C7 | Audit log + lineage | Every model/tool call + **what left the device**. The DPO's single evidence stream. | Gateway → Fleet Control | ❌ build — **START HERE** |
+| C8 | Observability + tracing | Tokens, latency, full trace per person/feature; replay any answer. | Gateway → Fleet Control | ❌ build |
+| C9 | Eval + red teaming | Quality gate on the local model **and** on SOP quality. Drift checks. | Fleet Control · Brain | ❌ build |
+| C9a | Bias + fairness | For consequential frontline decisions. | Fleet Control | ❌ build (later) |
+| C9c | Incident response + runbooks | **Kill switch** ("stop all AI / pause capture") + runbooks. | Fleet Control | ❌ build — kill switch early |
+| C14 | FinOps + cost | Per-device compute/battery budget; $ only matters once cloud fallback is on. | Fleet Control | ❌ build (light) |
+| C16 | DLP + exfil prevention | **The egress gate** — what may leave to a cloud model / connector / other device. The privacy guarantee (rule 5). | Gateway | ❌ build — **START HERE** |
+| C21 | Durable execution | Long agent runs (a batch SOP-mining job over a week of capture) resume, not restart. | Gateway / runtime | ❌ build (later) |
---
@@ -173,14 +173,14 @@ sources feed Brain, so the intelligence baked into nodes is grounded in **what p
> Where humans meet it. Mostly **already built** on the Personal AI side.
-| # | Navigator component | Off Grid realization | Owner | Status |
-|---|---|---|---|---|
-| D1 | Agent runtime / orchestration | The agents that do the work + the **learning-loop batch jobs** (observe→SOP). | Personal AI · Brain | ⚠️ partial (`crm/agent.ts`); loop build |
-| D2 | Human-in-the-loop | Approval-gated actions on anything consequential. | Personal AI | ✅ `crm/approvals.ts` |
-| D3 | Conversational + generative UI | The copilot UI, desktop-first. | Personal AI | ✅ React app |
-| D3b | Trust indicators | Citation chips — **"why this SOP / where it was observed"**, confidence. | Personal AI · Fleet Control | ❌ build |
-| D4 | Voice + telephony | Capture & understand calls (your explicit ask — phone, meetings). | Personal AI capture | ⚠️ meeting recorder + whisper; extend |
-| D8 | Feedback + data flywheel | Thumbs/corrections on SOPs feed eval + Brain. Start day one. | Personal AI → Brain | ❌ build |
+| # | Navigator component | Off Grid realization | Owner | Status |
+| --- | ------------------------------ | ----------------------------------------------------------------------------- | --------------------------- | --------------------------------------- |
+| D1 | Agent runtime / orchestration | The agents that do the work + the **learning-loop batch jobs** (observe→SOP). | Personal AI · Brain | ⚠️ partial (`crm/agent.ts`); loop build |
+| D2 | Human-in-the-loop | Approval-gated actions on anything consequential. | Personal AI | ✅ `crm/approvals.ts` |
+| D3 | Conversational + generative UI | The copilot UI, desktop-first. | Personal AI | ✅ React app |
+| D3b | Trust indicators | Citation chips — **"why this SOP / where it was observed"**, confidence. | Personal AI · Fleet Control | ❌ build |
+| D4 | Voice + telephony | Capture & understand calls (your explicit ask — phone, meetings). | Personal AI capture | ⚠️ meeting recorder + whisper; extend |
+| D8 | Feedback + data flywheel | Thumbs/corrections on SOPs feed eval + Brain. Start day one. | Personal AI → Brain | ❌ build |
---
@@ -188,19 +188,19 @@ sources feed Brain, so the intelligence baked into nodes is grounded in **what p
> Functions, not just tools. Realized mostly inside **Fleet Control** (the DPO's product).
-| # | Navigator component | Off Grid realization | Owner | Status |
-|---|---|---|---|---|
-| E1 | Framework mapping | Map controls → DPDP/etc clauses. **The DPO single compliant view + one-click export.** | Fleet Control | ❌ build |
-| E2 | AI use policy | What staff may do; authored and **pushed to every device** by Fleet Control. | Fleet Control | ❌ build |
-| E5 | Ethics / review board | Process; supported by Fleet Control's audit + eval evidence. | Fleet Control (workflow) | process |
-| E6 | DPIA / FRIA | Per use case; Fleet Control **generates the assessment pack** from the audit stream. | Fleet Control | ❌ build |
+| # | Navigator component | Off Grid realization | Owner | Status |
+| --- | --------------------- | -------------------------------------------------------------------------------------- | ------------------------ | -------- |
+| E1 | Framework mapping | Map controls → DPDP/etc clauses. **The DPO single compliant view + one-click export.** | Fleet Control | ❌ build |
+| E2 | AI use policy | What staff may do; authored and **pushed to every device** by Fleet Control. | Fleet Control | ❌ build |
+| E5 | Ethics / review board | Process; supported by Fleet Control's audit + eval evidence. | Fleet Control (workflow) | process |
+| E6 | DPIA / FRIA | Per use case; Fleet Control **generates the assessment pack** from the audit stream. | Fleet Control | ❌ build |
---
## Phase 0 — PHYSICAL PLANE
-| Navigator | Off Grid realization | Status |
-|---|---|---|
+| Navigator | Off Grid realization | Status |
+| -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
| GPUs / nodes / power / K8s | The **devices themselves** (laptops, phones) run inference on-device. Optional **on-prem org server** hosts Fleet Control + Brain. No hyperscaler in the path. | deployment |
---
@@ -211,17 +211,17 @@ The navigator's "pick the gateway first" inverts for us — the AI plane (B) is
so Stage 1 is **wrapping it with control (C7 + C16)**, then standing up Fleet Control.
- **Stage 1 — Control the chokepoint.** C1 routing · **C7 audit log** · **C16 egress gate** ·
- C9c kill switch. Pre/post hook pipeline in `model-server.ts`. *Done when every call is
- logged and nothing leaves the device unless policy allows.*
+ C9c kill switch. Pre/post hook pipeline in `model-server.ts`. _Done when every call is
+ logged and nothing leaves the device unless policy allows._
- **Stage 2 — Fleet Control foundation.** C4 identity · enroll a device · push policy (E2)
- down · pull audit (C7) up · the DPO view (E1) v0. On-prem. *Done when an admin can govern
- a device from one console.*
+ down · pull audit (C7) up · the DPO view (E1) v0. On-prem. _Done when an admin can govern
+ a device from one console._
- **Stage 3 — The learning loop.** D1 batch orchestration · A7/A9 PII tag+mask on ingest ·
- B5a provenance · SOP/pattern synthesis (D8 flywheel). *Done when the system writes a cited
- SOP from a week of observed work.*
+ B5a provenance · SOP/pattern synthesis (D8 flywheel). _Done when the system writes a cited
+ SOP from a week of observed work._
- **Stage 4 — Brain.** B3 org index · A10 distilled store · A11/C5 access control · push SOPs
- back to every device (D3b trust indicators at point of work). *Done when "what good is"
- flows back to everyone, worker-owns-raw enforced.*
+ back to every device (D3b trust indicators at point of work). _Done when "what good is"
+ flows back to everyone, worker-owns-raw enforced._
- **Stage 5 — Hardening.** C8 observability · C9/C9a eval+bias gates · C21 durable runs ·
A12a erasure · E6 DPIA packs · Sync at org scale · mobile parity.
diff --git a/docs/FEATURES.md b/docs/FEATURES.md
index d812de1d..a6348cca 100644
--- a/docs/FEATURES.md
+++ b/docs/FEATURES.md
@@ -7,24 +7,24 @@ device** — no cloud inference, no account, no API key. Each feature has its ow
## The studio
-| Feature | What it does |
-|---|---|
-| [The Gateway](features/gateway.md) | One local OpenAI-compatible API for every model — chat, vision, image, audio, embeddings. |
-| [Chat](features/chat.md) | Text + vision + reasoning, streaming, tabs, tools, voice mode, project scoping. |
-| [Image generation](features/image-generation.md) | Text→image and image→image (SDXL GGUFs), styles, LoRA, live previews. |
-| [Voice & speech](features/voice.md) | Speech→text (whisper) and text→speech (Kokoro), hands-free voice mode. |
-| [Projects (RAG)](features/projects.md) | Group chats, upload docs, chat grounded in them with cited retrieval. |
-| [Artifacts](features/artifacts.md) | HTML / React / SVG / Mermaid / Markdown rendered in a sandboxed canvas. |
-| [Connectors (MCP)](features/connectors.md) | Add Model Context Protocol servers (none / token / OAuth), use them in chat. |
-| [Models](features/models.md) | Curated, size-bucketed catalog + Hugging Face search; per-modality active model. |
+| Feature | What it does |
+| ------------------------------------------------ | ----------------------------------------------------------------------------------------- |
+| [The Gateway](features/gateway.md) | One local OpenAI-compatible API for every model — chat, vision, image, audio, embeddings. |
+| [Chat](features/chat.md) | Text + vision + reasoning, streaming, tabs, tools, voice mode, project scoping. |
+| [Image generation](features/image-generation.md) | Text→image and image→image (SDXL GGUFs), styles, LoRA, live previews. |
+| [Voice & speech](features/voice.md) | Speech→text (whisper) and text→speech (Kokoro), hands-free voice mode. |
+| [Projects (RAG)](features/projects.md) | Group chats, upload docs, chat grounded in them with cited retrieval. |
+| [Artifacts](features/artifacts.md) | HTML / React / SVG / Mermaid / Markdown rendered in a sandboxed canvas. |
+| [Connectors (MCP)](features/connectors.md) | Add Model Context Protocol servers (none / token / OAuth), use them in chat. |
+| [Models](features/models.md) | Curated, size-bucketed catalog + Hugging Face search; per-modality active model. |
## Platform
-| Topic | What it covers |
-|---|---|
-| [Privacy & data](features/privacy.md) | 100% local inference, encryption at rest, fully offline. |
+| Topic | What it covers |
+| ---------------------------------------------------- | --------------------------------------------------------------- |
+| [Privacy & data](features/privacy.md) | 100% local inference, encryption at rest, fully offline. |
| [Architecture (open core)](features/architecture.md) | The AGPL core, the `pro/` submodule, and the `activate()` seam. |
-| [Off Grid Pro](features/pro.md) | The sees / remembers / acts layer — **coming July 2026**. |
+| [Off Grid Pro](features/pro.md) | The sees / remembers / acts layer — **coming July 2026**. |
---
diff --git a/docs/FUNCTIONAL_TEST_STRATEGY.md b/docs/FUNCTIONAL_TEST_STRATEGY.md
index df40e5e4..ca4d63a5 100644
--- a/docs/FUNCTIONAL_TEST_STRATEGY.md
+++ b/docs/FUNCTIONAL_TEST_STRATEGY.md
@@ -1,7 +1,7 @@
# Functional test strategy — ensure features actually work, across every surface
-The unit-coverage ratchet (vitest, ~54%) proves *pure logic* is exercised. It does NOT prove
-*a feature works* - the boot crash and the "tools don't stream" fork both passed typecheck +
+The unit-coverage ratchet (vitest, ~54%) proves _pure logic_ is exercised. It does NOT prove
+_a feature works_ - the boot crash and the "tools don't stream" fork both passed typecheck +
unit tests + build. This doc tracks FUNCTIONAL tests (unit + integration) per product surface,
including the native (Swift) code, so "does capture -> OCR -> memory -> chat actually work" has
an answer that a test enforces. Bring in whatever framework a surface needs.
@@ -28,10 +28,10 @@ really a test of a third-party framework or engine we merely depend on:
75% branches / 77% functions / 79% lines on the TESTABLE surface (ratchet floor 77/74/76/78,
climbing toward the 85% goal). ~1200 unit/integration tests.
- **Swift, `npm run test:swift`:** 37 XCTest cases over the text-extractor pure classifiers.
-- **DB integration, `npm run test:db`:** 61 cases over `database.ts` + `rag/store.ts` against a
- real temp SQLite. Kept OUT of the default gate because it needs `better-sqlite3` rebuilt for
- the node ABI (the app builds it for Electron's ABI); the script rebuilds + restores. KNOWN GAP:
- wire `test:db` into CI as an isolated step so database.ts coverage is enforced there.
+- **DB integration, `npm run test:db`:** 105 cases over core and Pro persistence against real
+ temp SQLite. Kept OUT of the default gate because it needs `better-sqlite3` rebuilt for the
+ node ABI (the app builds it for Electron's ABI); the script rebuilds + restores. KNOWN GAP:
+ wire `test:db` into CI as an isolated step so database coverage is enforced there.
- The default coverage gate EXCLUDES (with a real alternative suite each): vendored/built
(`packages/**`, `**/dist/**`); native/DB/spawn shells (database.ts, rag/store.ts, imagegen.ts,
mflux.ts, sd-server.ts, model-server.ts, media-server.ts, whisper/parakeet/whisper-server) -
@@ -39,35 +39,37 @@ really a test of a third-party framework or engine we merely depend on:
e2e-covered UI components (MemoryChat.tsx, VaultScreen.tsx).
## Test types
+
- **unit (vitest)** - pure TS logic, no I/O. Fast, always-on. The ratchet floor.
- **integration (vitest, real collaborators)** - run the real implementation against a temp
SQLite DB / temp userData / a spawned native binary / the local engine. `skipIf` the
dependency (binary/model) is absent, so it runs on a dev Mac + release builds and SKIPS in a
plain `npm ci` CI rather than failing. This is where "the feature works" is proven.
- **swift unit (XCTest / swift-testing via SwiftPM)** - pure logic inside a Swift package.
-- **e2e (Playwright Electron)** - the rendered app tour (e2e/, 22 tests today).
+- **e2e (Playwright Electron)** - the rendered app tour (e2e/, 27 tests today).
- **gateway smoke (`npm run smoke`)** - the OpenAI `/v1` surface against a running app.
## Surface matrix (status - keep current)
-| Surface | Kind | How | Status |
-|---|---|---|---|
-| RAG/chat pure logic (ranking, prompts, routing, tool loop) | unit | vitest | DONE (growing) |
-| Streaming tool loop (C7) | integration-ish | vitest, faked model boundary | DONE (`tools-stream.test.ts`) |
-| **Native OCR (Vision, `ocr.swift`)** | integration | vitest spawns the built binary on a rendered fixture | **DONE (`ocr-helper.integration.test.ts`)** |
-| Gateway `/v1` (chat stream, embeddings, image) | smoke | `npm run smoke` vs running app | DONE (manual/pre-release) |
-| STT (whisper-cli / parakeet) + TTS (kokoro) | integration | vitest: TTS->STT round-trip via the gateway; skip if gateway down | **DONE (`audio-engines.integration.test.ts`)** |
-| Image gen (sd-cli / mflux) | integration | vitest: gateway `/v1/images/generations` on the installed model; heavy - local-gated, skip if no image model | TODO (local only) |
-| coreml-sd Swift package | - | **EXCLUDED - not our code.** ~50-line `main.swift` shim over Apple ml-stable-diffusion; testing it tests Apple's SD, not our logic. Covered indirectly by the gateway image-gen integration test when it is the active runtime. |
-| Accessibility watcher (`main.swift`) | integration | needs TCC (accessibility) - local-gated, spawn + assert JSON shape | TODO (local only) |
-| text-extractor / inspect_layout `.swift` | integration | spawn on a fixture; assert output shape | TODO |
-| Capture -> OCR -> memory ingest seam | integration | vitest: feed a fixture frame through the ingest path into a temp DB | TODO |
-| RAG end-to-end (retrieve -> prompt -> answer) | integration | vitest: seed a temp SQLite, run retrieval, assert grounded context | TODO |
-| Vault (kdbx + Argon2) | integration | vitest vs temp dir | DONE (`vault-service.test.ts`) |
-| Renderer surfaces render | e2e | Playwright tour | DONE (22) |
-| Pro dictation / clipboard / replay | e2e + unit | Playwright (`pro.spec.ts`) + pro unit | PARTIAL |
+| Surface | Kind | How | Status |
+| ---------------------------------------------------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- |
+| RAG/chat pure logic (ranking, prompts, routing, tool loop) | unit | vitest | DONE (growing) |
+| Streaming tool loop (C7) | integration-ish | vitest, faked model boundary | DONE (`tools-stream.test.ts`) |
+| **Native OCR (Vision, `ocr.swift`)** | integration | vitest spawns the built binary on a rendered fixture | **DONE (`ocr-helper.integration.test.ts`)** |
+| Gateway `/v1` (chat stream, embeddings, image) | smoke | `npm run smoke` vs running app | DONE (manual/pre-release) |
+| STT (whisper-cli / parakeet) + TTS (kokoro) | integration | vitest: TTS->STT round-trip via the gateway; skip if gateway down | **DONE (`audio-engines.integration.test.ts`)** |
+| Image gen (sd-cli / mflux) | integration | vitest: gateway `/v1/images/generations` on the installed model; heavy - local-gated, skip if no image model | TODO (local only) |
+| coreml-sd Swift package | - | **EXCLUDED - not our code.** ~50-line `main.swift` shim over Apple ml-stable-diffusion; testing it tests Apple's SD, not our logic. Covered indirectly by the gateway image-gen integration test when it is the active runtime. |
+| Accessibility watcher (`main.swift`) | integration | needs TCC (accessibility) - local-gated, spawn + assert JSON shape | TODO (local only) |
+| text-extractor / inspect_layout `.swift` | integration | spawn on a fixture; assert output shape | TODO |
+| Capture -> OCR -> memory ingest seam | integration | vitest: feed a fixture frame through the ingest path into a temp DB | TODO |
+| RAG end-to-end (retrieve -> prompt -> answer) | integration | vitest: seed a temp SQLite, run retrieval, assert grounded context | TODO |
+| Vault (kdbx + Argon2) | integration | vitest vs temp dir | DONE (`vault-service.test.ts`) |
+| Renderer surfaces render | e2e | Playwright tour | DONE (27) |
+| Pro dictation / clipboard / replay | e2e + unit | Playwright (`pro.spec.ts`) + pro unit | PARTIAL |
## Rules
+
- An integration test that needs a binary/model MUST `skipIf` it is absent and say so - never
fail because CI lacks the artifact, never silently pass by mocking the thing under test.
- Prefer a real round-trip (e.g. TTS->STT) over a hand-crafted fixture where it makes the test
diff --git a/docs/GAPS_BACKLOG.md b/docs/GAPS_BACKLOG.md
index 134c9d51..fa4de48e 100644
--- a/docs/GAPS_BACKLOG.md
+++ b/docs/GAPS_BACKLOG.md
@@ -5,98 +5,86 @@ how to reproduce, and the fix direction. Close with evidence; never hide.
---
-## RESOLVED
+## OPEN
-### Agentic `generate_image` tool errored (stale keep-alive socket in the tool loop)
-
-**Status:** RESOLVED. ECONNRESET root cause fixed + regression-guarded; the tool path works in
-every programmatic reproduction (4 ways, below) AND was confirmed working in the real UI —
-observed live: with Tools on and no project, "use your built-in image tool …" routed through the
-agentic `generate_image` tool and the image rendered in the reply. The one earlier UI "Sorry"
-was on a run whose Tools-toggle/grounding was unreliable (pre the provit DOM-grounding fix), not
-the engine fix. Closed.
-
-**Actual root cause (verified with in-process DIAG instrumentation):** the tool loop makes
-BACK-TO-BACK requests to llama-server. `llm.pause()`/`stop()` were never called (DIAG confirmed
-the engine stayed alive: `serverAlive=true, initialized=true` at the error). Round 0 (the
-`generate_image` call) succeeded; round 1's `streamChat` died with `read ECONNRESET`. Node's
-global HTTP agent pooled the round-0 socket; llama-server closes its socket after each response,
-so the pooled socket was half-closed and round 1's write reset. Single-shot chat never reused a
-socket, which is exactly why only the multi-round tool path broke.
-
-**Fix:** `src/main/llm.ts` — every `http.request` to the model now sets `agent: false` +
-`Connection: close` (a fresh connection per request, no keep-alive pool). Applied to all three
-request sites (both streaming methods + the non-streaming one).
-
-**Verified (programmatic, 4 ways):** in-process against the real `window.api.toolChat` — non-
-streaming, streaming (with `streamId`), full UI-faithful (real `imageGenStatus()` → `toolChat`
-→ deferred `generateImage`, which wrote `img-*.png` to disk), and with a 44k-char / 7471-token
-history — ALL return `toolCalls: ["generate_image"]` + `imageRequest`, no ECONNRESET, warm and cold.
-
-**NOT yet verified:** a clean pass through the real vision-driven UI (provit). One provit UI run
-post-fix still showed "Sorry"; could not reproduce it programmatically. Leading suspicion:
-sustained background load during the long provit run (the `[layout] learn` task hammering the
-model with mmproj-500s) puts the engine/queue in a state my quick repros don't hit — UNCONFIRMED.
-A UI-drive attempt to reconcile failed on selectors (nothing sent), so it's still open.
-
-**Regression guard:** `src/main/__tests__/llm-http-no-keepalive.test.ts` reads llm.ts and asserts
-every `http.request` site opts out of the pool (`agent: false` + `Connection: close`). Fails on
-`main` (0 of 3), passes after. (llm.ts can't be imported in a unit test — it pulls in electron —
-so the contract is guarded at the source, per the extract-prompt.test.ts pattern.)
-
-**Note on the earlier hypothesis (kept for honesty):** the first diagnosis blamed the modality
-queue evicting `llm` mid-loop (`imagegen.ts:389` `evicts:['llm']`). DIAG disproved it — pause was
-never called. The eviction machinery is fine; the bug was purely the socket pool.
+_None._ The data-layer/presentation-layer drift sweep (2026-07-09) is fully closed - see RESOLVED
+below. No open bugs or regressions tracked.
---
-## OPEN
+## RESOLVED
-### (historical hypothesis — see RESOLVED above) Agentic `generate_image` tool errors
-
-**Status:** superseded by the RESOLVED entry above (root cause was the keep-alive socket, not
-eviction). Kept below only as the investigation trail.
-
-**What:** The "image generation as an agentic tool" feature (the LLM calling `generate_image`
-in `toolChat`, meant to be the backstop when the intent classifier misses an image request)
-does NOT work end to end. In chat with **Tools ON** and **no project**, a request that reaches
-the tool loop returns *"Sorry, something went wrong while generating a response."*
-(`MemoryChat.tsx:927`).
-
-**Evidence (all verified):**
-- Engine + model are fine: direct probe of `:8439` (streaming, `generate_image` schema) returns
- `finish_reason: "tool_calls"`, `name: "generate_image"`, `arguments: {"prompt":"a solid red
- circle on a plain white background"}`. The multi-round re-feed (assistant `tool_calls` +
- `role:tool` result) also returns HTTP 200.
-- In-process repro against the real `window.api.toolChat` (Playwright-launched app, main stderr
- captured): `tools:chat` throws `read ECONNRESET` on every call, warm or cold.
-- llama-server logs a CLEAN exit mid-loop: `srv operator(): cleaning up before exit... exited
- with code 0` immediately after round 1 emits the tool call. Not a crash — a deliberate kill.
-
-**Root cause:** image generation runs `modalityQueue.run({ tier: 2, label: 'image',
-evicts: ['llm'] })` (`imagegen.ts:389`) — it evicts (kills) llama-server to free unified-memory
-RAM. An image-modality job evicts llama while the `toolChat` loop is between rounds, so the
-loop's next `streamChat` (`llm.ts:723`, hitting `:8439`) dies with ECONNRESET. The tool loop has
-no guard against its own engine being evicted underneath it.
-
-**Reproduce:**
-1. `OFFGRID_USER_DATA= OFFGRID_PRO=1` app.
-2. Chat → memory scope "No memory" → composer "+" → Tools On.
-3. Send a prompt that dodges `looksLikeImageRequest` (so it hits the tool, not the classifier),
- e.g. "Use your built-in image tool to output a solid red circle on a plain white background."
-4. Reply is the generic error; llama-server shows a clean mid-loop exit.
-
-**Fix direction:** guard the engine lifecycle so the LLM is not evicted while a tool loop that may
-still need it is in flight — e.g. hold an "llm in use" lease for the duration of `toolChat`, or
-make the deferred-image tool defer the ACTUAL eviction until the loop has returned (it already
-defers generation to the renderer; the eviction race is the remaining hole). Add a regression
-test that runs a `toolChat` turn which calls `generate_image` and asserts it returns an
-`imageRequest` without the engine being torn down.
-
-**Not the cause (ruled out):** the model refusing to call the tool (it calls it correctly); a
-cold-load race (fails warm too); the `<|tool_response>` tokenizer warnings (non-fatal).
-
-**Related:** the renderer intent classifier (`image-intent.ts` `looksLikeImageRequest`) is the
-ONLY working in-chat image path today; a leading "draw/sketch/paint/illustrate/render" routes
-straight to `generateImage()` (`MemoryChat.tsx:740`), bypassing the tool. That is what produced
-the image in the first provit chat run (PR #40 comment) — not the tool.
+### Data-layer / presentation-layer drift sweep (2026-07-09) - CLOSED
+
+Class: the UI kept its own copy of authoritative data instead of binding to the owning source
+(hygiene §A). Every TIER-1 item is fixed, behavior-neutral where required, and regression-tested;
+the coverage floor held (~97/92/96/98) throughout.
+
+- **T1a. Image composer `imgModel` shadowed the active model** → FIXED. The dropdown's `onChange`
+ now writes through the single owner (`MemoryChat.tsx:553` `setActiveModalModel('image', value)`)
+ and the composer reads the active value from `imageGenStatus().active` (no latch). Terminal-artifact
+ render test: `MemoryChat.image.test.tsx` asserts a dropdown change routes through
+ `setActiveModalModel` and reaches the `generateImage` payload.
+- **T1b. `imgSteps`/`imgSize` re-seed stomp** → FIXED. Per-model overrides resolved by the pure
+ `resolveImageParams`/`setOverride` (`lib/image-params.ts`), persisted via
+ `saveSetting('imageParams', …)`; a model change never clobbers a typed value. Render test asserts
+ the payload carries the user's steps (10), not the model default (28).
+- **T1c. `imgSeed`/`imgNegative`/`imgStrength`/`imgStyle` not persisted** → FIXED. Persisted +
+ reloaded through the data layer (`MemoryChat.tsx:314-317, 332-335`). (`imgInit` stays transient -
+ a per-turn init-image path, correctly not persisted.)
+- **T1d. Image params had no persisted owner** → FIXED (subsumed by T1a–T1c). Image-gen params now
+ have a single persisted owner (the settings store); the composer binds to it and writes through.
+ A separate Settings > Image editor is optional UX, not a drift bug - descoped, not a gap.
+- **T1e. KV cache / FlashAttn / ctxSize two-writer clobber via the mode preset** → FIXED.
+ `applyModePreset` (`llm/settings-math.ts`) MERGES - it only fills fields the user has NOT pinned;
+ the pinned set (`userExplicit`) is persisted (`llm.ts:194`) and restored on boot (`:125-126`), and
+ boot loads the stored `kvCacheType`/`flashAttn` DIRECTLY (never re-derived from the mode), so the
+ every-restart re-clobber path is closed too. Tests: `llm/__tests__/settings-merge.test.ts` +
+ `kv-launch-roundtrip.test.ts` (persist → restart → launch-args round-trip).
+- **T1f. Thinking/reasoning not persisted** → FIXED. Reasoning rides the persisted context blob via
+ `buildAssistantContext`/`readReasoning` (`lib/message-persistence.ts`) and is restored on remap.
+ Real DB round-trip test: `lib/__tests__/message-persistence.test.ts`.
+
+### TIER 2 (minor / adjacent) - dispositioned
+
+- **Preload `setLlmSettings` type omitted kvCacheType/flashAttn/gpuLayers/threads/batchSize/mode** →
+ FIXED (`src/preload/index.ts:244` - the type now carries every field the handler accepts;
+ runtime was always passing the whole object, this closes the type-check blind spot).
+- **Settings identity fields saved on `blur` only (edit lost if closed without blurring)** → FIXED -
+ now also commits on Enter (`Settings.tsx:472-473`), the standard keyboard commit, calling the same
+ `saveIdentity`.
+- **`ctxSize` halved + persisted by crash recovery (`llm.ts:479-483`)** → BY DESIGN, not a bug. This
+ is the deliberate post-crash safety fallback (a too-large KV cache froze macOS on 16GB); it
+ intentionally persists a smaller, safe context after a detected crash. Left as-is.
+- **VoiceScreen residency toggle fire-and-forget; ActionsScreen prop-resync** → minor UI polish, NOT
+ the data-layer drift class (no authoritative copy that diverges). Deferred as cosmetic; would need
+ on-device screenshot verification if ever pursued.
+
+### TIER 3 (ephemeral view prefs) - BY DESIGN
+
+ReplayScreen `speed`/`asideW`, ReflectScreen day/week `mode` reset on remount. No authoritative owner
+to diverge from - explicitly not the drift class. Persisting them is optional UX, not a gap.
+
+### Reference pattern (correct write-through / refetch-bound)
+
+SettingsPanel (LLM inference controls), ModelPicker (per-modality active model), Projects, Connectors,
+ChatDetail, DayView (persisted layout with get + write-back - the good reference), MeetingsScreen,
+ReflectScreen, composer chat-prefs (noMemory/tools/connectors/thinking/voice).
+
+### Agentic `generate_image` tool errored (stale keep-alive socket in the tool loop) - CLOSED
+
+**Root cause (verified with in-process DIAG):** the tool loop makes back-to-back requests to
+llama-server. Round 0 (`generate_image`) succeeded; round 1's `streamChat` died with `read
+ECONNRESET`. Node's global HTTP agent pooled the round-0 socket; llama-server closes its socket after
+each response, so the pooled socket was half-closed and round 1's write reset. (The earlier
+"modality queue evicts llm mid-loop" hypothesis was DISPROVED - DIAG confirmed the engine stayed
+alive; pause was never called.)
+
+**Fix:** every `http.request` to the model uses a fresh connection (`agent: false` +
+`Connection: close`); the SSE transport is now one shared `streamCompletion` (`llm/stream.ts`) used
+by both `chatStream` and `streamChat`. Regression guards: `__tests__/llm-http-no-keepalive.test.ts`
+(reads the source, asserts no keep-alive pool) + `llm/__tests__/stream.test.ts` (a real local SSE
+server exercises content/reasoning/tool-calls/abort/timeout). The double intent-decision that could
+route "draw …" away from the tool was also closed (`shouldAutoRouteImage` suppresses the renderer
+auto-route when the agentic path owns the turn; `image-intent.test.ts` + `MemoryChat.image.test.tsx`
+assert tools-ON → `toolChat`, not a direct `generateImage`).
diff --git a/docs/GATEWAY_SPINE.md b/docs/GATEWAY_SPINE.md
index f62c8413..4f7787dd 100644
--- a/docs/GATEWAY_SPINE.md
+++ b/docs/GATEWAY_SPINE.md
@@ -2,7 +2,7 @@
How the 5-layer agentic stack (`wednesdayai/knowledge-base/architecture.md`) maps onto
the local-first desktop gateway at `127.0.0.1:7878` (`src/main/model-server.ts`), and the
-plan to grow that gateway from a request *router* into a control-plane *spine*.
+plan to grow that gateway from a request _router_ into a control-plane _spine_.
References: Portkey OSS gateway (TS, plugin-based) and Bifrost (Go, high-perf) — what to
steal from each is called out below.
@@ -12,8 +12,8 @@ steal from each is called out below.
## Thesis
The knowledge base says it plainly: **Phase C (the AI gateway) is the spine — pick it
-first, not last.** The other four phases are *wired through* the chokepoint, not *stuffed
-into* one process.
+first, not last.** The other four phases are _wired through_ the chokepoint, not _stuffed
+into_ one process.
So "build all of this in the gateway" resolves to two different moves:
@@ -52,8 +52,8 @@ short-circuit (block, redact, rewrite) or annotate. This is Portkey's plugin mod
expressed in-process. Keep it synchronous and cheap — this is loopback, not 5k RPS.
```ts
-type HookResult = { action: 'pass' | 'block' | 'rewrite'; body?: unknown; reason?: string };
-type Hook = (ctx: GatewayCtx) => Promise | HookResult;
+type HookResult = { action: 'pass' | 'block' | 'rewrite'; body?: unknown; reason?: string }
+type Hook = (ctx: GatewayCtx) => Promise | HookResult
// preHooks: identity, budget, inputPolicy
// postHooks: outputPolicy, egressDlp, audit
```
@@ -62,18 +62,18 @@ type Hook = (ctx: GatewayCtx) => Promise | HookResult;
## What to steal from Portkey vs Bifrost
-| | Portkey OSS | Bifrost |
-|---|---|---|
-| Language | **TypeScript** (matches our gateway) | Go (separate process) |
-| Deploy | Node / edge / Docker, 122kb | NPX / Docker binary |
-| Model | **Config-driven + `/plugins` middleware** | Plugin middleware, ~15µs overhead @ 5k RPS |
-| Has | Guardrails (40+), fallbacks, retries, load-balance, semantic cache, virtual keys, **MCP gateway** | Virtual keys, hierarchical budgets, OIDC, semantic cache, **MCP gateway**, Prometheus |
+| | Portkey OSS | Bifrost |
+| -------- | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
+| Language | **TypeScript** (matches our gateway) | Go (separate process) |
+| Deploy | Node / edge / Docker, 122kb | NPX / Docker binary |
+| Model | **Config-driven + `/plugins` middleware** | Plugin middleware, ~15µs overhead @ 5k RPS |
+| Has | Guardrails (40+), fallbacks, retries, load-balance, semantic cache, virtual keys, **MCP gateway** | Virtual keys, hierarchical budgets, OIDC, semantic cache, **MCP gateway**, Prometheus |
- **Mirror Portkey's plugin/guardrail architecture in-process.** It's TypeScript like our
gateway, the plugin shape (pre/post verifiers returning pass/block/transform) is exactly
the pipeline above, and its MCP-gateway design (centralized auth + tool-call
observability + identity forwarding) is the model for our `/mcp` endpoint. Don't vendor
- Portkey; copy the *shape*.
+ Portkey; copy the _shape_.
- **Treat Bifrost as the "externalize later" reference.** If the gateway ever leaves the
Electron main process to become a standalone local daemon (shared by mobile/paired
devices, or to stop blocking the event loop), Bifrost's Go architecture, weighted-key
@@ -88,27 +88,27 @@ type Hook = (ctx: GatewayCtx) => Promise | HookResult;
The BFSI model assumes multi-tenant, regulated, cloud. On a single-user local device most
of the multi-tenant/regulatory machinery **collapses** — but a surprising amount stays
-**load-bearing**, often in a new guise. The privacy stakes are *higher*, not lower:
+**load-bearing**, often in a new guise. The privacy stakes are _higher_, not lower:
capture sees everything on screen.
-| BFSI layer | In Off Grid | Verdict | Where it lives |
-|---|---|---|---|
-| **A · Data plane** (CDC, lake, PII mask) | capture → OCR → entities → SQLite | Exists | `watcher.ts`, `vision.ts`, `database.ts`, `crm/*` — gateway *consumes*, doesn't own |
-| **A · PII masking** | redact before anything leaves the device | **Survives, critical** | post-hook egress DLP (see C16) |
-| **B · Model serving** | llama / whisper / TTS / diffusion | Exists | gateway already fronts it |
-| **B · KB + retrieval** | RAG extractors + embeddings | Promote | expose retrieval as a first-class gateway tool |
-| **B · Memory (sidecar)** | observations / entities / memory | Promote | memory read/write through the gateway, not baked into one agent |
-| **B · Tool layer (MCP)** | `/mcp` endpoint | Exists, extend | per-tool scope + audit, Portkey-style |
-| **C · Gateway** | `:7878` | **This whole doc** | `model-server.ts` |
-| **C · Audit log** | every chat/tool/model call recorded | **Survives, high value** | new SQLite table; "what did my AI see and do" is a *feature* of a memory product |
-| **C · Input policy** | injection scan of captured/retrieved text | **Survives** — captured screen text is hostile indirect-injection input | pre-hook |
-| **C · Output/DLP** | gate egress to cloud fallback / outbound MCP / paired device | **Survives, critical** | post-hook; the privacy guarantee of the product |
-| **C · Kill switch** | "stop all AI / pause capture now" | **Survives** | pipeline flag + tray |
-| **C · Observability** | tokens, latency, per-feature traces | **Survives** | local dashboard off the audit stream |
-| **C · Identity / RBAC** | single user | Collapses now, **re-emerges for paired devices** | per-device token (gateway already anticipates "a paired device later") |
-| **C · FinOps** | compute/battery budget; $ only if cloud fallback added | Mostly collapses | budget pre-hook, optional |
-| **D · Consumption** | React UI + approval-gated actions | Exists | `crm/approvals.ts`; gateway feeds it traces/citations/confidence |
-| **E · Org / regulatory** | local-first guarantee, recording indicator, user-owned data, AGPL | Collapses to product posture | not gateway code; "AIBOM" → a manifest of bundled models |
+| BFSI layer | In Off Grid | Verdict | Where it lives |
+| ---------------------------------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
+| **A · Data plane** (CDC, lake, PII mask) | capture → OCR → entities → SQLite | Exists | `watcher.ts`, `vision.ts`, `database.ts`, `crm/*` — gateway _consumes_, doesn't own |
+| **A · PII masking** | redact before anything leaves the device | **Survives, critical** | post-hook egress DLP (see C16) |
+| **B · Model serving** | llama / whisper / TTS / diffusion | Exists | gateway already fronts it |
+| **B · KB + retrieval** | RAG extractors + embeddings | Promote | expose retrieval as a first-class gateway tool |
+| **B · Memory (sidecar)** | observations / entities / memory | Promote | memory read/write through the gateway, not baked into one agent |
+| **B · Tool layer (MCP)** | `/mcp` endpoint | Exists, extend | per-tool scope + audit, Portkey-style |
+| **C · Gateway** | `:7878` | **This whole doc** | `model-server.ts` |
+| **C · Audit log** | every chat/tool/model call recorded | **Survives, high value** | new SQLite table; "what did my AI see and do" is a _feature_ of a memory product |
+| **C · Input policy** | injection scan of captured/retrieved text | **Survives** — captured screen text is hostile indirect-injection input | pre-hook |
+| **C · Output/DLP** | gate egress to cloud fallback / outbound MCP / paired device | **Survives, critical** | post-hook; the privacy guarantee of the product |
+| **C · Kill switch** | "stop all AI / pause capture now" | **Survives** | pipeline flag + tray |
+| **C · Observability** | tokens, latency, per-feature traces | **Survives** | local dashboard off the audit stream |
+| **C · Identity / RBAC** | single user | Collapses now, **re-emerges for paired devices** | per-device token (gateway already anticipates "a paired device later") |
+| **C · FinOps** | compute/battery budget; $ only if cloud fallback added | Mostly collapses | budget pre-hook, optional |
+| **D · Consumption** | React UI + approval-gated actions | Exists | `crm/approvals.ts`; gateway feeds it traces/citations/confidence |
+| **E · Org / regulatory** | local-first guarantee, recording indicator, user-owned data, AGPL | Collapses to product posture | not gateway code; "AIBOM" → a manifest of bundled models |
**The three that matter most locally:** audit log (C7), input policy against indirect
injection from captured content (C2), and egress DLP (C16). Those are the spine's
@@ -123,40 +123,45 @@ Staged like the knowledge base's path — each stage earns the next. Don't build
machinery before the pipeline exists.
### Stage 1 — Make it a pipeline (the unlock)
+
- Add `Hook` type + `pipeline()` around `serve()` / `proxyToLlama()`.
- Add the **audit log**: one SQLite table, every request (prompt, modality, model, tokens,
latency, tool calls, outcome). Start logging before any policy exists — it's the
evidence stream everything else reads from.
- Add the **kill switch**: a single pipeline flag, wired to the tray.
-- *Done when:* every call through `:7878` produces an audit row and can be halted instantly.
+- _Done when:_ every call through `:7878` produces an audit row and can be halted instantly.
### Stage 2 — Policy (the privacy beams)
+
- **Input pre-hook:** Presidio-style PII tag + prompt-injection heuristics, run especially
- over *retrieved/captured* content, not just the user prompt. Treat screen text as
+ over _retrieved/captured_ content, not just the user prompt. Treat screen text as
hostile.
- **Egress post-hook (DLP):** before any byte leaves the device — cloud model fallback,
- outbound MCP tool call, paired-device sync — redact/block per policy. This is *the*
+ outbound MCP tool call, paired-device sync — redact/block per policy. This is _the_
privacy guarantee; make it the one hook that cannot be bypassed.
-- *Done when:* nothing leaves the device unredacted, and injected instructions in captured
+- _Done when:_ nothing leaves the device unredacted, and injected instructions in captured
content are caught.
### Stage 3 — Consolidate Phase B through the gateway
+
- Promote **RAG retrieval** and **memory read/write** to first-class gateway tools/endpoints
(today they're libraries the renderer calls directly).
- Extend **`/mcp`** with per-tool scope + audit (Portkey MCP-gateway shape).
- Add **semantic cache** (embed prompt, short-circuit near-duplicates).
-- *Done when:* CRM agents and the renderer reach memory/KB/tools *through* the gateway, so
+- _Done when:_ CRM agents and the renderer reach memory/KB/tools _through_ the gateway, so
every reach is audited and policy-checked.
### Stage 4 — Observability + routing
+
- **Local dashboard** off the audit stream: tokens, latency, per-feature traces, "why this
answer" + citations into the UI (D3b trust indicators).
- **Routing/fallback** (Portkey/Bifrost): local-first, optional cloud (Claude) fallback for
- hard reasoning — and *that* call goes through the egress DLP hook by construction.
-- *Done when:* you can replay any single answer's full trace, and cloud fallback is
+ hard reasoning — and _that_ call goes through the egress DLP hook by construction.
+- _Done when:_ you can replay any single answer's full trace, and cloud fallback is
governed, not a side channel.
### Stage 5 — Paired-device identity
+
- Per-device token issuance (C4 re-emerges). The gateway already says "a paired device
later" in its header comment — this is where RBAC stops being a no-op.
@@ -165,6 +170,6 @@ machinery before the pipeline exists.
## The one rule
Every model call, tool call, memory read, and outbound byte goes through `:7878`. The
-moment a feature reaches a model or leaves the device *around* the gateway, the spine stops
+moment a feature reaches a model or leaves the device _around_ the gateway, the spine stops
reading true — exactly the knowledge base's warning about shadow AI. One chokepoint, no
exceptions.
diff --git a/docs/IMAGE_GEN_OPTIMIZATION.md b/docs/IMAGE_GEN_OPTIMIZATION.md
index e7852f57..d2360c1f 100644
--- a/docs/IMAGE_GEN_OPTIMIZATION.md
+++ b/docs/IMAGE_GEN_OPTIMIZATION.md
@@ -9,12 +9,12 @@ dpm++2m, `--diffusion-fa`.
Two independent costs stack up, and only the second is quality-optional:
-| Steps | Quality | Wall clock (768²) |
-|------:|---------|-------------------|
-| 4 | blank / blob (unusable) | ~19s |
-| 8 | garbage (rainbow banding) | ~47s |
-| 12 | usable | ~64s |
-| 20 | good (the target) | ~105s |
+| Steps | Quality | Wall clock (768²) |
+| ----: | ------------------------- | ----------------- |
+| 4 | blank / blob (unusable) | ~19s |
+| 8 | garbage (rainbow banding) | ~47s |
+| 12 | usable | ~64s |
+| 20 | good (the target) | ~105s |
- **Sampling (UNet) dominates:** ~4.2-4.6 s/step at 768² cfg 7. 20 steps ≈ 92s.
This is the wall. It's the `ggml_conv_2d` operator - the 2D convolutions in the
@@ -40,16 +40,16 @@ actually beats the ANE - so ANE is not the desktop ceiling either.)
## Levers evaluated
-| Lever | Result | Status |
-|-------|--------|--------|
-| **Persistent `sd-server`** (keep model resident) | Warm images skip the ~13s Metal shader warmup + ~5s model reload | **DONE** - `src/main/sd-server.ts`, wired into `imagegen.ts`, tested |
-| **taesd fast VAE** (`--taesd taesdxl`) | VAE decode 1.47s vs ~10-16s (verified live, non-black) | **DONE** - opt-in `fastVae` param, wired both paths, tested. Needs `taesdxl.safetensors` in models dir |
-| **Rebuild from latest upstream** (6314af4 vs bundled 92a3b73) | 4.15-4.5 s/step - NO speedup. Same generic conv kernels | **Ruled out** for perf (would still bring newer model support) |
-| **Winograd conv fork** (arXiv 2412.05781, `SealAILab/stable-diffusion-cpp`, claimed 3-4.79× SDXL on Metal) | Repo is 404 / org gone - not publicly available | **Unavailable**; implementing Winograd in ggml ourselves is a major effort |
-| **`--conv-direct`** flags | 9× SLOWER (33 s/step) - ggml's direct conv is worse | **Rejected** |
-| **f16 instead of q8_0** | Untested (needs ~13GB f16 GGUF, not in our repo) | **Open** - may cut per-step (no dequant); worth a benchmark if a file is produced |
-| **Fewer steps** on the full model | Unusable below ~12 steps | **Rejected** (quality) |
-| **Distilled model** (Lightning/DMD2) | Not yet produced | **Open - the real quality-preserving speed answer** |
+| Lever | Result | Status |
+| ---------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
+| **Persistent `sd-server`** (keep model resident) | Warm images skip the ~13s Metal shader warmup + ~5s model reload | **DONE** - `src/main/sd-server.ts`, wired into `imagegen.ts`, tested |
+| **taesd fast VAE** (`--taesd taesdxl`) | VAE decode 1.47s vs ~10-16s (verified live, non-black) | **DONE** - opt-in `fastVae` param, wired both paths, tested. Needs `taesdxl.safetensors` in models dir |
+| **Rebuild from latest upstream** (6314af4 vs bundled 92a3b73) | 4.15-4.5 s/step - NO speedup. Same generic conv kernels | **Ruled out** for perf (would still bring newer model support) |
+| **Winograd conv fork** (arXiv 2412.05781, `SealAILab/stable-diffusion-cpp`, claimed 3-4.79× SDXL on Metal) | Repo is 404 / org gone - not publicly available | **Unavailable**; implementing Winograd in ggml ourselves is a major effort |
+| **`--conv-direct`** flags | 9× SLOWER (33 s/step) - ggml's direct conv is worse | **Rejected** |
+| **f16 instead of q8_0** | Untested (needs ~13GB f16 GGUF, not in our repo) | **Open** - may cut per-step (no dequant); worth a benchmark if a file is produced |
+| **Fewer steps** on the full model | Unusable below ~12 steps | **Rejected** (quality) |
+| **Distilled model** (Lightning/DMD2) | Not yet produced | **Open - the real quality-preserving speed answer** |
## Net effect of what shipped tonight
@@ -86,7 +86,7 @@ re-quantize) and wiring it as the "Fast" image model, keeping full animagine as
Tried to bake SDXL-Lightning into animagine to ship a few-step q8 Fast model:
-- **sd.cpp's runtime LoRA for SDXL is broken.** LCM *and* Lightning both report
+- **sd.cpp's runtime LoRA for SDXL is broken.** LCM _and_ Lightning both report
`2364/2364 tensors applied` but produce corrupted (banded) output - proven across
q8 AND f16-GGUF, Metal AND CPU, with/without flash-attn. Same models with no LoRA
are clean. So a runtime LoRA is a dead end here; distillation must be baked into the
diff --git a/docs/MARKETING_ANGLES.md b/docs/MARKETING_ANGLES.md
index 5913b14b..07a16fac 100644
--- a/docs/MARKETING_ANGLES.md
+++ b/docs/MARKETING_ANGLES.md
@@ -3,6 +3,7 @@
Background intelligence for knowledge workers. Completely offline, completely private.
**Source docs to pull from / stay consistent with:**
+
- `../../website/vision.md` — "The world we're building toward" (the long-form ambient/proactive Personal-AI-OS essay: morning briefing, one-brain-across-devices, private-by-architecture, intelligence for everyone).
- `../../website/ethos.md` and `../../website/guides/vision-ai.md` — ethos + vision detail.
- `../../mobile/docs/brand_tone_voice.md` — the voice rules (proof-first, privacy-as-mechanism, no exclamation/em-dash/slop). Mac's launch note (the three-products thesis) lives in the section below.
@@ -11,7 +12,7 @@ Background intelligence for knowledge workers. Completely offline, completely pr
One private intelligence layer that lives across your laptop and your phone, learns your work and your life in the background, and gets ahead of you. A truly smart assistant: it remembers everything, syncs device to device over your own network, and acts to make your day easier before you ask. All of it on your hardware. None of it on anyone's cloud.
-The arc: it starts by *seeing and remembering* (capture + memory), then *reflects* (where your attention goes), then *acts* (drafts, files, reminds, with your approval), and finally becomes *proactive* (the briefing, the meeting prep, the nudge) - the same assistant the powerful have always had, now private and in everyone's hands.
+The arc: it starts by _seeing and remembering_ (capture + memory), then _reflects_ (where your attention goes), then _acts_ (drafts, files, reminds, with your approval), and finally becomes _proactive_ (the briefing, the meeting prep, the nudge) - the same assistant the powerful have always had, now private and in everyone's hands.
Voice rules (from `mobile/docs/brand_tone_voice.md`): proof-first, privacy stated as a mechanism not a promise, plain words, no exclamation marks, no em dashes, no hype-slop. Emotional arc: Recognition -> Return -> Freedom. Mix and match for tweets, a landing hero, a launch thread, or a waitlist page.
diff --git a/docs/MASTER_PLAN.md b/docs/MASTER_PLAN.md
index eb6ee710..d4963f1b 100644
--- a/docs/MASTER_PLAN.md
+++ b/docs/MASTER_PLAN.md
@@ -15,6 +15,7 @@ Desktop/Mobile), grounds them in the **organizational brain**, and proves compli
regulator.
**Two halves, one story:**
+
- **Frontline value** — a private, on-device copilot for every worker; democratize the best
people's know-how to the whole field force (sales productivity, frontline enablement).
- **Enterprise control** — the auditable, on-prem control plane a DPO/CISO can defend.
@@ -35,17 +36,17 @@ is how we land where the top-down compliance vendors (e.g. Pints) can't reach.
## 3. The nine planes (and the 5-layer reference mapping)
-| Plane (our module) | Reference layer | Status |
-|---|---|---|
-| **Gateway** (Off Grid AI Gateway, :7878) | AI plane / Control chokepoint | ✅ built (desktop) + console reads it |
-| **Fleet** | Control (devices) | ✅ console (enroll/policy/kill/audit) |
-| **Control** (policy, guardrails, egress, audit, RBAC) | Control plane (C) | ✅ console |
-| **Data** (connectors, ingest, masking, catalog, DSAR) | Data plane (A) | ✅ console |
-| **Brain** (LanceDB ingestion→retrieval) | AI plane (B) | ✅ console |
-| **Agents** (pre-built use cases) | Consumption (D) | ⬜ scaffold |
-| **Analytics** (usage, latency, drift, perf) | Control observability | ✅ console |
-| **Reports** (regulator-ready exports) | Consumption (D) | ⬜ scaffold |
-| **Regulatory** (framework mapping, DPIA export) | Org/Regulatory (E) | ✅ console |
+| Plane (our module) | Reference layer | Status |
+| ----------------------------------------------------- | ----------------------------- | ------------------------------------- |
+| **Gateway** (Off Grid AI Gateway, :7878) | AI plane / Control chokepoint | ✅ built (desktop) + console reads it |
+| **Fleet** | Control (devices) | ✅ console (enroll/policy/kill/audit) |
+| **Control** (policy, guardrails, egress, audit, RBAC) | Control plane (C) | ✅ console |
+| **Data** (connectors, ingest, masking, catalog, DSAR) | Data plane (A) | ✅ console |
+| **Brain** (LanceDB ingestion→retrieval) | AI plane (B) | ✅ console |
+| **Agents** (pre-built use cases) | Consumption (D) | ⬜ scaffold |
+| **Analytics** (usage, latency, drift, perf) | Control observability | ✅ console |
+| **Reports** (regulator-ready exports) | Consumption (D) | ⬜ scaffold |
+| **Regulatory** (framework mapping, DPIA export) | Org/Regulatory (E) | ✅ console |
Layer order (linear story): **Data → AI → Control → Org/Regulatory → Consumption.**
@@ -68,9 +69,9 @@ Layer order (linear story): **Data → AI → Control → Org/Regulatory → Con
in-repo: gateway over HTTP, Brain via a lib interface (LanceDB swappable), Postgres via
Drizzle.
5. **Tiered integration — native vs embed.** Each capability declares `render: 'native' |
- 'embed'`:
+'embed'`:
- **Tier 1 — commodity:** our UI over the API (secrets, vector store, audit query, basic
- metrics, RBAC). *Built.*
+ metrics, RBAC). _Built._
- **Tier 2 — common-80% native, deep-20% embed:** drift (our cards + deep-link),
policies (Monaco + OPA eval — OPA has no heavy UI anyway).
- **Tier 3 — rich UI, don't rebuild:** Grafana-class dashboards, Keycloak admin, eval
@@ -118,7 +119,7 @@ one compatibility test per adapter.
for, and their access. Build **now** to avoid a retrofit.
- **ABAC on top of RBAC** (RBAC already built): attributes = tenant · purpose · data-class,
enforced at the gateway and on each tool/data slice. Policy-as-code via OPA (Monaco editor
- + OPA eval API — Tier-2 native).
+ - OPA eval API — Tier-2 native).
- **SSO** (Google + Microsoft Entra via Auth.js) shipped; Keycloak at scale.
## 9. What's built & verified (today)
@@ -137,7 +138,7 @@ All `tsc`/lint/prettier clean; APIs validated by curl.
internal Admin module, evaluate endpoint.
3. ✅ **Adapter layer** — `src/lib/adapters/` capability ports (inference/observability/
secrets/guardrails/retrieval) + adapters, swap via `OFFGRID_ADAPTER_`, `render:
- native|embed|headless`; Brain embeds through the inference port; `/admin/adapters` API +
+native|embed|headless`; Brain embeds through the inference port; `/admin/adapters` API +
Admin "Integrations · adapters" surface.
4. ✅ **Evals + golden sets** — golden query→expected-doc sets over the Brain; recall-scored
runs persisted; surfaced in the Brain module; `/admin/golden-cases` + `/admin/evals` API.
@@ -158,19 +159,19 @@ All `tsc`/lint/prettier clean; APIs validated by curl.
9. ✅ **Tier-3 embeds** — SSO'd iframes for rich OSS UIs (SigNoz, OpenBao, Keycloak, Marquez,
Langfuse), driven by `render:'embed'` + `embedUrl`. Mere aggregation → license never touches core.
10. ✅ **Model routing (smart + conditional + cloud leash)** — `routing_rules` evaluator folded into
- the policy bundle; `/admin/routing` + `/evaluate`; Control-plane UI + tester. PII→local, etc.
+ the policy bundle; `/admin/routing` + `/evaluate`; Control-plane UI + tester. PII→local, etc.
11. ✅ **Node↔console wiring** — desktop node client (enroll→policy→audit→commands) + Settings UI.
## 10b. Backlog (approved, sequenced) — toward Portkey/Bedrock parity
a. **Infra:** Redis (cache + rate-limit), OpenSearch (SIEM), Unleash (feature flags) → compose +
- `caching`/`siem`/`flags` capabilities.
+`caching`/`siem`/`flags` capabilities.
b. **Caching** (first-party): exact + semantic response cache, Redis-backed (we own it).
c. **Feature-flag module control**: module/capability enablement routed through flags (Unleash).
d. **Prompt registry**: first-party templates + versioning (Langfuse optional backend).
e. **Evals expansion**: promptfoo (Node) + Ragas/DeepEval (service); golden-set stays baseline.
f. **FinOps + token issuance**: virtual keys scoped to user/project, budgets, cost = tokens×price.
- (Gaps we are NOT closing per decision: reversible tokenization vault, Ranger cell-level policies.)
+(Gaps we are NOT closing per decision: reversible tokenization vault, Ranger cell-level policies.)
## 11. Principles to never break
diff --git a/docs/P0_P2_INTEGRATION_COVERAGE.md b/docs/P0_P2_INTEGRATION_COVERAGE.md
new file mode 100644
index 00000000..7b926e75
--- /dev/null
+++ b/docs/P0_P2_INTEGRATION_COVERAGE.md
@@ -0,0 +1,546 @@
+# P0-P2 integration coverage
+
+Living status for the 155 release journeys in
+[`RELEASE_TEST_CHECKLIST.csv`](RELEASE_TEST_CHECKLIST.csv). This file is intentionally
+conservative: a unit test, source-reading assertion, rendered shell without the real behavior, or
+manual claim does not count as complete integration coverage.
+
+## Current status - 2026-07-17
+
+- Status snapshot:
+ - P0: 74 total, 72 covered, 2 left.
+ - P1: 71 total, 71 covered, 0 left.
+ - P2: 10 total, 10 covered, 0 left.
+ - Overall: 155 total, 153 covered, 2 left.
+- P0 handoff status:
+ - Every P0 journey with an automatable application seam is integration-covered.
+ - #1 Core DMG install and #2 Pro DMG install remain release-device checks against the signed,
+ notarized artifacts.
+ - Signed macOS permission, global-hotkey, cross-app paste, and full-volume behavior still require
+ manual confirmation even where the application seam is integration-covered below.
+- Green gates today:
+ - `npm run test:coverage`: 209 files passed, 1 skipped; 2,223 tests passed, 1 skipped;
+ 96.80% statements, 91.64% branches, 96.19% functions, and 97.54% lines.
+ - `npm run test:db`: 17 files and 112 real SQLite integration tests passed; Electron ABI
+ restored afterward.
+ - `npm run test:e2e`: 28 Playwright Electron tests passed against fresh synthetic temp profiles.
+ - Core main, renderer, and Pro TypeScript projects pass.
+- Not yet a clean handoff: release-journey coverage remains incomplete, and strict ESLint exposes
+ a legacy backlog. Neither is hidden by the coverage percentage.
+
+## Covered P0 journeys
+
+- #3 - Fresh profile is truly fresh. `e2e/smoke.spec.ts` launches the built Electron app with a
+ new temp `OFFGRID_USER_DATA` directory and verifies first-run onboarding and the preload bridge.
+- #4 - Core and Pro artifact separation. `release-packaging.integration.test.ts` builds both
+ resolved source graphs with sourcemaps and proves Core contains no `pro/` implementation while
+ retaining the locked shell and entitlement gates; Pro excludes the stub and includes both real
+ activation entry points.
+- #5 - Packaged helper binaries exist. `packaged-helpers.integration.test.ts` runs real
+ `electron-vite` and `electron-builder` with the production packaging config, then proves the
+ artifact contains hydrated executable llama, ffmpeg, and Whisper helpers at the runtime-resolved
+ paths plus every staged dylib as an exact-name regular file.
+- #6 - Packaged llama dependency closure. `release-packaging.integration.test.ts` invokes the exact
+ repository `scripts/build-llama.sh` against a disposable CI-shaped output and proves transitive
+ `@rpath` closure, non-symlink staging, exact deployment-target comparison, and rejection of both
+ `/opt/homebrew` and `/usr/local` dependencies.
+- #7 - Upgrade preserves user data. Pro `upgrade-profile.dbtest.ts` loads a fixture pinned to the
+ previous release's Core and Pro schemas, runs current migrations and owners, then relaunches and
+ proves chats, memory, projects, knowledge, settings, Pro data, entitlement, and model selections
+ remain intact. Signed installer replacement remains a separate device check.
+- #9 - Fresh onboarding completes. `e2e/smoke.spec.ts` drives the real onboarding flow and lands on
+ Models.
+- #10 - Configure for me completes. `model-server-chat.integration.test.ts` runs production
+ auto-configuration against a temp profile, real catalog/model manager and filesystem, proving
+ the conservative chat, transcription and voice baseline downloads, activates, and reaches the
+ terminal done state through only download/native-process boundaries.
+- #13 - System Health is truthful. `e2e/smoke.spec.ts` launches a fresh profile, compares the real
+ gateway `/health` payload with the System Health IPC components, verifies absent runtimes are
+ reported as `not_installed`, and confirms the unavailable chat engine port is actually down.
+- #15 - Required macOS permissions granted. `permission-recovery.test.ts`,
+ `media-permission.test.ts`, Pro notification tests, the live capture scheduler journey, and the
+ connected dictation overlay prove explicit Screen Recording and Accessibility requests,
+ non-prompting health polls, media admission, live recovery, and native notification `.show()`.
+ Granting all four TCC permissions in the signed app and checking relaunch prompts remains manual.
+- #17 - Text model downloads. `model-download-matrix.integration.test.ts` streams deterministic
+ GGUF bytes through the production manager's HTTP boundary, verifies observable progress and
+ atomic promotion, then activates the installed catalog model through the real selection owner.
+- #25 - Interrupted download recovers. `model-integrity.integration.test.ts` interrupts a real
+ streamed partial, reloads the manager, resumes with the correct HTTP range, verifies exact final
+ bytes and installation, then reloads again to prove completed state stays cleared.
+- #26 - Truncated GGUF is rejected. `model-integrity.integration.test.ts` drives the real model
+ manager against a temp filesystem, with only HTTP delivery faked, and proves truncated downloads
+ and local imports are rejected before promotion, installation, copying, or registration.
+- #27 - Disk write failure does not crash. The same real model-manager integration injects
+ `ENOSPC` only at the OS write boundary and proves the error is contained, no partial model is
+ installed, failed status is recorded, and an existing installed model remains readable.
+- #28 - Active text model survives relaunch. `model-integrity.integration.test.ts` installs and
+ activates a real catalog text fixture through the production model manager, reloads every module,
+ and proves the same installed model remains the active chat selection.
+- #32 - First local message replies. `MemoryChat.chat-lifecycle.test.tsx` sends through the real
+ rendered composer, routes a streamed token through production ownership, resolves the local-model
+ boundary, and proves one assistant bubble with the exact answer is persisted once.
+- #33 - No memory scope works. The same rendered integration keeps No memory visibly selected,
+ sends a turn through the production chat path with retrieval disabled and no project scope, then
+ renders the conversation-only answer normally.
+- #34 - All memory scope works. `rag-empty-memory.dbtest.ts` seeds a synthetic capture into real
+ SQLite/FTS storage, invokes the production `rag:chat` IPC handler in All memory mode, and proves
+ the local-model prompt, answer, streamed retrieval count, and returned `[S1]` citation all carry
+ the exact matching source.
+- #38 - Stop before first token. `MemoryChat.chat-lifecycle.test.tsx` holds the real rendered turn
+ at the preload persistence boundary, clicks Stop during the pre-stream window, proves the model
+ transport never starts, and immediately completes a second turn normally.
+- #39 - Stop during streaming. The same integration routes a live token through production stream
+ ownership, clicks Stop, verifies cancellation uses that stream ID, and proves the partial answer
+ remains visible and persisted while the busy state clears.
+- #41 - Conversation switch isolation. The same integration starts and streams conversation A,
+ switches the rendered screen to B, proves B receives none of A's partial or completed state, then
+ reopens A and retrieves its correctly persisted result.
+- #42 - Project switch isolation. The same integration sends from Project Alpha, changes the real
+ project selector to Project Beta while the model boundary is pending, and proves the result and
+ parsed HTML artifact retain the Alpha project and conversation captured at send time.
+- #43 - Chat survives relaunch. `e2e/chat-memory.spec.ts` creates multiple scoped and unscoped
+ conversations with messages and context through production IPC, fully closes Electron, reopens
+ the same profile, and verifies every association and payload through the reloaded preload path.
+- #48 - Error clears busy state. `MemoryChat.chat-lifecycle.test.tsx` rejects a live turn at the
+ native-model boundary, proves the rendered error is useful and Stop clears, then sends and renders
+ a successful second turn through the same production composer.
+- #52 - Attach a knowledge document. `rag-store-integration.dbtest.ts` drives a real Markdown file
+ through production extraction/chunking, real SQLite persistence and retrieval, and prompt
+ formatting, with only the local embedding-model boundary deterministic.
+- #53 - Project retrieval is grounded. `rag-store-integration.dbtest.ts` indexes real Markdown
+ files into two projects through the production RAG service and SQLite store, then proves selected
+ project and enabled-document filters exclude obsolete and cross-project facts from retrieval and
+ prompt context.
+- #51 - Create a project. `src/main/__tests__/rag-store-integration.dbtest.ts` exercises the real
+ project store against temp SQLite and verifies the round trip.
+- #56 - Delete project cascades. `src/main/__tests__/project-delete-cascade.dbtest.ts` uses the real
+ project, conversation, message, artifact, document, and chunk paths and proves no orphans remain.
+- #61 - Text prompt generates an image. `MemoryChat.image.test.tsx` drives the real rendered image
+ composer through its preload boundary, holds the native image job pending, emits live production
+ progress, then proves exactly one generated image reaches the conversation.
+- #62 - Image cancellation is scoped. The same rendered integration starts an image job in one
+ conversation, switches conversations, and proves progress and Stop stay with the owning
+ conversation; stopping it calls the native cancellation boundary exactly once.
+- #63 - Image cancellation keeps text. `e2e/chat-memory.spec.ts` drives a real tool turn through a
+ fake native llama socket, renders and clicks the tool-owned image Stop action during pre-spawn
+ memory reclamation, proves the shared lifecycle prevents `sd-cli` from starting, then fully
+ relaunches Electron on the same real SQLite profile and restores the assistant text.
+- #68 - Vision answers about attachment. The rendered composer processes a ready image attachment,
+ sends its persisted path and the exact typed question through the production vision path, and
+ renders the returned answer.
+- #69 - Text-only model guards image input. The same integration proves an unavailable vision
+ capability produces a visible explanation before image processing or model delivery, then
+ completes a text-only turn normally.
+- #71 - Connector can be added. `integration-tests/mcp-connector-setup.dbtest.ts` drives the real
+ Integrations screen through a native IPC boundary adapter into production connector persistence,
+ encrypted SQLite, MCP discovery, and a real stdio child, proving connected appears only after
+ discovery and exactly one row with `read_status` survives database reopen.
+- #73 - Connector tool executes. The real connector extension executes a read-only tool through
+ its remote boundary and returns the result; `tools-loop.dbtest.ts` proves extension output flows
+ through the production tool loop into the final answer.
+- #74 - Write tool requires approval. `mcp-connector-tool-extension.dbtest.ts` uses real connector
+ state and the production extension to prove a write is queued at the approval boundary before
+ the remote call can execute.
+- #75 - Stop prevents connector side effect. `tools-loop.dbtest.ts` aborts after the streamed tool
+ call but before extension execution and proves the side-effect implementation is never invoked;
+ the dispatch guard ensures connector tools use that same abstraction.
+- #76 - Expired connector becomes error. The connector integration injects an authorization
+ failure only at the remote boundary and proves the real SQLite connector changes to an error
+ state with an actionable detail while healthy tools remain available.
+- #79 - Gateway models endpoint. `e2e/smoke.spec.ts` starts the real Electron gateway, seeds an
+ active model only inside the disposable profile, calls `/v1/models` over HTTP, and verifies
+ modality metadata in both supported response shapes.
+- #80 - Gateway chat streaming. `model-server-chat.integration.test.ts` sends an
+ OpenAI-compatible streaming request through the real HTTP gateway to a loopback llama-server
+ boundary and proves the first SSE token arrives before upstream completion, later chunks retain
+ their content, and the stream terminates with `[DONE]`.
+- #83 - Screen capture permission path. `capture-disabled.integration.test.ts` keeps the real
+ production capture interval, focus extractor, settings store, and filesystem writer connected
+ while only the Electron/TCC boundary changes from denied to granted; the next eligible tick writes
+ a frame without restarting the loop or app.
+- #84 - Capture disabled means no capture. `capture-disabled.integration.test.ts` backs the real
+ capture state machine with the production settings store and proves the persisted privacy pause
+ survives hydration, prevents OS capture work across ticks, and resumes only after user action.
+- #85 - OCR creates searchable memory. `capture-exclusion.dbtest.ts` drives a normal surface through
+ screenshot, OCR, the real extractor, model transport, and SQLite persistence, then queries the
+ production observation search and retrieves the derived memory; only OS capture/OCR and the
+ native model socket are controlled boundaries.
+- #86 - Sensitive or excluded apps are omitted. `capture-exclusion.dbtest.ts` drives the production
+ extractor and real SQLite persistence, with only screenshot/OCR and the model socket at their
+ external boundaries, and proves configured apps, authentication surfaces, private browser
+ windows, and password-manager URLs stop before capture while a normal app still persists.
+- #87 - Replay timeline renders. `e2e/pro.spec.ts` uses production Pro seeding to write real PNG and
+ SQLite capture data, independently verifies filesystem chronology matches IPC order, then drives
+ the real Replay UI through every frame and proves image, app, caption, full timestamp, scrubber
+ time, and position remain usable and ordered.
+- #90 - Unified search finds each source. `pro/main/__tests__/universal-search.dbtest.ts` writes
+ captured and connector-derived observations plus meeting, entity, fact, memory, chat, and
+ knowledge records through their production SQLite owners, then proves one production search
+ returns every source with its real facet and deep-link identifier.
+- #94 - Delete all removes capture corpus. `pro/main/__tests__/personal-data.integration.test.ts`
+ registers the real Pro personal-data owner, runs the real delete-all path, and verifies the
+ observations corpus is gone.
+- #95 - Meeting detection is truthful. `meeting-lifecycle.integration.test.ts` drives the production
+ classifier and controller with only active-window/native-recording boundaries controlled. It
+ proves supported presence starts once, explicit lobby/post-call states do not record, leaving
+ warns then stops, and the capture resource is released.
+- #106 - Mic and TTS stop cleanly. `MemoryChat.chat-lifecycle.test.tsx` proves canceled synthesis
+ cannot start late and unmount pauses active audio; `DictationOverlay.integration.test.tsx` proves
+ unmount stops the recorder, microphone track and audio context and removes every event listener.
+- #96 - Manual meeting recording. `MeetingsScreen.integration.test.tsx` renders the real screen and
+ recorder hook, clicks Record then Stop, and proves exactly one sane-duration completed meeting is
+ visible with capture inactive.
+- #97 - Meeting transcript and summary. Pro `meeting-persistence.dbtest.ts` sends synthetic WAV
+ bytes through production ffmpeg and Whisper selection, persists the exact transcript, sends it to
+ the local summary boundary, verifies the returned recap retains the named owner, deadline, and
+ next step, folds both into memory, and restores the completed meeting after relaunch.
+- #99 - Global dictation hotkey. Pro `dictation-paste-failure.ui.integration.dbtest.ts` connects the
+ production shortcut controller, overlay, recorder, STT, IPC, and database; it proves one
+ Option+Space toggle lifecycle, correct rebind/unregister behavior, and complete resource teardown.
+- #100 - Dictation pastes at cursor. The same connected journey captures the target app, sends the
+ exact transcript once through the native paste boundary, preserves it in the saved recording,
+ and restores the prior clipboard. Real TextEdit caret placement remains a signed-device check.
+- #112 - Approval queue gates actions. `approvals.integration.test.ts` exercises real proposal,
+ decision, execution, failure, and audit persistence against SQLite.
+- #116 - CRM processing tolerates schema upgrades. `crm-schema-upgrade.dbtest.ts` creates a real
+ legacy SQLite schema, drives the production observation funnel, verifies additive migration and
+ row preservation, then reopens the database and proves processing remains idempotent.
+- #117 - Clipboard records text. `e2e/pro.spec.ts` writes unique text to the real OS clipboard and
+ waits for the production poller and encrypted history store to expose that exact item over IPC.
+- #121 - Clipboard restore text. The same E2E journey overwrites the OS clipboard after capture,
+ restores the stored item through production IPC, and verifies the original text returns.
+- #122 - Clipboard restore file. `e2e/pro.spec.ts` drives the real capture-to-restore path and
+ verifies macOS receives path text, file URL, and native bytes, including an image-file case.
+- #125 - Create and unlock vault. `vault-service.test.ts` uses real KDBX4, Argon2id, WASM crypto,
+ and a real temp directory; correct and incorrect passwords are covered.
+- #126 - Vault item types round-trip. `vault-service.test.ts` covers real encrypted CRUD and binary
+ attachment persistence across lock and unlock.
+- #128 - Vault recovery and backup. `vault-service.test.ts` and `vault-recovery.test.ts` exercise
+ real KDBX export bytes, recovery setup, wrong phrases, and recovery to a new password.
+- #132 - Settings survive relaunch. Existing journeys 129 and 131 cover model residency and
+ resource settings across relaunch. Core and Pro `settings-persistence.dbtest.ts` tests add the
+ other owning stores: they change software-update, capture-privacy, identity, and proactive
+ delivery settings over real encrypted SQLite, close the database, reload every Off Grid module,
+ rehydrate each owner, and verify every value restores.
+- #134 - Clear cache preserves user data. `cache-cleanup.integration.test.ts` and the rendered
+ Storage journey exercise the production control through IPC and prove its allowlist can reach
+ only Electron's `cache` data type. Chats, projects, models, vault, settings, entitlement, and
+ unknown app files are unreachable by construction; success and failure states are both visible.
+- #135 - Delete category is scoped. The real SQLite/filesystem integration deletes the Chats
+ category through `clearCategory` and proves memory, projects, connectors, encrypted tokens,
+ models, and unrelated personal files remain.
+- #136 - Delete all is complete. Core and Pro DB integration tests seed projects, chats, memory,
+ knowledge, connectors, encrypted tokens, profile data, every registered Pro table, and every
+ personal-data directory. They run the real delete-all registry, close and reopen the encrypted
+ database, and verify personal data stays gone while models and ordinary preferences survive.
+- #137 - Core locked Pro tabs. The free-tier Electron tour discovers every lock-bearing nav item
+ rendered from the production catalog, opens each one, verifies its matching upgrade heading, and
+ confirms the Pro entitlement remains false.
+- #138 - Pro license activates. `licensing.integration.test.ts` activates through the production
+ IPC/service against a remote license boundary, proves the cached key is encrypted, reloads every
+ module, and verifies the synchronous entitlement gate still unlocks Pro. The rendered Upgrade
+ screen proves the user sees activation and the required restart action.
+- #140 - Offline entitlement behavior. The licensing integration activates a lifetime entitlement,
+ reloads with its network boundary unavailable, and proves the signed cache remains entitled while
+ both a fresh profile and an expired cached entitlement stay locked.
+- #144 - Local use works offline. A shared offline boundary rejects and records every outbound
+ request while preserving real loopback transports. Connected Core and Pro integrations prove
+ local chat, image generation, Vision OCR, replay, SQLite/FTS search, dictation, and KDBX/Argon2
+ vault operations remain usable with zero unexpected egress; the real model manager also proves a
+ network-only download fails clearly and retries without corrupting installed state.
+- #145 - Cold relaunch after forced quit. `e2e/chat-memory.spec.ts` kills the real Electron main
+ process during non-destructive chat activity, waits for process exit, reopens the same profile,
+ and verifies clean boot, preload availability, usable input, and durable committed chat data.
+- #147 - Engine restart recovers. `image-runtime-reliability.integration.dbtest.ts` crashes the
+ native llama executable boundary while a real gateway request waits, proves the production service
+ marks it down, starts exactly one replacement, and returns the recovered chat response. Teardown
+ verifies the gateway/model ports rebind and every owned child process exits.
+- #148 - Low disk space is handled. Model and artifact integrations constrain only disposable OS
+ write boundaries, force mid-stream `ENOSPC`, and prove the active partial is removed, resumable
+ network partials are retained, artifact JSON uses atomic promotion, and existing model and
+ artifact bytes remain readable.
+- #155 - No private data in release evidence. Both Core and Pro screenshot harnesses now consume
+ `release-evidence-profile.mjs`, which rejects non-temporary profiles, strips hostile inherited
+ profile/seed variables, and enables only the synthetic seeders. The integration probe proves a
+ real user-data path cannot be selected; the defect where Core's tour opened the default profile
+ is fixed.
+
+## Covered P1 journeys
+
+- #8 - Window identity and product name. `product-identity.test.ts` locks package, builder, local
+ Core/Pro build, renderer document, and runtime bootstrap names to `Off Grid AI Desktop`;
+ `e2e/tour.spec.ts` launches the built Electron app and verifies both its visible window title and
+ Electron runtime name match that canonical product identity.
+- #11 - Manual setup path works. `manual-model-setup.test.ts` drives PermissionGate into manual
+ model selection, downloads through the production manager and real catalog, verifies only the
+ chosen GGUF is fetched and promoted, activates it through the real selection owner, and proves
+ the rendered Models card reaches Active while every unchosen model remains absent.
+- #12 - Onboarding resumes after relaunch. The production Electron journey uses a fresh Core
+ profile across three full process launches, proves the saved step and all six capability labels
+ restore, completion clears progress, and an interrupted registry plus `.part` file returns as a
+ failed transfer with the exact Retry UI without losing partial bytes.
+- #14 - Chat engine stderr is surfaced. The same integration starts a native child that reports an
+ incompatible `gemma4` architecture and exits, then proves System Health returns the classified
+ engine-too-old reason rather than a generic down message.
+- #16 - Denied permission is recoverable. `permission-recovery.test.ts` and
+ `MemoryChat.microphone-permission.integration.test.tsx` drive the production permission owners
+ from denied to the correct System Settings target and prove the rendered microphone path stays
+ usable for retry without reloading the app.
+- #18 - Vision model downloads. `model-download-matrix.integration.test.ts` proves a real catalog
+ vision model remains unavailable until both weights and projector finish, then activates the exact
+ primary/projector pair persisted by the production manager.
+- #19 - Speech model downloads. The same matrix downloads every Parakeet file, activates it through
+ the manager, and runs the production transcription selector against only a native executable fake
+ to return synthetic dictation text.
+- #20 - TTS model downloads. `model-download-tts.integration.dbtest.ts` downloads all voice files,
+ activates SQLite-backed speech residency, and drives production synthesis to a valid WAV through
+ only the heavyweight worker boundary.
+- #21 - Image model downloads. The model matrix holds a multi-file catalog image runtime unavailable
+ until its full file set lands, then proves the production image status and active selection agree.
+- #22 - Multiple downloads queue. Production download ownership enforces three active transfers,
+ exposes FIFO queued state, rejects duplicate queued IDs, cancels queued work, and drains four real
+ transfer promises without dropping or prematurely resolving any model.
+- #23 - Delete does not cancel another download. The matrix holds one real HTTP download pending,
+ deletes a different installed model through the production manager, then completes and installs
+ the untouched in-flight model.
+- #24 - Offline download fails clearly. `model-integrity.integration.test.ts` drives the real model
+ manager through an offline fetch failure, verifies a clear network-unavailable error and clean
+ filesystem state, preserves an existing installed model, then retries successfully with the same
+ manager and exact GGUF bytes.
+- #29 - Active modal models survive relaunch. `model-integrity.integration.test.ts` installs and
+ activates real image, STT, and TTS catalog fixtures, reloads every manager module, and proves each
+ persisted modality restores its own selection without crossing into another modality.
+- #30 - Deleting an active model clears selection. `model-integrity.integration.test.ts` activates
+ installed text, vision, image, speech, and transcription fixtures through the production model
+ manager, deletes each one, and proves all runtime and persisted selections remain cleared after a
+ fresh module load.
+- #35 - Empty memory degrades safely. `rag-empty-memory.dbtest.ts` invokes the real `rag:chat` IPC
+ handler on an empty SQLite/RAG corpus, verifies a normal answer, empty context and zero retrieval
+ counts, then completes an immediate second turn to prove the queue and controller were released.
+- #36 - Thinking streams separately. `MemoryChat.chat-lifecycle.test.tsx` drives a real rendered
+ turn through the production thinking-stream parser and proves reasoning renders in its own block
+ while the final answer remains separate.
+- #37 - Plain reply hides think markers. The same rendered production path proves a plain reply
+ exposes no parser markers or literal think tags while preserving the final response.
+- #40 - Queued message order. `MemoryChat.chat-lifecycle.test.tsx` sends a second message through
+ the real composer while the first model-boundary promise is pending, then proves production queue
+ draining preserves user/assistant order without collision, duplication, or loss.
+- #45 - Delete conversation cascades. `conversation-delete-cascade.dbtest.ts` proves real messages
+ and artifacts do not survive conversation deletion.
+- #47 - Regenerate reply. `MemoryChat.chat-lifecycle.test.tsx` invokes the rendered Regenerate
+ action, replaces the old assistant answer from the same user context, and proves both the visible
+ and persisted transcripts contain one user turn and the new answer without duplication.
+- #49 - Long answer respects configured cap. The production stream adapters preserve native finish
+ reasons, normalize only the configured token-cap cutoff, render the visible limit, and persist and
+ restore that exact cutoff contract after conversation reload.
+- #54 - New chat inherits its project. `MemoryChat.project-inheritance.test.tsx` drives the real
+ composer from a project target across the preload boundary and proves both conversation creation
+ and RAG retrieval receive the same project ID; reopening a saved conversation restores that scope.
+- #57 - Text artifact saves and reopens. `artifact-persistence.integration.test.ts` saves exact
+ text, title, conversation, and project scope through the production artifact service, reloads its
+ modules, and proves the persisted artifact reopens without content or ownership drift.
+- #58 - Image artifact saves and reopens. The same integration writes a real PNG to the disposable
+ user-data tree, persists its metadata through the production artifact service, reloads the service,
+ and proves both its source association and exact image bytes remain available.
+- #60 - Unsupported document fails clearly. The production picker excludes unsupported types;
+ `rag-store-integration.dbtest.ts` drives a corrupt PDF through the real parser, RAG service, and
+ SQLite store and proves extraction fails clearly before documents, chunks, or embeddings exist.
+- #64 - Image settings apply. `MemoryChat.image.test.tsx` drives the real image composer through
+ per-model size, steps, and guidance overrides plus seed and negative prompt, proves the exact
+ payload across remount, and restores generation metadata from persisted conversation context.
+- #65 - Image runtime eviction recovers. `image-runtime-reliability.integration.dbtest.ts` starts
+ the real chat service against a native executable boundary, generates an image through the real
+ modality queue to evict it, then proves a second native chat process starts and answers the next
+ message without an application restart.
+- #66 - Image RAM guard is safe. `image-runtime-reliability.integration.dbtest.ts` proves an
+ over-budget request stops before native execution, while the rendered integration exposes one
+ explicit `Run anyway` recovery that retries the identical request and scope with the unsafe
+ override and no duplicate turn.
+- #67 - Generated image opens. `MemoryChat.image.test.tsx` opens the generated output in the shared
+ lightbox, exports the exact production path and filename through the native save boundary, and
+ closes the preview without duplicating the artifact.
+- #70 - Damaged image fails safely. `files-image-upload.dbtest.ts` real-decodes image bytes with
+ Sharp and proves invalid content never persists. The rendered composer shows the specific error
+ on the attachment and successfully completes the next text turn.
+- #72 - Connector tools load. `mcp-connector-tool-extension.dbtest.ts` discovers schemas through
+ the production extension and real connector database, preserving enabled/disabled state while
+ controlling only the remote MCP transport.
+- #77 - Dead connector does not hang all tools. `mcp-timeout.dbtest.ts` runs the default production
+ extension over real SQLite connectors and the real eight-second timeout; a non-responsive MCP
+ process becomes an error while the healthy connector schema still returns.
+- #78 - Connector delete removes secrets. `connector-delete-secrets.dbtest.ts` deletes through the
+ production connector repository, reopens the encrypted database, and proves all owned OAuth,
+ PKCE, client-registration, and env secrets are gone while unrelated secrets remain readable.
+- #81 - Gateway image route. `model-server-image.integration.dbtest.ts` sends a real HTTP request
+ through the production gateway, image orchestrator, modality queue, SQLite residency owner,
+ argument builder, and generated-image filesystem. Only the native `sd-cli` executable is faked;
+ the test verifies its PNG reaches the OpenAI-compatible response and disk, plus the missing-runtime
+ path returns a stable `not_installed` envelope.
+- #82 - Gateway failure envelope. `model-server-chat.integration.test.ts` sends malformed input
+ through the real HTTP gateway, proves it receives the stable OpenAI-style JSON error contract
+ without reaching the native model boundary, then calls the gateway again to verify it stays healthy.
+- #88 - Replay playback uses media server. `media-server.integration.test.ts` runs the real
+ loopback server over temp PNG, MP4, WAV, and byte ranges, while rendered Replay follows its
+ emitted URL, fetches the exact bytes, advances playback, and shares the same media lifecycle with
+ Meetings and Voice.
+- #89 - Replay navigation preserves target. `e2e/pro.spec.ts` derives a query from an actual
+ interior capture, opens its visible Search result through the production Pro router, and proves
+ Replay lands on that exact image, app, timestamp, caption, and timeline position rather than the
+ beginning of the session.
+- #91 - Search filters and sort apply. The universal-search DB integration proves production source,
+ recency, and match filtering over fresh results; `SearchScreen.integration.test.tsx` drives the
+ rendered filter and sort controls and verifies visible ordering changes without stale rows.
+- #92 - Day briefing renders. `DayReplay.integration.test.tsx` backs the rendered Day view with real
+ SQLite and filesystem owners plus the real LLM service over a native-engine HTTP boundary, then
+ proves priorities, meetings, suggestions, journal, time spent, and timeline all render.
+- #93 - Day links open correct records. The rendered Day integration follows typed production
+ targets to an exact action backed by real SQLite, entity ID, external calendar URL, Replay block
+ timestamp, and meeting; stale action or meeting targets fail closed instead of selecting an
+ unrelated record.
+- #98 - Meeting survives relaunch. `meeting-persistence.dbtest.ts` saves synthetic meeting media,
+ transcript, and local-model summary through production filesystem/SQLite owners, closes the DB,
+ resets modules, and proves the exact audio metadata, transcript, and summary restore.
+- #101 - Dictation paste failure is visible.
+ `dictation-paste-failure.ui.integration.dbtest.ts` connects the real rendered overlay, voice
+ bridge, IPC, controller, decode/transcription/storage path, and paste sink against denied
+ focus/paste platform boundaries, proving the transcript remains on the clipboard and the exact
+ actionable error stays visible after the controller broadcasts idle.
+- #102 - Dictation engine selection. `voice-journeys.dbtest.ts` persists Whisper and Parakeet
+ choices through generic dictation settings and runs both real CLI implementations against native
+ executable boundaries without caller-side engine branching.
+- #103 - Import media for transcription. The same real IPC/filesystem/SQLite integration imports
+ synthetic media into a completed recording, while `VoiceScreen.integration.test.tsx` proves the
+ rendered drop gesture refreshes into a searchable transcript card.
+- #105 - Speak assistant reply. `e2e/tts-speak.spec.ts` clicks the real rendered Speak action and
+ runs production preload, IPC, TTS normalization, worker spawn, WAV data URL, and Chromium audio;
+ only the heavyweight ONNX worker is replaced. It proves markdown becomes the spoken text
+ `A local reply with code` before the UI enters and exits the real Stop state.
+- #107 - Entities are synthesized. `entity-action-journeys.dbtest.ts` records person, project, and
+ company mentions through production observation and entity owners, proving one correctly typed
+ record per entity with automatic identifiers and two supporting observations.
+- #108 - Self mentions are filtered. The same real-DB integration stores the user's name and aliases,
+ passes mixed mentions through production identity filtering, and proves only the external person
+ becomes an entity.
+- #109 - Entity detail opens. `EntityNavigation.integration.test.tsx` drives the real Search result
+ gesture into the real Entities screen and proves the selected ID's type, narrative, handle, and
+ both evidence rows remain consistent across entry points.
+- #110 - Entity merge preserves evidence. `resolve.integration.test.ts` exercises real entity,
+ aliases, observations, relationships, action reassignment, split, and merge persistence.
+- #111 - Action items are extracted. `entity-action-journeys.dbtest.ts` sends a synthetic commitment
+ through the production extractor over a loopback native-model boundary and proves exactly one
+ imperative action persists with its due date and source evidence.
+- #113 - Action status survives persistence. `approval-relaunch.dbtest.ts` approves, rejects, and
+ dismisses separate synthetic items through production encrypted SQLite and a real stdio MCP
+ child, closes and reopens the profile, proves every status remains, and verifies stale or
+ concurrent decisions cannot duplicate external execution or learning feedback.
+- #114 - Notifications open their target. Production notifications preserve typed approval,
+ action, and calendar targets across native and rendered delivery, wait for Pro routing readiness,
+ dedupe live and persisted copies, focus the primary window, and fail closed when a target is stale
+ instead of selecting an unrelated record.
+- #115 - Reflect uses real time ranges. `reflect.integration.test.ts` verifies real observation
+ windows, dwell caps, category rollups, context switches, and seven-day aggregation.
+- #118 - Clipboard deduplicates repeated copy. `clipboard-store.integration.test.ts` exercises the
+ real store and proves timestamp bump-to-top without duplicate rows or lost tags.
+- #119 - Clipboard records images and files. `e2e/pro.spec.ts` captures real file URLs and a new
+ pixel bitmap, then restores the bitmap bytes and verifies their exact dimensions.
+- #120 - Clipboard live refresh keeps valid selection.
+ `ClipboardScreen.integration.test.tsx` renders the real screen and proves selection is retained
+ while the item exists, then moves to a valid remaining item after deletion.
+- #123 - Clipboard popup hotkey. Pro `clipboard-popup-journey.dbtest.ts` registers the production
+ `CommandOrControl+Shift+C` shortcut, waits for renderer readiness, reuses the popup, and preserves
+ failed selection; rendered popup integration proves search, arrows, Enter restore, reopen reset,
+ and visible retry without mocking clipboard logic.
+- #129 - Runtime residency toggles persist. `e2e/settings-residency.spec.ts` changes image, STT,
+ and TTS through the real Settings controls, verifies production IPC, fully relaunches Electron,
+ and verifies all values reload. The SQLite and runtime-manager integrations prove that same map
+ controls persistence and re-warm behavior.
+- #127 - Vault copy actions. `e2e/pro.spec.ts` creates a real encrypted KDBX entry through
+ production IPC and verifies username, revealed password, and URL copy into the OS clipboard.
+- #130 - Chat residency stays required. `e2e/settings-residency.spec.ts` verifies the production
+ switch stays checked and disabled before and after relaunch, while the real SQLite integration
+ proves an on-demand write is normalized back to `resident`.
+- #131 - Resource mode applies. `resource-mode.integration.test.ts` drives all three presets through
+ the real LLM settings owner, disk persistence, fresh-service launch arguments, recommendation,
+ and setup planner with only host RAM controlled; the Electron tour proves selection stays
+ responsive and the sizing guards enforce memory clamps.
+- #133 - Storage usage is truthful. `storage-usage.integration.dbtest.ts` writes exact synthetic
+ model, capture, meeting, image, artifact, and thumbnail byte counts to a temp profile, then proves
+ production storage owners and the rendered Storage/Data Privacy panels report their real totals,
+ categories, models, and orphaned partials.
+- #139 - Invalid or exhausted license fails clearly. `licensing.integration.test.ts` drives invalid
+ and device-limit responses through the production service and proves entitlement remains false;
+ `UpgradeScreen.license.test.tsx` verifies distinct actionable messages remain on the locked screen.
+- #141 - Core and Pro override behavior. The licensing integration applies both development
+ overrides through the production bootstrap seam and proves neither mutates the encrypted persisted
+ entitlement; a core build remains incapable of force-loading Pro implementation code.
+- #142 - Manual update check. `update-check.integration.dbtest.ts` drives the rendered Settings
+ action through production updater IPC, real encrypted update preferences, and deterministic
+ updater events, proving current `0.0.103`, available `0.0.104`, and offline error states all leave
+ Checking, preserve the stable channel, and never call install without approval.
+- #143 - Update channel persists. `src/main/__tests__/settings-persistence.dbtest.ts` changes the
+ channel through the production update IPC handler, closes the encrypted database, reloads every
+ Off Grid module, and verifies the fresh update-preferences handler restores the beta channel.
+- #146 - Model ports are single-owner. `model-port-ownership.integration.test.ts` starts a real
+ foreign parent with the only fake native llama process on production port 8439, then proves the
+ production contender preserves that live owner, starts no second engine, reports its own Chat
+ health as Down rather than borrowing the other process's readiness, and exposes the actionable
+ `port_in_use` reason while the first engine remains responsive.
+- #149 - Large seeded collections stay usable. Core Electron and Pro rendered integrations drive
+ 120 models, 120 persisted chats, 120 entities, 300 clipboard items, and 120 observations through
+ their production owners, proving filters, scrolling, dense master-detail layouts, and bounded
+ result surfaces remain usable at desktop scale.
+- #150 - Window resize preserves desktop layout. `e2e/desktop-polish.spec.ts` resizes the real
+ Electron viewport from 1280 to 1800 pixels and proves the production Models collection expands
+ from three to four computed columns while its search/filter context remains reachable.
+- #151 - Keyboard focus is visible. `e2e/desktop-polish.spec.ts` tabs through the real sidebar,
+ Models form controls, model actions, primary Download action, and command-palette focus trap,
+ proving logical order and the settled theme-aware two-pixel focus treatment at each surface.
+- #154 - External links use the system browser. `e2e/tour.spec.ts` drives the real locked-Pro
+ surface through Electron preload and IPC, proves purchase and Mobile links reach
+ `shell.openExternal` with their production URLs, and verifies the Electron page never navigates.
+
+## Covered P2 journeys
+
+- #44 - Rename conversation. `conversation-rename.dbtest.ts` renders production chat against real
+ temp SQLite and proves scoped and unscoped rename update the sidebar and tab, survive remount,
+ reject blank or missing rows with retry state, and cancel with Escape.
+- #50 - Keyboard and navigation shortcuts. `App.navigation.integration.test.tsx` drives the real
+ shell through Project Beta to Integrations, then proves Cmd+[ and Cmd+] restore both routes and
+ the selected project instead of losing screen state.
+- #46 - Copy assistant reply. `MemoryChat.clipboard-overlay.test.tsx` invokes Copy on an assistant
+ message, proves its exact text reaches the native/browser clipboard boundary, and verifies visible
+ success feedback only after the copy completes.
+- #55 - Edit project. `rag-ipc-project-create.dbtest.ts` changes every editable field through the
+ production project IPC handlers and real SQLite store, reloads the modules, and proves the updated
+ project is returned with its exact name, description, prompt, icon, and memory setting.
+- #59 - Project list uses desktop layout. `e2e/projects-layout.spec.ts` seeds 12 projects and eight
+ chats through production IPC, then measures the real Electron master-detail geometry, scroll
+ reachability, adjacent detail controls, and a three-plus-column chat grid at desktop width.
+- #31 - Models use desktop density. `e2e/desktop-polish.spec.ts` resizes the real Electron window
+ from 1280 to 1800 pixels and proves the production model collection forms three then four computed
+ columns while its controls remain reachable.
+- #104 - Voice retention settings apply. `voice-journeys.dbtest.ts` persists a seven-day retention
+ setting, completes a new import, and proves expired SQLite rows and media are deleted while the
+ fresh recording and file remain.
+- #152 - Escape closes transient UI. Rendered integrations prove Models detail and nested shared
+ modals close only the top layer, preserve the underlying filter/workspace state, and restore
+ focus; the Electron journey confirms Escape returns to the intact collection.
+- #153 - Reduced motion remains usable. The Electron journey emulates macOS reduced motion, proves
+ production transition duration collapses to a near-zero value, and still opens and closes the
+ model detail layer normally.
+- #124 - Clipboard retention applies. `clipboard-store.integration.test.ts` exercises the real
+ retention-days and max-items policies against SQLite while preserving newer rows.
+
+## Left - package and install
+
+- #1 - Core DMG installs cleanly.
+- #2 - Pro DMG installs cleanly.
+
+## Next implementation order
+
+- Run #1 and #2 against the signed, notarized Core and Pro DMGs on the release Mac.
+- Complete the signed-device permission, global-hotkey, TextEdit paste, full-volume, and installer
+ replacement confirmations recorded beside their integration-covered journeys.
+- Resume the remaining P1 and P2 seams only after the P0 release-device pass, reusing the same real
+ harnesses rather than adding parallel mocks.
diff --git a/docs/RELEASE_DESKTOP.md b/docs/RELEASE_DESKTOP.md
index 9ff40ea4..6e2b533d 100644
--- a/docs/RELEASE_DESKTOP.md
+++ b/docs/RELEASE_DESKTOP.md
@@ -19,6 +19,7 @@ mobile licensing model (same Keygen account/product — a key works on both).
synchronously at load via the `pro:is-enabled` IPC (preload → `window.api.isPro`).
Env overrides (dev/contributor only):
+
- `OFFGRID_PRO=0` → force free even in a pro build.
- `OFFGRID_PRO=1` → force pro on **without** a license (for working on pro features).
- unset → the real paid path: license-gated.
@@ -41,6 +42,7 @@ git lfs pull # ensure resources/bin/* are real binaries, no
These builds are **unsigned** (no cert/Apple-ID prompts) and never touch GitHub.
### Smoke test
+
1. Open `dist/OffGrid-core-.dmg`, drag to /Applications, **right-click → Open**
(unsigned → Gatekeeper). Confirm: app launches, a model downloads + chat works,
pro tabs show the UpgradeScreen (no license box, since core).
@@ -73,6 +75,7 @@ Buy Pro on web (RevenueCat checkout)
```
Desktop licensing code (ported from mobile):
+
- `src/main/licensing/keygen-config.ts` — account/product/policy IDs, public key (non-secret).
- `src/main/licensing/keygen-client.ts` — Keygen REST (validate / activate / list / deactivate).
- `src/main/licensing/device-fingerprint.ts` — stable per-install id (userData file).
@@ -89,18 +92,21 @@ fingerprint and reclaim their slot.
> Not yet wired into CI as two artifacts — see TODO at the bottom. The mechanics:
### macOS (working today)
+
Signing uses the Apple Developer ID + notarization (`electron-builder.yml`
`mac.notarize: true`). CI secrets: `CSC_LINK`, `CSC_KEY_PASSWORD`,
`APPLE_API_KEY*`. Do **not** add an afterSign re-sign hook — it invalidates the
notarization staple (this was a past bug).
### Windows — PARKED
+
Windows is on hold and owned elsewhere (the bundled llama-server doesn't start in
the Windows binary; being fixed separately). Azure Trusted Signing setup (§4) is
kept below for when Windows resumes, but it is **not** on the current path. Focus
is macOS core + pro.
### Per-artifact config
+
Core and pro differ only by `OFFGRID_FORCE_CORE`, `productName`, `appId`, and
`artifactName` (see `scripts/build-mac-local.sh` for the exact overrides). They
must publish to **separate update channels** so electron-updater never feeds a
@@ -115,6 +121,7 @@ so a downloadable `.pfx` no longer works in CI. Azure Trusted Signing is the
cheapest CI-native option (~$10/mo) and electron-builder 25+ supports it natively.
### One-time setup (Azure portal — only the org owner can do this)
+
1. **Subscription + provider**: in an Azure subscription, register the
`Microsoft.CodeSigning` resource provider.
2. **Trusted Signing Account**: create one (region: East US / West US3 /
@@ -127,17 +134,19 @@ cheapest CI-native option (~$10/mo) and electron-builder 25+ supports it nativel
it the **"Trusted Signing Certificate Profile Signer"** role on the account.
### Values to collect
-| What | Where it goes |
-|------|----------------|
+
+| What | Where it goes |
+| ------------------------------------------------------ | ----------------------- |
| `endpoint` (e.g. `https://eus.codesigning.azure.net/`) | electron-builder config |
-| `codeSigningAccountName` | electron-builder config |
-| `certificateProfileName` | electron-builder config |
-| `publisherName` (exact validated org name) | electron-builder config |
-| `AZURE_TENANT_ID` | GitHub repo secret |
-| `AZURE_CLIENT_ID` | GitHub repo secret |
-| `AZURE_CLIENT_SECRET` | GitHub repo secret |
+| `codeSigningAccountName` | electron-builder config |
+| `certificateProfileName` | electron-builder config |
+| `publisherName` (exact validated org name) | electron-builder config |
+| `AZURE_TENANT_ID` | GitHub repo secret |
+| `AZURE_CLIENT_ID` | GitHub repo secret |
+| `AZURE_CLIENT_SECRET` | GitHub repo secret |
### electron-builder wiring (when values are in hand)
+
Add to the Windows config (electron-builder 25+ reads `AZURE_*` env via
DefaultAzureCredential and auto-downloads the Trusted Signing dlib):
@@ -145,10 +154,10 @@ DefaultAzureCredential and auto-downloads the Trusted Signing dlib):
win:
executableName: off-grid-ai
azureSignOptions:
- publisherName: ""
- endpoint: "https://.codesigning.azure.net/"
- certificateProfileName: ""
- codeSigningAccountName: ""
+ publisherName: ''
+ endpoint: 'https://.codesigning.azure.net/'
+ certificateProfileName: ''
+ codeSigningAccountName: ''
```
SmartScreen reputation accrues over downloads; Microsoft-backed certs gain trust
@@ -157,6 +166,7 @@ quickly. No EV needed.
---
## TODO before first paid release (macOS)
+
- [ ] Split `.github/workflows/release.yml` into **macOS** core + pro build jobs,
using `OFFGRID_FORCE_CORE` and the per-artifact overrides; separate update channels.
- [ ] Confirm the RevenueCat offering/products issue desktop-valid keys (they're
@@ -165,5 +175,6 @@ quickly. No EV needed.
(`license:list-devices` / `license:deactivate`).
### Parked (owned elsewhere)
+
- [ ] Windows: bundled llama-server doesn't start — fix in progress separately.
- [ ] Azure Trusted Signing validated + `azureSignOptions` wired (§4) — resumes with Windows.
diff --git a/docs/RELEASE_TEST_CHECKLIST.csv b/docs/RELEASE_TEST_CHECKLIST.csv
new file mode 100644
index 00000000..312c7aa2
--- /dev/null
+++ b/docs/RELEASE_TEST_CHECKLIST.csv
@@ -0,0 +1,156 @@
+#,Phase,What to test,How (Mac steps),Expected result,Priority,macOS Core,macOS Pro,Notes / annotations
+1,0 Package and install,Core DMG installs cleanly,"Build the core DMG, mount it, drag Off Grid AI Desktop to Applications, then open it",The app opens without a crash or white screen,P0,,,"Run from the packaged artifact, not npm run dev"
+2,0 Package and install,Pro DMG installs cleanly,"Build the Pro DMG, mount it, drag Off Grid AI Desktop to Applications, then open it",The app opens and remains locked until entitlement is present,P0,,,Pro artifact only; mark Core N/A
+3,0 Package and install,Fresh profile is truly fresh,"Move ~/Library/Application Support/Off Grid AI Desktop aside before first launch",Onboarding appears with no chats models captures or credentials from another run,P0,,,Use a disposable profile; never delete a real profile without a backup
+4,0 Package and install,Core and Pro artifact separation,Install and open each release artifact in turn,Core contains no Pro implementation; Pro exposes locked or entitled Pro screens as expected,P0,,,Release blocker for open-core packaging
+5,0 Package and install,Packaged helper binaries exist,Open System Health after installing the DMG,"llama-server, ffmpeg, whisper helpers, and required dylibs are present; no missing-binary error",P0,,,Check Console.app if a component is down
+6,0 Package and install,Packaged llama dependency closure,Start the chat model from the packaged app,The bundled llama-server launches without dyld or foreign Homebrew dependency errors,P0,,,Release blocker; CI-built engine path only
+7,0 Package and install,Upgrade preserves user data,Install the previous release; create data and settings; install this build over it,Chats models projects settings and entitlement remain intact,P0,,,Run as a separate pass from fresh install
+8,0 Package and install,Window identity and product name,Inspect the app name menu title About surface and installed application,Every visible name is Off Grid AI Desktop,P1,,,No legacy product names
+9,1 Onboarding and health,Fresh onboarding completes,Follow onboarding from the first screen through setup,The app lands in the desktop shell with no stuck or blank step,P0,,,Run on a fresh profile
+10,1 Onboarding and health,Configure for me completes,Choose the automatic setup path and let it finish,Recommended models download and active choices are populated,P0,,,Allow enough disk space and network access
+11,1 Onboarding and health,Manual setup path works,Choose models manually and complete onboarding,Only the chosen models are downloaded and the app becomes usable,P1,,,
+12,1 Onboarding and health,Onboarding resumes after relaunch,Quit midway through a download or setup step and reopen,The app resumes coherently without losing completed work or duplicating downloads,P1,,,
+13,1 Onboarding and health,System Health is truthful,Open System Health and compare each component with actual behavior,Healthy components work; failed components show the captured actionable reason,P0,,,Do not accept a generic Down label when stderr has a cause
+14,1 Onboarding and health,Chat engine stderr is surfaced,Temporarily use an incompatible or damaged model and start the engine,System Health reports the classified model or engine failure,P1,,,Restore a valid model after the check
+15,1 Onboarding and health,Required macOS permissions granted,Enable screen recording accessibility microphone and notifications when prompted,Each granted capability becomes healthy without repeated prompts,P0,,,Pro capture and dictation need their respective permissions
+16,1 Onboarding and health,Denied permission is recoverable,Deny one macOS permission then use its feature,A clear explanation and Settings route appear; the rest of the app stays usable,P1,,,Exercise screen recording and microphone separately
+17,2 Models and downloads,Text model downloads,Models -> choose a small chat model -> Download,Progress advances and the model becomes available for activation,P0,,,
+18,2 Models and downloads,Vision model downloads,Download a vision-capable GGUF and its projector,All required files complete and the model is not marked ready early,P1,,,
+19,2 Models and downloads,Speech model downloads,Download a Whisper or Parakeet model,The model becomes selectable for dictation and is not falsely shown as resident,P1,,,Pro use path; model management is core
+20,2 Models and downloads,TTS model downloads,Download the supported voice model,The model becomes selectable and can speak a reply,P1,,,Pro use path; model management is core
+21,2 Models and downloads,Image model downloads,Download an image model and wait for extraction,It becomes usable only after every required file and extraction step completes,P1,,,
+22,2 Models and downloads,Multiple downloads queue,Start more downloads than the concurrency limit,Running and queued counts agree; the queue drains without dropping an item,P1,,,
+23,2 Models and downloads,Delete does not cancel another download,Delete an installed model while another model is downloading,The in-flight download continues and only the selected model is removed,P1,,,
+24,2 Models and downloads,Offline download fails clearly,Disable network access and start a new model download,A connection error appears with a usable retry path and no phantom ready model,P1,,,Real network boundary
+25,2 Models and downloads,Interrupted download recovers,Quit the app during a download and reopen,The item resumes or becomes explicitly retryable; it never remains falsely ready,P0,,,
+26,2 Models and downloads,Truncated GGUF is rejected,Interrupt or substitute a file below the integrity floor,The file is not promoted to installed or loadable,P0,,,Adversarial release check
+27,2 Models and downloads,Disk write failure does not crash,Use a nearly full disposable volume or unwritable test location and start a download,The download fails; Off Grid AI Desktop stays open and other features remain usable,P0,,,Never risk a real data volume
+28,2 Models and downloads,Active text model survives relaunch,Activate a chat model; quit fully; reopen,The same model remains active and answers a new message,P0,,,
+29,2 Models and downloads,Active modal models survive relaunch,Activate image STT and TTS choices; quit fully; reopen,Each modality restores its own selection without cross-over,P1,,,
+30,2 Models and downloads,Deleting active model clears selection,Activate then delete a model for each available modality,No dangling active pointer remains; the UI asks for or selects a valid replacement,P1,,,
+31,2 Models and downloads,Models use desktop density,Resize the window across normal desktop widths,Model cards form a dense multi-column grid; controls stay beside their card content,P2,,,Visual check against docs/DESIGN.md
+32,3 Chat and conversations,First local message replies,Select a local text model; create a chat; send a prompt,A response streams into one assistant bubble and is persisted,P0,,,
+33,3 Chat and conversations,No memory scope works,Choose No memory and send a prompt,The reply uses the conversation only and the selected scope remains visible,P0,,,
+34,3 Chat and conversations,All memory scope works,Seed or capture memory; choose All memory; ask about it,The answer uses retrieved local context and citations where applicable,P0,,,A blank demo profile must be seeded before this check
+35,3 Chat and conversations,Empty memory degrades safely,On a truly fresh profile choose All memory and send a prompt,The app answers without context or shows a clear empty-state message; no generic crash bubble,P1,,,
+36,3 Chat and conversations,Thinking streams separately,Enable Thinking and ask a reasoning question,Reasoning appears in its own block while the final answer stays separate,P1,,,
+37,3 Chat and conversations,Plain reply hides think markers,Disable Thinking and send a plain prompt,No literal think tags or parser markers appear,P1,,,
+38,3 Chat and conversations,Stop before first token,Send a prompt and click Stop during search or classification,Work aborts promptly; no tool or side effect runs; the next send starts normally,P0,,,
+39,3 Chat and conversations,Stop during streaming,Click Stop after answer tokens appear,Streaming ends cleanly and the partial answer remains,P0,,,
+40,3 Chat and conversations,Queued message order,Send a second message while the first reply is still streaming,Messages run in order without collision duplication or loss,P1,,,
+41,3 Chat and conversations,Conversation switch isolation,Start work in conversation A then switch to B,B shows none of A's spinner progress or partial state; A completes against A's history,P0,,,
+42,3 Chat and conversations,Project switch isolation,Start a project-scoped generation then change the project selection,The turn and resulting artifacts stay attributed to the project captured at send time,P0,,,
+43,3 Chat and conversations,Chat survives relaunch,Create several conversations and fully quit,All conversations messages scopes and project associations restore correctly,P0,,,
+44,3 Chat and conversations,Rename conversation,Rename a conversation and navigate away and back,The new name persists everywhere it is shown,P2,,,
+45,3 Chat and conversations,Delete conversation cascades,Create a chat with messages and an artifact then delete it,The chat messages and chat-owned artifact disappear with no orphaned sidebar item,P1,,,
+46,3 Chat and conversations,Copy assistant reply,Use the copy action on an assistant message,Pasting into another app yields the expected message text,P2,,,Real clipboard boundary
+47,3 Chat and conversations,Regenerate reply,Regenerate an assistant answer,A new answer is produced from the same conversation context without duplicating the user turn,P1,,,
+48,3 Chat and conversations,Error clears busy state,Trigger a model or gateway failure during a turn,A useful error appears; spinner and Stop clear; another send can run,P0,,,
+49,3 Chat and conversations,Long answer respects configured cap,Raise the response limit and request a long answer,Output can pass the old cap and ends with a clear cutoff state if the new limit is reached,P1,,,
+50,3 Chat and conversations,Keyboard and navigation shortcuts,"Use Cmd+[ and Cmd+] after visiting several screens",Back and forward navigate through desktop view history without losing screen state,P2,,,
+51,4 Projects and artifacts,Create a project,Projects -> create a named project,The project appears and survives navigation and relaunch,P0,,,
+52,4 Projects and artifacts,Attach a knowledge document,Open a project and add a supported document,Indexing completes and the document appears once,P0,,,
+53,4 Projects and artifacts,Project retrieval is grounded,Start a chat inside the project and ask about the document,The answer uses relevant document content and does not invent an attachment,P0,,,
+54,4 Projects and artifacts,New chat inherits project,Create a new chat from inside a project,The conversation is filed under that project and retains its knowledge scope,P1,,,
+55,4 Projects and artifacts,Edit project,Change a project's name or details and save,The new values persist in every project surface,P2,,,
+56,4 Projects and artifacts,Delete project cascades,Create a project with chat messages documents and artifacts then delete it,Project-owned records disappear without phantom badges or orphaned artifacts,P0,,,
+57,4 Projects and artifacts,Text artifact saves and reopens,Generate or save a text artifact from chat then open it from its list,The exact content opens and remains after relaunch,P1,,,
+58,4 Projects and artifacts,Image artifact saves and reopens,Generate or save an image artifact then reopen it,The image renders correctly and remains associated with its source,P1,,,
+59,4 Projects and artifacts,Project list uses desktop layout,Open Projects with enough seeded items to fill the window,Collection uses multiple columns or an appropriate dense master-detail layout,P2,,,No single 1900px-wide card rows
+60,4 Projects and artifacts,Unsupported document fails clearly,Attach an unsupported or damaged file,The file is rejected with a specific reason and no half-indexed document remains,P1,,,
+61,5 Image and vision,Text prompt generates an image,Activate an image model; use image mode; send a prompt,Progress advances through generation and one image renders,P0,,,
+62,5 Image and vision,Image cancellation is scoped,Start image generation in chat A; switch to B; return and stop it,Only A owns the progress and cancellation; B remains idle,P0,,,
+63,5 Image and vision,Image cancellation keeps text,Trigger a tool turn that produces text then an image; cancel the image,The text answer remains and is still present after relaunch,P0,,,
+64,5 Image and vision,Image settings apply,Change size steps guidance and seed then generate,Output metadata and dimensions reflect the selected values,P1,,,
+65,5 Image and vision,Image runtime eviction recovers,Generate an image while the chat model is resident then send a chat prompt,The heavy model handoff completes and chat reloads without a dead server,P1,,,
+66,5 Image and vision,Image RAM guard is safe,Choose an image model that exceeds the configured memory budget,Generation is refused with a clear message unless the explicit override is used,P1,,,Use a disposable test case; do not induce system-wide OOM
+67,5 Image and vision,Generated image opens,Click a generated image thumbnail,The existing lightbox opens; close and save controls work,P1,,,
+68,5 Image and vision,Vision answers about attachment,Activate a vision model; attach an image; ask what it contains,The reply describes the attached image and preserves the text prompt,P0,,,
+69,5 Image and vision,Text-only model guards image input,Attach an image while a text-only model is active,The app does not send unsupported image content; it explains or proceeds text-only without engine garbage,P0,,,
+70,5 Image and vision,Damaged image fails safely,Attach a damaged or unsupported image,The app shows a specific error; the conversation remains usable,P1,,,
+71,6 Integrations and gateway,Connector can be added,Integrations -> add a reachable connector and complete its setup,It appears once with a truthful connected state,P0,,,
+72,6 Integrations and gateway,Connector tools load,Open a connected connector and inspect its tools,Real remote tools are listed with stable enable states,P1,,,
+73,6 Integrations and gateway,Connector tool executes,Enable a read-only connector tool and ask chat to use it,A real tool result appears and the final answer uses it,P0,,,Prefer a reversible read-only tool for release QA
+74,6 Integrations and gateway,Write tool requires approval,Ask a connector to perform a write action,An approval is required before the external side effect occurs,P0,,,Release blocker; use a disposable target
+75,6 Integrations and gateway,Stop prevents connector side effect,Start a turn likely to propose a write then stop before approval or execution,No message event or other write is created,P0,,,Verify at the external system
+76,6 Integrations and gateway,Expired connector becomes error,Revoke a connector token then trigger tool discovery,The connector changes to an error or reconnect state instead of silently losing tools,P0,,,
+77,6 Integrations and gateway,Dead connector does not hang all tools,Configure several connectors including one unreachable endpoint,Healthy connectors load concurrently and the turn proceeds within the timeout,P1,,,
+78,6 Integrations and gateway,Connector delete removes secrets,Delete a connector and relaunch,It stays deleted and its credential cannot still be used,P1,,,
+79,6 Integrations and gateway,Gateway models endpoint,Open Gateway and call GET /v1/models on 127.0.0.1:7878,A valid OpenAI-compatible response lists the active local model,P0,,,
+80,6 Integrations and gateway,Gateway chat streaming,Send an OpenAI-compatible streaming chat request to the local gateway,SSE chunks arrive and terminate cleanly,P0,,,
+81,6 Integrations and gateway,Gateway image route,Send a valid image generation request through the gateway,The request reaches the active image runtime and returns a usable image result,P1,,,
+82,6 Integrations and gateway,Gateway failure envelope,Call a route with no suitable model or invalid input,A stable non-HTML error envelope is returned and the app remains healthy,P1,,,
+83,7 Capture memory and replay,Screen capture permission path,Enable capture and grant macOS Screen Recording permission,New frames begin arriving without an app restart loop,P0,,,Pro only; mark Core N/A
+84,7 Capture memory and replay,Capture disabled means no capture,Turn capture off and work in another app for several minutes,No new replay frames or observations are created,P0,,,Pro only; privacy gate
+85,7 Capture memory and replay,OCR creates searchable memory,Display distinctive text in another app and allow capture processing,Search finds the text or its derived observation,P0,,,Pro only; allow processing time
+86,7 Capture memory and replay,Sensitive or excluded apps are omitted,Use a configured excluded or sensitive application while capture is on,Its content does not appear in replay search or extracted memory,P0,,,Pro only; privacy gate
+87,7 Capture memory and replay,Replay timeline renders,Open Replay after seeded or real synthetic capture,Frames appear in chronological order with usable timestamps,P0,,,Pro only
+88,7 Capture memory and replay,Replay playback uses media server,Open a replay session and scrub or play across several moments,Playback responds and media requests succeed on port 7879,P1,,,Pro only
+89,7 Capture memory and replay,Replay navigation preserves target,Open a replay hit from Search or Day,Replay opens at the selected moment rather than the start,P1,,,Pro only
+90,7 Capture memory and replay,Unified search finds each source,Search for seeded capture meeting entity and connector records,Each enabled source contributes relevant results,P0,,,Pro only
+91,7 Capture memory and replay,Search filters and sort apply,Toggle source filters and switch Relevant Recent and Match,Results and ordering update without stale rows,P1,,,Pro only
+92,7 Capture memory and replay,Day briefing renders,Open Day on a seeded profile,Priorities meetings suggestions journal time spent and timeline render from real stored data,P1,,,Pro only
+93,7 Capture memory and replay,Day links open correct records,Click a meeting entity action and replay moment from Day,Each opens the intended detail screen and record,P1,,,Pro only
+94,7 Capture memory and replay,Delete all removes capture corpus,Seed capture observations frames entities and replay data then use Delete all my data,Search Day Entities and Replay are empty after completion and relaunch,P0,,,Pro only; use synthetic data
+95,8 Meetings voice and dictation,Meeting detection is truthful,Join and leave a supported call while meeting auto-detect is enabled,Recording starts or prompts according to settings and stops after the call,P0,,,Pro only; real OS boundary
+96,8 Meetings voice and dictation,Manual meeting recording,Start and stop a meeting recording from Meetings,One recording is created with sane duration and no lingering mic/system capture,P0,,,Pro only
+97,8 Meetings voice and dictation,Meeting transcript and summary,Record a short synthetic meeting and let processing finish,Transcript matches the audio and summary/action items derive from it,P0,,,Pro only; do not use private meeting data
+98,8 Meetings voice and dictation,Meeting survives relaunch,Quit after a completed recording then reopen,Audio metadata transcript and summary remain accessible,P1,,,Pro only
+99,8 Meetings voice and dictation,Global dictation hotkey,"Set Hold Toggle or Both; use Option+Space in another app",Exactly one recording lifecycle occurs according to the selected mode,P0,,,Pro only; accessibility and microphone permissions required
+100,8 Meetings voice and dictation,Dictation pastes at cursor,Place the cursor in TextEdit; dictate a short phrase,The transcript is pasted once at the cursor and the saved recording contains the same text,P0,,,Pro only; real paste boundary
+101,8 Meetings voice and dictation,Dictation paste failure is visible,Remove Accessibility permission and dictate,The transcript is retained and a clear paste-permission error appears,P1,,,Pro only
+102,8 Meetings voice and dictation,Dictation engine selection,Switch between installed Whisper and Parakeet models and dictate,The selected engine runs and produces a transcript without caller-side engine branching,P1,,,Pro only
+103,8 Meetings voice and dictation,Import media for transcription,Drop a supported audio or video file into Voice,Processing finishes and a searchable recording with transcript is created,P1,,,Pro only
+104,8 Meetings voice and dictation,Voice retention settings apply,Set a recording or transcript retention limit and create old test records,Expired records are removed while newer ones remain,P2,,,Pro only
+105,8 Meetings voice and dictation,Speak assistant reply,Use the Speak action on an assistant response,Local TTS speaks clean text and does not read markdown syntax,P1,,,Requires installed TTS model
+106,8 Meetings voice and dictation,Mic and TTS stop cleanly,Start dictation or speech then stop or navigate away,Native audio activity ends and no background process keeps recording or speaking,P0,,,
+107,9 Entities actions and reflection,Entities are synthesized,Generate synthetic observations about a person project and company,Entities appear once with correct type aliases and supporting observations,P1,,,Pro only
+108,9 Entities actions and reflection,Self mentions are filtered,Seed the user's identity and an observation referring to self,No duplicate external person entity is created for the user,P1,,,Pro only
+109,9 Entities actions and reflection,Entity detail opens,Open an entity from Search Day or the graph,The same entity detail and related evidence appear across entry points,P1,,,Pro only
+110,9 Entities actions and reflection,Entity merge preserves evidence,Merge two duplicate synthetic entities,One entity remains with combined aliases observations and relationships,P1,,,Pro only; use disposable data
+111,9 Entities actions and reflection,Action items are extracted,Process a synthetic observation or meeting containing a commitment,One correctly worded action item appears with its source,P1,,,Pro only
+112,9 Entities actions and reflection,Approval queue gates actions,Open a proposed action then approve or reject it,Rejected actions do nothing; approved actions execute once and record status,P0,,,Pro only; use a reversible target
+113,9 Entities actions and reflection,Action status survives relaunch,Approve reject and dismiss separate synthetic items then reopen,Each status remains correct with no duplicate execution,P1,,,Pro only
+114,9 Entities actions and reflection,Notifications open their target,Click a meeting prep approval or to-do notification,The intended screen and record open; duplicate notifications are not created,P1,,,Pro only
+115,9 Entities actions and reflection,Reflect uses real time ranges,Open daily and weekly Reflect views on seeded data,Totals labels and source breakdowns match the selected date range,P1,,,Pro only
+116,9 Entities actions and reflection,CRM processing tolerates schema upgrades,Open an upgraded profile with older CRM tables,Migrations add missing columns once and CRM screens load without data loss,P0,,,Pro only; back up the fixture
+117,10 Clipboard and vault,Clipboard records text,Copy distinctive text in another app and open Clipboard,One searchable text item appears with the expected content,P0,,,Pro only
+118,10 Clipboard and vault,Clipboard deduplicates repeated copy,Copy the same text repeatedly,The existing item is refreshed according to policy instead of producing noisy duplicates,P1,,,Pro only
+119,10 Clipboard and vault,Clipboard records images and files,Copy a synthetic image and a disposable file,Each appears with the correct type and a usable preview or path,P1,,,Pro only
+120,10 Clipboard and vault,Clipboard live refresh keeps valid selection,Select an item while new clipboard events arrive,Selection stays on that item if it exists or moves to a valid item if it was removed,P1,,,Pro only; regression from this audit
+121,10 Clipboard and vault,Clipboard restore text,Choose a historical text item and restore it,Pasting in another app yields the exact text once,P0,,,Pro only
+122,10 Clipboard and vault,Clipboard restore file,Restore a historical copied file then paste into Finder and Terminal,"Finder receives the file URL; a text target receives its path",P0,,,Pro only
+123,10 Clipboard and vault,Clipboard popup hotkey,Press Cmd+Shift+C in another app,The compact popup opens; search and keyboard selection restore the chosen item,P1,,,Pro only
+124,10 Clipboard and vault,Clipboard retention applies,Set a short retention policy and use old synthetic rows,Expired history disappears without removing newer items,P2,,,Pro only
+125,10 Clipboard and vault,Create and unlock vault,Create a vault with a strong test master password then lock and unlock it,Correct password unlocks; incorrect password does not expose items,P0,,,Pro only; use synthetic secrets
+126,10 Clipboard and vault,Vault item types round-trip,Create a login app credential secure note API key and disposable secret file,Each saves reopens edits and deletes with the correct fields,P0,,,Pro only
+127,10 Clipboard and vault,Vault copy actions,Copy a synthetic username password and API key from Vault,The expected field reaches the clipboard and no other field is exposed,P1,,,Pro only
+128,10 Clipboard and vault,Vault recovery and backup,Export or locate the KDBX fixture then exercise the documented recovery flow,The vault remains encrypted and the supported recovery path restores access,P0,,,Pro only; never use a real credential vault
+129,11 Settings privacy licensing and updates,Runtime residency toggles persist,Change image STT and TTS residency then relaunch,Each setting persists and actual loading behavior matches it,P1,,,
+130,11 Settings privacy licensing and updates,Chat residency stays required,Open runtime residency settings,Chat model displays in-memory required and cannot be toggled off,P1,,,
+131,11 Settings privacy licensing and updates,Resource mode applies,Choose Conservative or another available resource preset,The selected limits and model behavior apply without freezing the UI,P1,,,
+132,11 Settings privacy licensing and updates,Settings survive relaunch,Change several model capture privacy update and Pro settings then quit fully,Every changed value restores from the owning store,P0,,,
+133,11 Settings privacy licensing and updates,Storage usage is truthful,Open Storage after downloading models and creating artifacts,Reported totals and per-category sizes roughly match files on disk,P1,,,
+134,11 Settings privacy licensing and updates,Clear cache preserves user data,Use the cache cleanup control then reopen the app,Ephemeral cache is removed while chats projects models and vault remain,P0,,,
+135,11 Settings privacy licensing and updates,Delete category is scoped,Delete one selected personal-data category,Only that category disappears; unrelated stores and credentials remain unless explicitly included,P0,,,Use synthetic data
+136,11 Settings privacy licensing and updates,Delete all is complete,Connect a test connector and seed core and Pro personal stores; run Delete all my data,Chats projects memory captures clipboard recordings connector tokens and personal rows are gone after relaunch,P0,,,Release blocker; use synthetic data only
+137,11 Settings privacy licensing and updates,Core locked Pro tabs,Run a core build and open every locked Pro navigation item,Each shows the correct Upgrade screen and no Pro implementation loads,P0,,,Core artifact only
+138,11 Settings privacy licensing and updates,Pro license activates,Run a Pro build without override; enter a valid test key; restart,Entitlement persists and Pro screens unlock,P0,,,Pro artifact only; use a test entitlement
+139,11 Settings privacy licensing and updates,Invalid or exhausted license fails clearly,Enter an invalid key and a key at its device limit,Activation explains the real reason and the app remains locked,P1,,,Pro artifact only
+140,11 Settings privacy licensing and updates,Offline entitlement behavior,Activate online; quit; disable network; reopen,The signed cached entitlement follows policy without exposing Pro on an unentitled profile,P0,,,Pro artifact only
+141,11 Settings privacy licensing and updates,Core and Pro override behavior,Launch dev builds with OFFGRID_PRO=0 and OFFGRID_PRO=1,Overrides force the documented free and Pro states without changing persisted entitlement,P1,,,Development check only
+142,11 Settings privacy licensing and updates,Manual update check,Settings -> Check for updates on stable,The current or available version is reported without a stuck checking state,P1,,,Packaged build only
+143,11 Settings privacy licensing and updates,Update channel persists,Toggle nightly builds then relaunch,The selected stable or beta channel remains selected and checks that channel,P1,,,Packaged build only
+144,12 Resilience and desktop polish,Local use works offline,Disable network after required models are installed,Chat image OCR replay search dictation and vault remain local; network features fail clearly,P0,,,
+145,12 Resilience and desktop polish,Cold relaunch after forced quit,Force quit during non-destructive activity then reopen,The app boots without a white screen or permanently busy state and committed data remains,P0,,,
+146,12 Resilience and desktop polish,Model ports are single-owner,Start one app instance then attempt a second development or capture instance,The conflict is diagnosed clearly and does not masquerade as model corruption,P1,,,Do not use this as a normal two-instance workflow
+147,12 Resilience and desktop polish,Engine restart recovers,Restart the local model engine while chat is open,Requests wait or fail clearly; the next request succeeds after health returns,P0,,,
+148,12 Resilience and desktop polish,Low disk space is handled,Use a safe disposable low-space volume for downloads and artifacts,Failures are contained and explained; existing data remains readable,P0,,,
+149,12 Resilience and desktop polish,Large seeded collections stay usable,Seed many models chats entities clipboard items and observations,Grids lists filters and detail panels remain responsive and dense,P1,,,Synthetic data only
+150,12 Resilience and desktop polish,Window resize preserves desktop layout,Resize from a normal laptop window to a wide desktop window,Collections gain columns and sticky context remains usable; no mobile-style stretched rows,P1,,,
+151,12 Resilience and desktop polish,Keyboard focus is visible,Tab through navigation forms dialogs and primary actions,Focus order is logical and a visible focus treatment is present,P1,,,
+152,12 Resilience and desktop polish,Escape closes transient UI,Open modals lightboxes menus and slide-over panels then press Escape,Only the top transient layer closes and underlying state is preserved,P2,,,
+153,12 Resilience and desktop polish,Reduced motion remains usable,Enable Reduce Motion in macOS and exercise panels and modals,Content remains reachable and transitions do not block interaction,P2,,,
+154,12 Resilience and desktop polish,External links use the system browser,Open purchase help and project links from the app,The expected HTTPS page opens externally and the Electron view does not navigate away,P1,,,
+155,12 Resilience and desktop polish,No private data in release evidence,Review screenshots videos logs and seeded profiles used for QA,Every publishable artifact contains synthetic data only,P0,,,Release blocker
diff --git a/docs/RELEASE_TEST_CHECKLIST.md b/docs/RELEASE_TEST_CHECKLIST.md
index db1b2381..85f59e07 100644
--- a/docs/RELEASE_TEST_CHECKLIST.md
+++ b/docs/RELEASE_TEST_CHECKLIST.md
@@ -3,6 +3,9 @@
Pure test-only additions are NOT listed here (they change no app behavior).
Updated as consolidation lands. Date started: 2026-07-09. -->
+> This is the historical checklist for one quality-hardening branch. The canonical,
+> reusable desktop release matrix is [`RELEASE_TEST_CHECKLIST.csv`](RELEASE_TEST_CHECKLIST.csv).
+
# Release Test Checklist — `feat/consolidation-and-coverage`
Everything here is behavior that a person should exercise on-device. Build + unit tests
@@ -14,8 +17,10 @@ passed typecheck/tests/build but was broken at runtime).
---
-## 1. Chat stop button (commit `feat(chat): stop button…`)
+## 1. Chat stop button (commit `feat(chat): stop button…`)
+
Highest-value manual check - this was the original bug.
+
- [ ] Ask a question with **All memory** + **Thinking** on. A red **stop** button appears next to Send **immediately** (during "Searching your memory…", before any tokens).
- [ ] Click stop during that pre-stream phase → generation aborts, no error bubble, no half-written answer.
- [ ] Ask again; let it start streaming tokens; click stop mid-stream → partial answer is kept, stream ends cleanly.
@@ -23,23 +28,28 @@ Highest-value manual check - this was the original bug.
- [ ] Queue a second message while one is generating, then stop → queued message is dropped, UI returns to idle.
- [ ] Image mode: start a generation, click **Stop** → image job cancels, no error bubble.
-## 2. ctxSize default (commit `refactor(config)…`, P0.3)
+## 2. ctxSize default (commit `refactor(config)…`, P0.3)
+
- [ ] Fresh profile / Settings → Model: context-window default now reads **16384** (was 32768 in UI while backend ran 16384).
- [ ] "Reset to defaults" sets context to 16384 and inference still runs (no engine restart failure).
-## 3. Engine ports (commit `refactor(config)…`, F1)
+## 3. Engine ports (commit `refactor(config)…`, F1)
+
Values are unchanged (8439/7878/7879) - this only de-duplicated the literals. Confirm no regression:
+
- [ ] App launches; chat model comes up (llama-server on **:8439**).
- [ ] Gateway reachable on **:7878** (Gateway screen / `curl 127.0.0.1:7878/v1/models`).
- [ ] Media playback (replay video) works (media server on **:7879**).
-## 4. Type-boundary fixes (commit `fix(types)…`, P0.1/P0.2)
+## 4. Type-boundary fixes (commit `fix(types)…`, P0.1/P0.2)
+
- [ ] Create a **project**, start a chat inside it, reload → the chat stays associated with the project (project_id no longer lost).
- [ ] Generate/save a **text** and an **image** artifact from chat → both save and reopen (saveArtifact union fix).
---
## 5. Consolidation refactors (behavior-preserving — smoke only)
+
Each is a DRY/SOLID dedup meant to change NO behavior. Listed so you can spot-check the paths they touched.
- **Transcription engine selection** (B3/C1 — `select.ts` dispatcher, `bin-resolution.ts`). Fallback order is unchanged (pickTranscription is byte-identical). Verify: with a Parakeet model active, dictation/file transcription routes to Parakeet; with a whisper ggml model active, whisper runs; STT resident mode still upgrades to the warm whisper-server and degrades to one-shot when the server binary is absent.
@@ -48,7 +58,9 @@ Each is a DRY/SOLID dedup meant to change NO behavior. Listed so you can spot-ch
- **Pro CRM helpers** (D1 extractJson + D4 today/hasColumn/notify, in the `pro` repo). Behavior-preserving per every call site; agent reported NO runtime-visible change. Low-risk, but if you exercise CRM/meetings/dictation LLM-extraction flows, confirm they still parse results and notifications still fire (skills notification body still truncates at 240 chars; proactive unchanged).
### Logic extraction (pure decision logic pulled out of I/O shells — behavior verbatim)
+
These moved pure logic into testable modules; the shell just imports and calls it. Smoke the paths:
+
- **Gateway request handling** (`model-server/*`): image generation with a size/aspect param, a vision request with a remote-URL image (still inlined to the model), async requests (`?async=true` -> 202 + poll), and an error response (e.g. no model installed -> correct error envelope). Chat message sanitization for Gemma still consolidates system messages.
- **LLM streaming** (`llm/sse-stream`, the highest-value path - this is the original-bug area): a chat with Thinking on streams token-by-token AND shows the reasoning bubble; content and reasoning stay separated; stop mid-stream keeps the partial. Payload shape unchanged (images, thinking flag).
- **Search ranking** (`search-ranking`): memory search returns sensibly ordered results (recency + relevance), citations resolve.
@@ -56,5 +68,6 @@ These moved pure logic into testable modules; the shell just imports and calls i
- **Image generation** (`imagegen/*`): generate an image on each runtime you have (sd-cli standard, Z-Image if present, Core ML if present) - same output/quality; the RAM guard still refuses an over-budget model with the same message; progress bar advances (sampling -> decoding); LoRA-on-quantized still refused; img2img still works. sd-server resident fast-path unchanged.
### NOT landed (recorded honestly)
+
- **D8 (pro clipboard TypeIcon dedup)** - the agent's work was lost (worktree auto-pruned before commit). No regression: the existing duplicated icon code remains and behaves as before. Re-do later; nothing to test.
- **D3 notification copy** - deliberately NOT applied. Notification timestamps keep their existing verbose format ("3 minutes ago"); the compact-format change was dropped to avoid a silent UX change.
diff --git a/docs/SYNC_PLAN.md b/docs/SYNC_PLAN.md
index a3dbace1..8120aa16 100644
--- a/docs/SYNC_PLAN.md
+++ b/docs/SYNC_PLAN.md
@@ -1,6 +1,6 @@
# Off Grid — Cross-Device Sync & Offload Plan
-> **Vision:** every Off Grid device is a window onto *all* of your information.
+> **Vision:** every Off Grid device is a window onto _all_ of your information.
> Open the phone, the laptop, the tablet — same chats, same projects, same
> memory, same search — and when a more capable device is nearby, heavy work
> (LLM inference, big search, media) transparently runs there. No cloud, no
@@ -8,7 +8,7 @@
> in vicinity.
>
> **And then it makes sense of all of it:** because every device ends up holding
-> the same unified corpus, the local model on *any* device can search, reason,
+> the same unified corpus, the local model on _any_ device can search, reason,
> and reflect across everything — every chat, project, and memory — no matter
> which device created it.
@@ -67,16 +67,16 @@ Status: **planning**. Nothing here is built yet except the pieces noted as
**Two traffic types, one transport:**
-| | Replication | Live RPC |
-|---|---|---|
-| **What** | chats, projects, memory | LLM offload, global search, media fetch |
-| **Pattern** | bidirectional op-log, converges | request/response (+ token streaming) |
+| | Replication | Live RPC |
+| ------------------ | -------------------------------------- | ---------------------------------------- |
+| **What** | chats, projects, memory | LLM offload, global search, media fetch |
+| **Pattern** | bidirectional op-log, converges | request/response (+ token streaming) |
| **Available when** | always (data is local on every device) | only when the target peer is in vicinity |
-| **Channel** | `@offgrid/sync` `app` channel `state` | `@offgrid/sync` `app` channel `rpc` |
+| **Channel** | `@offgrid/sync` `app` channel `state` | `@offgrid/sync` `app` channel `rpc` |
**Why tunnel RPC through `@offgrid/sync` instead of binding the gateway to
`0.0.0.0` + a token:** the gateway (`:7878`) and `universalSearch()` stay
-`127.0.0.1`-only; the desktop proxies tunneled requests to its *own* localhost.
+`127.0.0.1`-only; the desktop proxies tunneled requests to its _own_ localhost.
That gives us E2E encryption, no new attack surface, and reuses pairing + pro
gating for free. "In vicinity" is simply: the paired peer is visible on mDNS.
@@ -85,6 +85,7 @@ gating for free. "In vicinity" is simply: the paired peer is visible on mDNS.
## 3. What exists vs. what we build
### Already built (leverage)
+
- **`@offgrid/sync`** (`shared/packages/sync`): `SyncEngine`, `TransportBridge`
abstraction, NaCl crypto, passphrase challenge-response pairing, mDNS via
`bonjour-service`, generic `onAppMessage` channel, **Node** TCP + discovery
@@ -97,6 +98,7 @@ gating for free. "In vicinity" is simply: the paired peer is visible on mDNS.
licensing mirroring desktop (same account/product).
### Gaps to close (the actual work)
+
1. **RN `TransportBridge` adapter** for `@offgrid/sync` (only Node exists). ← critical path
2. **RN mDNS adapter** (browse/advertise `_offgrid._tcp`).
3. **Op-log replication layer** for chats + projects (extend ROADMAP 1.2/1.3
@@ -118,24 +120,24 @@ primitive is already cross-platform, so this is two adapter implementations
**Per-primitive support:**
-| Primitive | Desktop adapter (Electron/Node) | Mobile adapter (RN) |
-|---|---|---|
-| TCP transport | Node `net` (macOS ✅ / Windows ✅) | `react-native-tcp-socket` (iOS ✅ / Android ✅) |
+| Primitive | Desktop adapter (Electron/Node) | Mobile adapter (RN) |
+| -------------- | ----------------------------------------------------------------- | --------------------------------------------------------- |
+| TCP transport | Node `net` (macOS ✅ / Windows ✅) | `react-native-tcp-socket` (iOS ✅ / Android ✅) |
| mDNS discovery | `bonjour-service` — pure JS over UDP 5353 (macOS ✅ / Windows ✅) | `react-native-zeroconf` → iOS Bonjour ✅ / Android NSD ✅ |
-| Crypto (NaCl) | `tweetnacl` pure JS (all ✅) | `tweetnacl` + `react-native-get-random-values` (all ✅) |
-| Large media | HTTP-on-dynamic-port (all ✅) | HTTP-on-dynamic-port — bypasses RN bridge (all ✅) |
+| Crypto (NaCl) | `tweetnacl` pure JS (all ✅) | `tweetnacl` + `react-native-get-random-values` (all ✅) |
+| Large media | HTTP-on-dynamic-port (all ✅) | HTTP-on-dynamic-port — bypasses RN bridge (all ✅) |
So **one Node adapter covers macOS + Windows**, **one RN adapter covers iOS +
Android**. `@offgrid/sync`'s wire/crypto/pairing core is shared by both.
**Per-OS config & caveats (the only platform-specific work):**
-| OS | What's needed | Notes |
-|---|---|---|
-| **macOS** | nothing | Bonjour native; works today |
-| **Windows** | Firewall allow-rule for the app (inbound TCP + UDP 5353) | NSIS installer should add it, else first-listen prompt. mDNS binds 5353 with `SO_REUSEADDR` to coexist with the OS responder. **Desktop-as-offload-server also needs the Windows `llama-server` build, which is currently parked in CI** — replication/search work regardless; offloading *to* a Windows desktop waits on that binary. |
-| **iOS** | `NSLocalNetworkUsageDescription` + `NSBonjourServices` listing `_offgrid._tcp` | Declarative Info.plist entries; without them iOS 14+ hides the service. One-time OS permission prompt. |
-| **Android** | `WifiManager.MulticastLock` while browsing; `INTERNET` permission | `react-native-zeroconf` handles NSD; acquire/release the multicast lock around discovery for reliability. |
+| OS | What's needed | Notes |
+| ----------- | ------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| **macOS** | nothing | Bonjour native; works today |
+| **Windows** | Firewall allow-rule for the app (inbound TCP + UDP 5353) | NSIS installer should add it, else first-listen prompt. mDNS binds 5353 with `SO_REUSEADDR` to coexist with the OS responder. **Desktop-as-offload-server also needs the Windows `llama-server` build, which is currently parked in CI** — replication/search work regardless; offloading _to_ a Windows desktop waits on that binary. |
+| **iOS** | `NSLocalNetworkUsageDescription` + `NSBonjourServices` listing `_offgrid._tcp` | Declarative Info.plist entries; without them iOS 14+ hides the service. One-time OS permission prompt. |
+| **Android** | `WifiManager.MulticastLock` while browsing; `INTERNET` permission | `react-native-zeroconf` handles NSD; acquire/release the multicast lock around discovery for reliability. |
Conclusion: **all four platforms are supported from the existing two codebases.**
Per-OS deltas are config (plist / firewall / multicast lock), not forks.
@@ -145,6 +147,7 @@ Per-OS deltas are config (plist / firewall / multicast lock), not forks.
## 5. Data model & sync semantics
### Convergence model (ROADMAP 1.2)
+
- **Append-only op-log** per record type: each change is an op
`{ id, entity, entityId, field/patch, lamport, deviceId, ts }`.
- **Lamport clock** for causal ordering; **last-writer-wins** on `(lamport, deviceId)`
@@ -155,20 +158,22 @@ Per-OS deltas are config (plist / firewall / multicast lock), not forks.
(the message types ROADMAP already names for memory; reused here).
### ID alignment (must-do before bidirectional)
-| Record | Desktop today | Mobile today | Sync requirement |
-|---|---|---|---|
-| Conversation | `id TEXT` (UUID) ✅ | UUID ✅ | already compatible |
-| **Message** | `id INTEGER AUTOINCREMENT` ❌ | string id | **migrate to UUID** (autoincrement isn't mesh-safe) |
-| Project | `id TEXT` (UUID) ✅ | UUID ✅ | compatible |
-| Thread / project_message | mixed | mixed | give messages UUIDs |
+
+| Record | Desktop today | Mobile today | Sync requirement |
+| ------------------------ | ----------------------------- | ------------ | --------------------------------------------------- |
+| Conversation | `id TEXT` (UUID) ✅ | UUID ✅ | already compatible |
+| **Message** | `id INTEGER AUTOINCREMENT` ❌ | string id | **migrate to UUID** (autoincrement isn't mesh-safe) |
+| Project | `id TEXT` (UUID) ✅ | UUID ✅ | compatible |
+| Thread / project_message | mixed | mixed | give messages UUIDs |
Desktop migration: add a stable `uuid` to `messages` (or switch PK), keep the
old autoincrement for local FK joins. Mobile: messages already have ids; ensure
they're UUIDs. Both stamp `updated_at`/`lamport` on every write.
### What replicates vs. fetches on demand
+
- **Always replicate (small):** chats, projects, memory text/metadata, entities.
- → instant local search on *every* device.
+ → instant local search on _every_ device.
- **Fetch on demand (large):** capture frames/screenshots, recording media,
big files — pulled over the RPC/large-file path only when opened.
@@ -178,7 +183,8 @@ they're UUIDs. Both stamp `updated_at`/`lamport` on every write.
Each phase ends in a checkpoint mirroring the workspace ROADMAP (C4/C5).
-### Phase A — Pairing foundation *(unblocks everything)*
+### Phase A — Pairing foundation _(unblocks everything)_
+
**Goal:** any two of your devices discover each other, pair once, and hold an
encrypted session.
@@ -204,6 +210,7 @@ encrypted session.
passphrase, and exchange an encrypted ping. (= ROADMAP C1.1 across platforms.)
### Phase B — Chats + Projects replication (**bidirectional**)
+
**Goal:** create/edit a chat or project on any device; it appears everywhere.
- B1. Op-log schema + materializer (shared TS in `@offgrid/sync` or a new
@@ -220,6 +227,7 @@ passphrase, and exchange an encrypted ping. (= ROADMAP C1.1 across platforms.)
edit both offline, reconnect, both converge identically. (= ROADMAP C1.2 / C4.1.)
### Phase C — Global search + LLM offload (the "seamless offload")
+
**Goal:** search all your info from any device; run models on the best nearby
device automatically.
@@ -240,7 +248,8 @@ chat completion runs on the laptop's model automatically because it's nearby.
(= ROADMAP C5.1 + your offload goal.)
### Phase D — Cross-device intelligence ("make sense of all of it")
-**Goal:** any device reasons over the *whole* mesh's information as one corpus.
+
+**Goal:** any device reasons over the _whole_ mesh's information as one corpus.
- D1. Point each device's RAG / search / reflect at the **merged store** (the
replicated chats + projects + memory + entities), so the local model answers
@@ -250,14 +259,15 @@ chat completion runs on the laptop's model automatically because it's nearby.
- D3. Heavy reasoning auto-offloads to the most capable present peer (reuses the
Phase C tunnel) — e.g. the phone asks a question; the laptop's bigger model
answers over the unified corpus and streams back.
-- D4. (Ties to ROADMAP Phase 2B) only explicitly-shared, scoped *intelligence*
+- D4. (Ties to ROADMAP Phase 2B) only explicitly-shared, scoped _intelligence_
crosses devices — never raw frames by default.
**Checkpoint D:** ask a question on the phone that can only be answered from data
created on the laptop, and get a correct, cited answer — reasoned locally/offloaded,
never via cloud.
-### Phase E — Parity expansion *(ROADMAP Phase 5, later)*
+### Phase E — Parity expansion _(ROADMAP Phase 5, later)_
+
Mobile screen capture (ReplayKit / MediaProjection + Vision / ML Kit),
integrations, universal clipboard via the `@offgrid/clipboard` RN bridge — all
riding the same mesh. Brings mobile toward feature parity with desktop.
@@ -307,6 +317,7 @@ This is the part that has to feel magical, so it's explicit:
---
## 8. Security model
+
- **Pairing:** passphrase never leaves the device; only PBKDF2-style derived
proofs (existing `@offgrid/sync` crypto).
- **In transit:** every post-pairing frame is XSalsa20-Poly1305 (NaCl secretbox)
@@ -320,23 +331,26 @@ This is the part that has to feel magical, so it's explicit:
---
## 9. Risks & mitigations
-| Risk | Mitigation |
-|---|---|
-| iOS Local Network permission friction | Clear `NSLocalNetworkUsageDescription`; graceful prompt + fallback messaging |
-| Token streaming over framed encrypted channel | Stream deltas as small `app` frames (wire format already frames); backpressure aware |
-| Autoincrement message IDs break sync | UUID migration in Phase B before bidirectional |
-| Op-log divergence / clock skew | Lamport clock (not wall-clock) for ordering; LWW only as tiebreak |
-| Battery / radio on mobile | Pause browse in background; keepalive paused during transfers (EasyShare pattern) |
-| RN ↔ Node framing parity | Same `@offgrid/sync` wire code on both; conformance test across all 4 OSes |
-| Windows firewall blocks listen / mDNS | Installer adds an allow-rule (inbound TCP + UDP 5353); bind 5353 with `SO_REUSEADDR` to coexist with Windows' own mDNS responder |
-| Android drops multicast mDNS packets | Acquire `WifiManager.MulticastLock` while browsing; release when idle to save battery |
-| Offload to a Windows desktop | Gated on the parked Windows `llama-server` build; until then Windows is a sync/search peer, not an inference host |
-| Replicating huge capture corpus to phone | Replicate metadata/text only; frames/media fetched on demand |
+
+| Risk | Mitigation |
+| --------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
+| iOS Local Network permission friction | Clear `NSLocalNetworkUsageDescription`; graceful prompt + fallback messaging |
+| Token streaming over framed encrypted channel | Stream deltas as small `app` frames (wire format already frames); backpressure aware |
+| Autoincrement message IDs break sync | UUID migration in Phase B before bidirectional |
+| Op-log divergence / clock skew | Lamport clock (not wall-clock) for ordering; LWW only as tiebreak |
+| Battery / radio on mobile | Pause browse in background; keepalive paused during transfers (EasyShare pattern) |
+| RN ↔ Node framing parity | Same `@offgrid/sync` wire code on both; conformance test across all 4 OSes |
+| Windows firewall blocks listen / mDNS | Installer adds an allow-rule (inbound TCP + UDP 5353); bind 5353 with `SO_REUSEADDR` to coexist with Windows' own mDNS responder |
+| Android drops multicast mDNS packets | Acquire `WifiManager.MulticastLock` while browsing; release when idle to save battery |
+| Offload to a Windows desktop | Gated on the parked Windows `llama-server` build; until then Windows is a sync/search peer, not an inference host |
+| Replicating huge capture corpus to phone | Replicate metadata/text only; frames/media fetched on demand |
---
## 10. Decisions
+
**Resolved**
+
- Licensing/device cap → **Keygen only (5 machines)**; `@offgrid/sync` injects no
cap policy. Phone + laptop each activate the same key.
- **Scales per-license, not globally.** The cap is per license key (a
@@ -347,10 +361,11 @@ This is the part that has to feel magical, so it's explicit:
- Chats/projects → **bidirectional** from the start.
- Platforms → **all four: macOS + Windows (Electron) and iOS + Android (RN)**,
from the two existing codebases. Per-OS deltas are config only (plist /
- firewall / multicast lock). Caveat: offloading *to* a Windows desktop awaits
+ firewall / multicast lock). Caveat: offloading _to_ a Windows desktop awaits
the parked Windows `llama-server` binary; sync/search to/from Windows do not.
**Open (not blocking the plan; decide before Phase C)**
+
- Routing policy default: auto-offload whenever a desktop is present, or only
on Wi-Fi / when charging / above a model-size threshold?
- Search default: replicate-and-search-locally only, or always fan out live to
@@ -359,6 +374,7 @@ This is the part that has to feel magical, so it's explicit:
---
## 11. First step
+
Phase A1+A2: stand up the RN `TransportBridge` + mDNS adapters and prove a
desktop↔phone encrypted ping (Checkpoint A). Everything else builds on that
session. On your go, I'll expand Phase A into a file-by-file checklist (adapter
diff --git a/docs/WINDOWS_SUPPORT.md b/docs/WINDOWS_SUPPORT.md
index a7667622..b2c2dd26 100644
--- a/docs/WINDOWS_SUPPORT.md
+++ b/docs/WINDOWS_SUPPORT.md
@@ -16,48 +16,48 @@ at the bottom.
## Legend
-| Status | Meaning |
-|---|---|
-| 🟢 **Present — needs testing** | Cross-platform code path exists and any Windows-specific handling is implemented. Not yet verified on a real Windows machine. |
-| 🟡 **At risk — needs testing** | Implemented, but there's a known Windows-specific gap or fragile spot to confirm during testing (details in notes). |
-| 🔴 **Not present** | Not built / not wired for Windows yet. Work required. |
+| Status | Meaning |
+| --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
+| 🟢 **Present — needs testing** | Cross-platform code path exists and any Windows-specific handling is implemented. Not yet verified on a real Windows machine. |
+| 🟡 **At risk — needs testing** | Implemented, but there's a known Windows-specific gap or fragile spot to confirm during testing (details in notes). |
+| 🔴 **Not present** | Not built / not wired for Windows yet. Work required. |
| ⚪ **N/A — Apple-only by design** | Will never run on Windows (Apple Silicon / Core ML / macOS Vision). Feature degrades gracefully; a cross-platform fallback usually covers it. |
---
## 1. Build, packaging & distribution
-| Item | Status | Notes / evidence |
-|---|---|---|
-| Windows CI build | 🟢 | `.github/workflows/windows-build.yml` (`windows-2022`) builds + packages a branch artifact; `release.yml`'s `build-win` job publishes the installer + updater feed to the release. Verified by installing a build on real Windows hardware. |
-| Native-module compile (node-gyp) | 🟢 | Pinned toolchain: `windows-2022` (VS 2022) + Python 3.12. `windows-latest`/VS 2026 + Python 3.13 break node-gyp 11 — documented in the workflow. Covers `better-sqlite3-multiple-ciphers`, `node-llama-cpp`, `sharp`. |
-| Windows runtime binaries fetch | 🟢 | `scripts/fetch-win-binaries.ps1` pulls win64 `llama-server` / `whisper-cli` / `sd-cli` / `ffmpeg` (+ DLLs) from upstream GitHub releases at build time. **`llama-server` is pinned to `b9838`** (byte-for-byte parity with the macOS engine); `whisper-cli` / `sd-cli` / `ffmpeg` resolve dynamically from their latest upstream releases. Repo LFS binaries are macOS-only and skipped (`lfs: false`). Fails loud if `llama-server.exe` is missing. |
-| NSIS installer | 🟢 | `electron-builder.yml` → `win.executableName`, `nsis` block (desktop shortcut, uninstall name). Untested end-to-end. |
-| Code signing | 🟡 | Optional via `WIN_CSC_LINK` / `WIN_CSC_KEY_PASSWORD` secrets; **unset → unsigned build → SmartScreen will warn** on install. No cert configured yet. |
-| Auto-update | 🟢 | `electron-updater` is cross-platform (`src/main/updater.ts`); `release.yml`'s `build-win` job publishes `latest.yml` (stable) / `beta.yml` (nightly) to the release, so Windows installs self-update like macOS. |
+| Item | Status | Notes / evidence |
+| -------------------------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| Windows CI build | 🟢 | `.github/workflows/windows-build.yml` (`windows-2022`) builds + packages a branch artifact; `release.yml`'s `build-win` job publishes the installer + updater feed to the release. Verified by installing a build on real Windows hardware. |
+| Native-module compile (node-gyp) | 🟢 | Pinned toolchain: `windows-2022` (VS 2022) + Python 3.12. `windows-latest`/VS 2026 + Python 3.13 break node-gyp 11 — documented in the workflow. Covers `better-sqlite3-multiple-ciphers`, `node-llama-cpp`, `sharp`. |
+| Windows runtime binaries fetch | 🟢 | `scripts/fetch-win-binaries.ps1` pulls win64 `llama-server` / `whisper-cli` / `sd-cli` / `ffmpeg` (+ DLLs) from upstream GitHub releases at build time. **`llama-server` is pinned to `b9838`** (byte-for-byte parity with the macOS engine); `whisper-cli` / `sd-cli` / `ffmpeg` resolve dynamically from their latest upstream releases. Repo LFS binaries are macOS-only and skipped (`lfs: false`). Fails loud if `llama-server.exe` is missing. |
+| NSIS installer | 🟢 | `electron-builder.yml` → `win.executableName`, `nsis` block (desktop shortcut, uninstall name). Untested end-to-end. |
+| Code signing | 🟡 | Optional via `WIN_CSC_LINK` / `WIN_CSC_KEY_PASSWORD` secrets; **unset → unsigned build → SmartScreen will warn** on install. No cert configured yet. |
+| Auto-update | 🟢 | `electron-updater` is cross-platform (`src/main/updater.ts`); `release.yml`'s `build-win` job publishes `latest.yml` (stable) / `beta.yml` (nightly) to the release, so Windows installs self-update like macOS. |
---
## 2. Core runtime features (the studio)
-| Feature | Status | Depends on | Notes / evidence |
-|---|---|---|---|
-| **The Gateway** (OpenAI-compatible API on `:7878`) | 🟢 | llama-server + Node HTTP | No platform-specific code; rides on chat. Headless `--server-only` path is pure Node. |
-| **Chat** (text + vision + reasoning, streaming) | 🟢 | `llama-server.exe` | `src/main/llm.ts` is the most Windows-hardened path: `exe()` suffix, DLL-dir prepended to `PATH`, and orphan-port cleanup via `netstat`/`tasklist`/`taskkill`. Highest-confidence runtime. |
-| **Model catalog + Hugging Face download** | 🟢 | Node fetch | `@offgrid/models` + `models-manager.ts` — pure JS, downloads into userData. Path handling is cross-platform. |
-| **Image generation — SD/SDXL/Z-Image (GGUF)** | 🟡 | `sd-cli.exe` | `src/main/imagegen.ts` uses `exe('sd-cli')` and the fetch script ships the win build + DLLs. **Gap to check:** the spawn only sets `cwd = binary dir` (`imagegen.ts:628`) and, unlike `llm.ts`, does **not** prepend the bin dir to `PATH`. Relies on Windows' default "load DLLs from the exe's own directory" behaviour — verify SD DLLs resolve. |
-| ↳ Image gen — **MLX / FLUX.2 / Z-Image-via-MLX** | ⚪ | mflux (Apple MLX) | `src/main/mflux.ts` is explicitly Apple-Silicon-only (`process.platform !== 'darwin'` gated off). On Windows, MLX models are simply not offered; **Z-Image still works via the `sd-cli` GGUF path.** |
-| ↳ Image gen — **Core ML / ANE acceleration** | ⚪ | `coreml-sd` (Swift) | macOS-only, gated off in `imagegen.ts`. Windows uses the standard `sd-cli` path. |
-| **Voice — Speech→Text** (whisper.cpp) | 🟢 | `whisper-cli.exe` + `ffmpeg.exe` | `src/main/rag/extractors.ts` uses `exe('whisper-cli')` / `exe('ffmpeg')`; both fetched by the PS1 script. ffmpeg is a self-contained static build. Untested. |
-| **Voice — Text→Speech** (Kokoro-82M) | 🟢 | onnxruntime-node worker | `src/main/tts.ts` runs the worker as Electron-as-Node (`ELECTRON_RUN_AS_NODE=1`) with a cross-platform prebuilt ORT. No platform branching. Untested. |
-| **Hands-free voice mode** | 🟢 | STT + TTS above | Renderer orchestration only; inherits STT/TTS status. |
-| **Embeddings** (`@xenova/transformers`) | 🟢 | onnxruntime-node | Prebuilt native runtime, cross-platform. Powers RAG + `/v1/embeddings`. |
-| **Projects / RAG** (docs, cited retrieval) | 🟢 | LanceDB + better-sqlite3 | Text/PDF/DOCX extraction is pure JS; vector store is `@lancedb/lancedb` (native, compiled in CI). Image docs are captioned by the **vision model** (not OCR), so they're cross-platform. Audio/video ingestion inherits whisper/ffmpeg status. |
-| **Artifacts / canvas** (HTML/React/SVG/Mermaid) | 🟢 | Renderer only | Sandboxed iframe, no platform code. Very high confidence. |
-| **Connectors (MCP)** | 🟢 | stdio / HTTP transports | HTTP/SSE connectors are platform-neutral. stdio connectors spawn via `StdioClientTransport` (`src/main/mcp.ts:114`), whose SDK uses **`cross-spawn`** — which resolves `npx` → `npx.cmd` via `cmd.exe /c` on Windows automatically. The classic gotcha is already handled by the dependency; still worth a live test. |
-| **Tools in chat** (calculator, datetime, web search) | 🟢 | Node | `src/main/tools.ts` — pure JS. Web search is the one intentionally-online feature (DuckDuckGo fetch). |
-| **Encryption at rest** | 🟢 | `better-sqlite3-multiple-ciphers` | Native module compiled in CI (node-gyp). Cross-platform SQLite cipher. |
-| **Onboarding / Settings / Command palette / theming** | 🟢 | Renderer only | Pure web UI, no platform code. |
+| Feature | Status | Depends on | Notes / evidence |
+| ----------------------------------------------------- | ------ | --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| **The Gateway** (OpenAI-compatible API on `:7878`) | 🟢 | llama-server + Node HTTP | No platform-specific code; rides on chat. Headless `--server-only` path is pure Node. |
+| **Chat** (text + vision + reasoning, streaming) | 🟢 | `llama-server.exe` | `src/main/llm.ts` is the most Windows-hardened path: `exe()` suffix, DLL-dir prepended to `PATH`, and orphan-port cleanup via `netstat`/`tasklist`/`taskkill`. Highest-confidence runtime. |
+| **Model catalog + Hugging Face download** | 🟢 | Node fetch | `@offgrid/models` + `models-manager.ts` — pure JS, downloads into userData. Path handling is cross-platform. |
+| **Image generation — SD/SDXL/Z-Image (GGUF)** | 🟡 | `sd-cli.exe` | `src/main/imagegen.ts` uses `exe('sd-cli')` and the fetch script ships the win build + DLLs. **Gap to check:** the spawn only sets `cwd = binary dir` (`imagegen.ts:628`) and, unlike `llm.ts`, does **not** prepend the bin dir to `PATH`. Relies on Windows' default "load DLLs from the exe's own directory" behaviour — verify SD DLLs resolve. |
+| ↳ Image gen — **MLX / FLUX.2 / Z-Image-via-MLX** | ⚪ | mflux (Apple MLX) | `src/main/mflux.ts` is explicitly Apple-Silicon-only (`process.platform !== 'darwin'` gated off). On Windows, MLX models are simply not offered; **Z-Image still works via the `sd-cli` GGUF path.** |
+| ↳ Image gen — **Core ML / ANE acceleration** | ⚪ | `coreml-sd` (Swift) | macOS-only, gated off in `imagegen.ts`. Windows uses the standard `sd-cli` path. |
+| **Voice — Speech→Text** (whisper.cpp) | 🟢 | `whisper-cli.exe` + `ffmpeg.exe` | `src/main/rag/extractors.ts` uses `exe('whisper-cli')` / `exe('ffmpeg')`; both fetched by the PS1 script. ffmpeg is a self-contained static build. Untested. |
+| **Voice — Text→Speech** (Kokoro-82M) | 🟢 | onnxruntime-node worker | `src/main/tts.ts` runs the worker as Electron-as-Node (`ELECTRON_RUN_AS_NODE=1`) with a cross-platform prebuilt ORT. No platform branching. Untested. |
+| **Hands-free voice mode** | 🟢 | STT + TTS above | Renderer orchestration only; inherits STT/TTS status. |
+| **Embeddings** (`@xenova/transformers`) | 🟢 | onnxruntime-node | Prebuilt native runtime, cross-platform. Powers RAG + `/v1/embeddings`. |
+| **Projects / RAG** (docs, cited retrieval) | 🟢 | LanceDB + better-sqlite3 | Text/PDF/DOCX extraction is pure JS; vector store is `@lancedb/lancedb` (native, compiled in CI). Image docs are captioned by the **vision model** (not OCR), so they're cross-platform. Audio/video ingestion inherits whisper/ffmpeg status. |
+| **Artifacts / canvas** (HTML/React/SVG/Mermaid) | 🟢 | Renderer only | Sandboxed iframe, no platform code. Very high confidence. |
+| **Connectors (MCP)** | 🟢 | stdio / HTTP transports | HTTP/SSE connectors are platform-neutral. stdio connectors spawn via `StdioClientTransport` (`src/main/mcp.ts:114`), whose SDK uses **`cross-spawn`** — which resolves `npx` → `npx.cmd` via `cmd.exe /c` on Windows automatically. The classic gotcha is already handled by the dependency; still worth a live test. |
+| **Tools in chat** (calculator, datetime, web search) | 🟢 | Node | `src/main/tools.ts` — pure JS. Web search is the one intentionally-online feature (DuckDuckGo fetch). |
+| **Encryption at rest** | 🟢 | `better-sqlite3-multiple-ciphers` | Native module compiled in CI (node-gyp). Cross-platform SQLite cipher. |
+| **Onboarding / Settings / Command palette / theming** | 🟢 | Renderer only | Pure web UI, no platform code. |
---
@@ -66,7 +66,7 @@ at the bottom.
Ordered by likelihood of biting:
1. **Signing / SmartScreen.** The first release ships unsigned unless `WIN_CSC_LINK` / `WIN_CSC_KEY_PASSWORD` are set, so SmartScreen warns on install. Add a cloud-signing cert (e.g. Azure Trusted Signing) to clear it.
-2. **`sd-cli` DLL resolution** — `imagegen.ts:628` sets `cwd` but not `PATH` (chat's `llm.ts` does both). Windows searches the exe's own dir for DLLs by default, so this is *probably* fine; if SD fails to load its DLLs, mirror the `llm.ts` `PATH`-prepend fix (a few lines).
+2. **`sd-cli` DLL resolution** — `imagegen.ts:628` sets `cwd` but not `PATH` (chat's `llm.ts` does both). Windows searches the exe's own dir for DLLs by default, so this is _probably_ fine; if SD fails to load its DLLs, mirror the `llm.ts` `PATH`-prepend fix (a few lines).
3. **Upstream binary compatibility** — the fetched llama/whisper/sd builds are CPU/AVX2 x64 baselines; confirm they spawn (no missing VC++ redistributable, correct AVX level) on target hardware.
4. **Unsigned installer / SmartScreen** — expected until a signing cert is added; will scare testers.
5. **Cross-repo publish** - `build-win`'s `electron-builder --publish` and the `gh release upload` aliases must land on the same release as the macOS assets; verify on the first `release.yml` Windows run.
@@ -87,7 +87,7 @@ The Pro "sees / remembers / reflects / acts" layer is **not part of core** and i
present on Windows** (🔴). Its native binaries are macOS-only:
- Screen capture watcher (Swift) and meeting recorder — macOS binaries added by the Pro build.
-- **OCR** — `src/main/ocr.ts` shells out to a bundled **macOS Vision** binary; no Windows equivalent. (Note: this is *not* used by core image-RAG, which captions via the vision model.)
+- **OCR** — `src/main/ocr.ts` shells out to a bundled **macOS Vision** binary; no Windows equivalent. (Note: this is _not_ used by core image-RAG, which captions via the vision model.)
- macOS permissions (screen recording / accessibility) — `src/main/permissions.ts` is fully `darwin`-gated and no-ops elsewhere.
Porting Pro to Windows is a separate effort and not tracked in this matrix.
diff --git a/docs/WINDOWS_TEST_MODELS.md b/docs/WINDOWS_TEST_MODELS.md
index 6b09968e..6066e52b 100644
--- a/docs/WINDOWS_TEST_MODELS.md
+++ b/docs/WINDOWS_TEST_MODELS.md
@@ -16,11 +16,11 @@ and what test inputs to use. Model names below match the **Models** screen catal
Press **Win + Pause** (or Task Manager → Performance) to see installed RAM, then use the
smallest option that fits. Bigger = better quality but slower / needs more RAM.
-| Your RAM | Use the "Light" picks below | Can also try "Standard" picks |
-|---|---|---|
-| 8 GB | ✅ required | ⚠️ only the ~4GB image model, one at a time |
-| 16 GB | ✅ | ✅ |
-| 24 GB+ | ✅ | ✅ (plus the large models if you want) |
+| Your RAM | Use the "Light" picks below | Can also try "Standard" picks |
+| -------- | --------------------------- | ------------------------------------------- |
+| 8 GB | ✅ required | ⚠️ only the ~4GB image model, one at a time |
+| 16 GB | ✅ | ✅ |
+| 24 GB+ | ✅ | ✅ (plus the large models if you want) |
**Only ONE large model loads at a time.** Chat and image generation can't both be resident —
the app swaps them automatically, so don't be alarmed if starting an image generation pauses
@@ -32,13 +32,13 @@ chat briefly.
Download these **five** models first. Total ≈ **6.9 GB**.
-| Suite | Kind | Model name (in app) | Size | Min RAM |
-|---|---|---|---|---|
-| C — Chat | text | **Qwen 3.5 0.8B** | 0.53 GB | 3 GB |
-| C — Chat (vision) | vision | **Qwen3-VL 2B** | 1.9 GB (incl. vision file) | 6 GB |
-| E — Image gen | image | **SDXL Lightning (4-step)** | 4.1 GB | 8 GB |
-| F — Voice (speak) | voice | **Kokoro TTS 82M** | ~0.1 GB | 3 GB |
-| F — Voice (dictate) | transcription | **Whisper Base** | 0.15 GB | 3 GB |
+| Suite | Kind | Model name (in app) | Size | Min RAM |
+| ------------------- | ------------- | --------------------------- | -------------------------- | ------- |
+| C — Chat | text | **Qwen 3.5 0.8B** | 0.53 GB | 3 GB |
+| C — Chat (vision) | vision | **Qwen3-VL 2B** | 1.9 GB (incl. vision file) | 6 GB |
+| E — Image gen | image | **SDXL Lightning (4-step)** | 4.1 GB | 8 GB |
+| F — Voice (speak) | voice | **Kokoro TTS 82M** | ~0.1 GB | 3 GB |
+| F — Voice (dictate) | transcription | **Whisper Base** | 0.15 GB | 3 GB |
That set lets you run every core suite. Everything below is optional depth.
@@ -47,43 +47,48 @@ That set lets you run every core suite. Everything below is optional depth.
## 2. Per-suite model picks (with alternatives)
### Suite C — Chat (text)
-| Role | Model name | HF repo | Size | Notes |
-|---|---|---|---|---|
-| **Light (start here)** | **Qwen 3.5 0.8B** | `unsloth/Qwen3.5-0.8B-GGUF` | 0.53 GB | Tiny + fast; best first smoke test — proves `llama-server.exe` runs. |
-| Standard | Qwen 3.5 4B | `unsloth/Qwen3.5-4B-GGUF` | 2.7 GB | Better answers; use if you have ≥8 GB. |
-| Reasoning check (TC-CHAT-03) | Qwen 3.5 2B or 4B | — | — | These support "thinking" mode; use one of them for the reasoning test. |
-
-### Suite C — Chat with images (vision) → also used by TC-CHAT-04 & TC-PROJ-04
-| Role | Model name | HF repo | Size | Notes |
-|---|---|---|---|---|
-| **Light (start here)** | **Qwen3-VL 2B** | `unsloth/Qwen3-VL-2B-Instruct-GGUF` | 1.9 GB | Downloads the vision add-on automatically. Lightest capable vision model. |
-| Alternative light | SmolVLM2 2.2B | `ggml-org/SmolVLM2-2.2B-Instruct-GGUF` | 2.0 GB | Equivalent; use if Qwen3-VL misbehaves. |
-| Standard | Gemma 4 E4B | `unsloth/gemma-4-E4B-it-GGUF` | 6.0 GB | Higher quality vision + thinking; needs more RAM. |
+
+| Role | Model name | HF repo | Size | Notes |
+| ---------------------------- | ----------------- | --------------------------- | ------- | ---------------------------------------------------------------------- |
+| **Light (start here)** | **Qwen 3.5 0.8B** | `unsloth/Qwen3.5-0.8B-GGUF` | 0.53 GB | Tiny + fast; best first smoke test — proves `llama-server.exe` runs. |
+| Standard | Qwen 3.5 4B | `unsloth/Qwen3.5-4B-GGUF` | 2.7 GB | Better answers; use if you have ≥8 GB. |
+| Reasoning check (TC-CHAT-03) | Qwen 3.5 2B or 4B | — | — | These support "thinking" mode; use one of them for the reasoning test. |
+
+### Suite C — Chat with images (vision) → also used by TC-CHAT-04 & TC-PROJ-04
+
+| Role | Model name | HF repo | Size | Notes |
+| ---------------------- | --------------- | -------------------------------------- | ------ | ------------------------------------------------------------------------- |
+| **Light (start here)** | **Qwen3-VL 2B** | `unsloth/Qwen3-VL-2B-Instruct-GGUF` | 1.9 GB | Downloads the vision add-on automatically. Lightest capable vision model. |
+| Alternative light | SmolVLM2 2.2B | `ggml-org/SmolVLM2-2.2B-Instruct-GGUF` | 2.0 GB | Equivalent; use if Qwen3-VL misbehaves. |
+| Standard | Gemma 4 E4B | `unsloth/gemma-4-E4B-it-GGUF` | 6.0 GB | Higher quality vision + thinking; needs more RAM. |
> A "vision" model is what lets chat **see attached images**. Without one, TC-CHAT-04 and the
> image parts of Projects can't work — that's expected, not a bug.
### Suite E — Image generation
-| Role | Model name | HF repo | Size | Notes |
-|---|---|---|---|---|
-| **Start here** | **SDXL Lightning (4-step)** | `mzwing/SDXL-Lightning-GGUF` | 4.1 GB | Single file, "Recommended" — simplest path to prove `sd-cli.exe` + its DLLs load on Windows. **Do this one first.** |
-| Fastest drafts | SDXL Turbo (fast drafts) | `OlegSkutte/sdxl-turbo-GGUF` | 4.1 GB | 1–4 step quick drafts. |
-| **Advanced (test 2nd)** | Z-Image Turbo (2026) | `leejet/Z-Image-Turbo-GGUF` | ~6.7 GB total | Flagship, but uses a **multi-file** pipeline (downloads a text-encoder + VAE too). Because it's a more complex spawn, test it **after** SDXL Lightning succeeds — if Lightning works and Z-Image doesn't, note that difference. |
-| img2img (TC-IMG-03) | SDXL Lightning or any SDXL above | — | — | The SDXL models support image-to-image; Z-Image is txt2img only. |
+
+| Role | Model name | HF repo | Size | Notes |
+| ----------------------- | -------------------------------- | ---------------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| **Start here** | **SDXL Lightning (4-step)** | `mzwing/SDXL-Lightning-GGUF` | 4.1 GB | Single file, "Recommended" — simplest path to prove `sd-cli.exe` + its DLLs load on Windows. **Do this one first.** |
+| Fastest drafts | SDXL Turbo (fast drafts) | `OlegSkutte/sdxl-turbo-GGUF` | 4.1 GB | 1–4 step quick drafts. |
+| **Advanced (test 2nd)** | Z-Image Turbo (2026) | `leejet/Z-Image-Turbo-GGUF` | ~6.7 GB total | Flagship, but uses a **multi-file** pipeline (downloads a text-encoder + VAE too). Because it's a more complex spawn, test it **after** SDXL Lightning succeeds — if Lightning works and Z-Image doesn't, note that difference. |
+| img2img (TC-IMG-03) | SDXL Lightning or any SDXL above | — | — | The SDXL models support image-to-image; Z-Image is txt2img only. |
> **Image gen is the #1 Windows risk area.** If generation fails, grab the console text
> (per the test plan) — a `.dll` / library error here is exactly what we're hunting for.
### Suite F — Voice
-| Role | Model name | HF repo | Size | Notes |
-|---|---|---|---|---|
-| **Text-to-speech (speak)** | **Kokoro TTS 82M** | `onnx-community/Kokoro-82M-v1.0-ONNX` | ~0.1 GB | Default voice; used by TC-VOICE-01 / 03. |
-| TTS alternative | Piper – Lessac (English) | `rhasspy/piper-voices` | 0.06 GB | Use only if Kokoro fails. |
-| **Speech-to-text (dictate)** | **Whisper Base** | `ggerganov/whisper.cpp` (base) | 0.15 GB | Default for TC-VOICE-02 / 03. Proves `whisper-cli.exe` + `ffmpeg.exe`. |
-| STT lightest | Whisper Tiny | `ggerganov/whisper.cpp` (tiny) | 0.08 GB | Fastest, lower accuracy — fine for a functional test. |
-| STT best | Whisper Large v3 Turbo | `ggerganov/whisper.cpp` (large-v3-turbo) | 1.6 GB | Only if you want to check accuracy on ≥6 GB RAM. |
+
+| Role | Model name | HF repo | Size | Notes |
+| ---------------------------- | ------------------------ | ---------------------------------------- | ------- | ---------------------------------------------------------------------- |
+| **Text-to-speech (speak)** | **Kokoro TTS 82M** | `onnx-community/Kokoro-82M-v1.0-ONNX` | ~0.1 GB | Default voice; used by TC-VOICE-01 / 03. |
+| TTS alternative | Piper – Lessac (English) | `rhasspy/piper-voices` | 0.06 GB | Use only if Kokoro fails. |
+| **Speech-to-text (dictate)** | **Whisper Base** | `ggerganov/whisper.cpp` (base) | 0.15 GB | Default for TC-VOICE-02 / 03. Proves `whisper-cli.exe` + `ffmpeg.exe`. |
+| STT lightest | Whisper Tiny | `ggerganov/whisper.cpp` (tiny) | 0.08 GB | Fastest, lower accuracy — fine for a functional test. |
+| STT best | Whisper Large v3 Turbo | `ggerganov/whisper.cpp` (large-v3-turbo) | 1.6 GB | Only if you want to check accuracy on ≥6 GB RAM. |
### Suites G/H/J/K (Projects, Artifacts, Tools, Settings)
+
No extra models needed — they reuse the **text** model from Suite C (and the **vision** model
for image documents in TC-PROJ-04). Web search (TC-TOOL-02) needs internet but no model.
@@ -93,15 +98,15 @@ for image documents in TC-PROJ-04). Web search (TC-TOOL-02) needs internet but n
Put these in a folder like `C:\OffGridTest\` so they're easy to find in file pickers.
-| File | For test | How to make it |
-|---|---|---|
-| `budget.txt` | TC-PROJ-02 | A plain text file containing exactly: `The Q3 project budget is $5,000 and the deadline is March 14.` |
-| `budget.pdf` | TC-PROJ-02 (alt) | Same text saved/printed as a PDF (to test PDF extraction too). |
-| `photo.jpg` | TC-CHAT-04 / TC-PROJ-04 | Any clear photo — e.g. a picture of a **dog on grass**, or a screenshot with visible text. |
-| `sign.png` | TC-PROJ-04 | An image containing readable text (a sign, a slide) — checks the model reads text from images. |
-| short `speech.wav`/`.mp3` | TC-PROJ-04 (audio) | Record ~10 s of you saying a sentence, or grab any short clip. |
+| File | For test | How to make it |
+| ------------------------- | ----------------------- | ----------------------------------------------------------------------------------------------------- |
+| `budget.txt` | TC-PROJ-02 | A plain text file containing exactly: `The Q3 project budget is $5,000 and the deadline is March 14.` |
+| `budget.pdf` | TC-PROJ-02 (alt) | Same text saved/printed as a PDF (to test PDF extraction too). |
+| `photo.jpg` | TC-CHAT-04 / TC-PROJ-04 | Any clear photo — e.g. a picture of a **dog on grass**, or a screenshot with visible text. |
+| `sign.png` | TC-PROJ-04 | An image containing readable text (a sign, a slide) — checks the model reads text from images. |
+| short `speech.wav`/`.mp3` | TC-PROJ-04 (audio) | Record ~10 s of you saying a sentence, or grab any short clip. |
-For the doc-grounding test (TC-PROJ-02), the expected AI answer to *"What is the budget?"* is
+For the doc-grounding test (TC-PROJ-02), the expected AI answer to _"What is the budget?"_ is
**"$5,000"** with a cited source — that specific number is why we plant it in the file.
---
@@ -113,6 +118,7 @@ server — it's public and needs no login (first launch downloads the package, s
on for this test).
**TC-INT-02 — add this stdio connector:**
+
- Sidebar → **Integrations** → add connector → choose the **command** option.
- **Name:** `Filesystem`
- **command:** `npx`
@@ -123,6 +129,7 @@ on for this test).
exactly; that's the specific Windows behavior we're verifying.
**TC-INT-03 — use it in chat:**
+
- In a chat (with a text model loaded), ask: `List the files in C:\OffGridTest using your tools.`
- **Expected:** the AI calls the filesystem tool and lists `budget.txt`, `photo.jpg`, etc.
@@ -145,4 +152,4 @@ URL fails gracefully. Don't file "couldn't connect" as a bug without a real endp
5. (Optional) **Z-Image Turbo** → advanced image test.
If a download itself fails or hangs, that's a **Suite B (Models)** bug — report it with the
-model name and console output, and move on to whatever you *can* test.
+model name and console output, and move on to whatever you _can_ test.
diff --git a/docs/WINDOWS_TEST_PLAN.md b/docs/WINDOWS_TEST_PLAN.md
index 617f5bc5..2fb97f57 100644
--- a/docs/WINDOWS_TEST_PLAN.md
+++ b/docs/WINDOWS_TEST_PLAN.md
@@ -1,7 +1,7 @@
# Off Grid AI — Windows Test Plan (Core)
**For the tester:** You don't need to know this product beforehand. This doc tells you what
-each feature is, exactly what to click, and what *should* happen. Your job is to run each
+each feature is, exactly what to click, and what _should_ happen. Your job is to run each
test case on Windows and record **Pass / Fail / Blocked**, and file any problem using the
[bug format](#3-how-to-report-a-bug) below.
@@ -28,12 +28,13 @@ Everything happens locally, so the **first time you use a feature you usually ha
download a model** for it. That's expected.
### The window layout
+
- A **left sidebar** with these items: **Projects, Chat, Integrations, Models, Gateway,
Settings**. (You navigate by clicking these.)
- Some sidebar items may show a **lock icon or an "Upgrade / Pro" screen** when clicked
(e.g. Day, Replay, Reflect, Meetings, Actions). **These are "Pro" features and are OUT
OF SCOPE — skip them.** Only test the items listed above.
-- The app should work **fully offline**. The *only* feature that intentionally uses the
+- The app should work **fully offline**. The _only_ feature that intentionally uses the
internet is downloading models and "web search" in chat.
---
@@ -51,6 +52,7 @@ GPU (if any) : __________
```
### How to capture logs (do this — most bugs are useless without logs)
+
The packaged app hides its internal logs by default. To see them, **launch it from a
terminal so its output prints there:**
@@ -59,16 +61,18 @@ terminal so its output prints there:**
```powershell
& "$env:LOCALAPPDATA\Programs\off-grid-ai\off-grid-ai.exe"
```
- (If it installed elsewhere, right-click the desktop shortcut → *Open file location* to
+ (If it installed elsewhere, right-click the desktop shortcut → _Open file location_ to
find the `.exe`, then run that path.)
3. Leave this PowerShell window open — **error messages and `[llama-server]` / `[OCR]` /
`[update]` lines print here.** Copy/paste relevant lines into your bug report.
**Where app data lives** (models, database, generated images) — useful to attach or clear:
+
```
%APPDATA%\Off Grid AI (try this first)
%APPDATA%\off-grid-ai (fallback)
```
+
Open by pasting that into the File Explorer address bar.
---
@@ -99,19 +103,22 @@ Notes : anything else (e.g. "worked after restarting app")
```
### Severity rubric
-| Severity | Use when… |
-|---|---|
-| **Blocker** | The app won't install/launch, or a whole feature is completely unusable and has no workaround. |
-| **High** | A core feature fails or gives wrong results, but other features work. |
-| **Medium** | Feature works but is broken in a noticeable way (bad layout, slow, confusing error, minor data issue). |
-| **Low** | Cosmetic: typo, misalignment, wrong icon, polish. |
+
+| Severity | Use when… |
+| ----------- | ------------------------------------------------------------------------------------------------------ |
+| **Blocker** | The app won't install/launch, or a whole feature is completely unusable and has no workaround. |
+| **High** | A core feature fails or gives wrong results, but other features work. |
+| **Medium** | Feature works but is broken in a noticeable way (bad layout, slow, confusing error, minor data issue). |
+| **Low** | Cosmetic: typo, misalignment, wrong icon, polish. |
### Special things to flag loudly (Windows-specific red flags)
+
If you see any of these, note it prominently — they're the failures we most expect:
+
- A popup or console error mentioning **`.dll`**, **"VCRUNTIME"**, **"MSVCP"**, **"was not
found"**, or **"is not a valid Win32 application"** → a bundled AI binary failed to load.
- **SmartScreen / "Windows protected your PC"** warning during install → expected (build is
- unsigned) — just note it happened; click *More info → Run anyway* to continue.
+ unsigned) — just note it happened; click _More info → Run anyway_ to continue.
- A feature spins forever / never responds → capture the console and say which feature.
- After closing the app, a leftover **`llama-server.exe`** in Task Manager (see TC-STAB-02).
@@ -127,110 +134,138 @@ something earlier failed) · ⏭️ Skipped.
---
-### Suite A — Install & first launch `P0`
+### Suite A — Install & first launch `P0`
**What it proves:** the installer works and the app opens on Windows.
**TC-INSTALL-01 — Install the app**
+
1. Double-click the `-setup.exe` installer.
-2. If **"Windows protected your PC" (SmartScreen)** appears → click *More info* → *Run
- anyway*. (Note in your report that it appeared — expected for now.)
+2. If **"Windows protected your PC" (SmartScreen)** appears → click _More info_ → _Run
+ anyway_. (Note in your report that it appeared — expected for now.)
3. Complete the installer.
+
- **Expected:** Installs without error; a desktop shortcut named **Off Grid AI** is created.
**TC-INSTALL-02 — First launch & onboarding**
+
1. Launch the app (ideally from PowerShell per section 2 so you capture logs).
2. Observe the first-run **onboarding** screen(s); click the **Continue / Next** button
through to the end.
+
- **Expected:** A welcome/onboarding screen appears, then you land on the main app on the
**Models** screen. No crash, no blank white window.
**TC-INSTALL-03 — Window & navigation**
+
1. Click each sidebar item that is in scope: **Projects, Chat, Integrations, Models,
Gateway, Settings.**
+
- **Expected:** Each opens its screen without crashing. (Locked/Pro tabs showing an upgrade
screen are fine — skip them.)
---
-### Suite B — Models (download a model) `P0`
+### Suite B — Models (download a model) `P0`
**What it proves:** the app can download an AI model — nothing else works without one.
**TC-MODEL-01 — Browse the catalog**
+
1. Sidebar → **Models**.
2. Look at the recommended models list (grouped by size, e.g. "Fits in …").
+
- **Expected:** A list of models renders. No blank screen or error.
**TC-MODEL-02 — Download a small text model**
+
1. In **Models**, pick a **small** recommended chat/text model (smallest available, so the
download is quick).
2. Click its **Download** button. Watch the progress bar.
+
- **Expected:** Download progresses to 100% and the model shows as installed/active. A
**Cancel** control is available during download.
- **Watch for:** stuck at 0%, network error, or the file downloads but never becomes
"ready."
**TC-MODEL-03 — Hugging Face search (uses internet)**
+
1. In **Models**, use the search to look up a model by name (e.g. type "qwen").
+
- **Expected:** Search returns results you could download.
---
-### Suite C — Chat `P0`
+### Suite C — Chat `P0`
**What it proves:** the core AI text engine (`llama-server.exe`) runs on Windows. This is
the single most important suite.
**TC-CHAT-01 — Send a text message**
+
1. Sidebar → **Chat**. Start a new chat.
2. Type `Hello, who are you?` and send.
+
- **Expected:** The AI replies with text that **streams in word-by-word**. Reply is coherent.
- **Watch for (Windows red flag):** no reply at all + a `.dll` / "llama-server" error in the
PowerShell console = the AI binary failed to load. **Report as Blocker.**
**TC-CHAT-02 — Multi-turn conversation**
+
1. After the reply, send a follow-up like `Summarize that in one sentence.`
+
- **Expected:** The AI responds in context (remembers the previous message).
**TC-CHAT-03 — Reasoning / "thinking" mode**
+
1. If there's a **reasoning / thinking** toggle for the chat, enable it and ask a
reasoning question (e.g. `If a train travels 60km in 45 minutes, what is its speed?`).
+
- **Expected:** You may see a separate "thinking" section, then a final answer. Answer is
correct (80 km/h).
-**TC-CHAT-04 — Vision (attach an image)** — *requires a vision-capable model*
+**TC-CHAT-04 — Vision (attach an image)** — _requires a vision-capable model_
+
1. Download a **vision** model from Models if prompted (one that supports images).
2. In a chat, attach an image file (e.g. a photo or screenshot) and ask
`What's in this image?`.
+
- **Expected:** The AI describes the image contents.
- **Watch for:** attach button does nothing, or the model errors on the image.
**TC-CHAT-05 — Per-chat settings**
+
1. Open the chat's settings (temperature, context window) and change the context window.
+
- **Expected:** Setting saves; chat continues to work afterward (the model restarts quietly).
---
-### Suite D — Gateway (local API) `P0`
+### Suite D — Gateway (local API) `P0`
**What it proves:** the local OpenAI-compatible web API works — a key selling point.
**TC-GW-01 — Gateway screen**
+
1. Sidebar → **Gateway**.
+
- **Expected:** Shows a **Base URL** (e.g. `http://127.0.0.1:7878/v1`) and a list of
**Endpoints**.
**TC-GW-02 — Call the API from PowerShell**
+
1. With a chat model downloaded and the app open, run in PowerShell:
```powershell
curl.exe http://127.0.0.1:7878/v1/chat/completions -H "Content-Type: application/json" -d '{"model":"local","messages":[{"role":"user","content":"Hello!"}]}'
```
+
- **Expected:** A JSON response containing the AI's reply text.
- **Watch for:** "connection refused" (server not listening) or an empty/error JSON.
**TC-GW-03 — Models endpoint**
+
1. Run: `curl.exe http://127.0.0.1:7878/v1/models`
+
- **Expected:** JSON listing your installed model(s).
---
@@ -241,21 +276,27 @@ the single most important suite.
Windows risk area** — test carefully and capture the console.
**TC-IMG-01 — Download an image model**
+
1. Sidebar → **Models**. Find an **image generation** model (e.g. an SDXL/Z-Image entry)
and download it.
+
- **Expected:** Downloads and shows as installed.
**TC-IMG-02 — Generate an image**
+
1. Open the image-generation UI, enter a prompt like `a red bicycle on a beach, sunset`.
2. Start generation.
+
- **Expected:** You see a **live step-by-step preview** as the image forms, a progress/ETA,
and a final PNG. The image roughly matches the prompt.
- **Watch for (Windows red flag):** generation fails immediately with a **`.dll` error** in
the console (the SD engine couldn't load its libraries). **Report with the exact console
text — this is a specific thing we're checking.**
-**TC-IMG-03 — Image-to-image** *(if the UI offers it)*
+**TC-IMG-03 — Image-to-image** _(if the UI offers it)_
+
1. Provide a starting image + a prompt and generate.
+
- **Expected:** Output is a variation based on the input image.
---
@@ -266,19 +307,25 @@ Windows risk area** — test carefully and capture the console.
(Kokoro) work on Windows.
**TC-VOICE-01 — Text-to-speech (speak)**
+
1. Find the **speak / play audio** control on an AI message (or the voice settings), and
trigger it. Download the voice model if prompted.
+
- **Expected:** You **hear** the text spoken aloud through your speakers.
- **Watch for:** no audio, or a console error about the TTS worker.
**TC-VOICE-02 — Speech-to-text (transcribe)**
+
1. Use the **microphone / voice input** to dictate a message (say a sentence).
+
- **Expected:** Your speech is transcribed into text in the message box.
- **Watch for:** Windows may ask for **microphone permission** — allow it. No transcript, or
an `ffmpeg`/`whisper` error in console = fail.
-**TC-VOICE-03 — Hands-free voice mode** *(if present)*
+**TC-VOICE-03 — Hands-free voice mode** _(if present)_
+
1. Enter the hands-free voice mode and have a short spoken back-and-forth.
+
- **Expected:** You speak → it transcribes → AI replies → reply is spoken aloud.
---
@@ -288,26 +335,34 @@ Windows risk area** — test carefully and capture the console.
**What it proves:** document upload + "answer using my files" works.
**TC-PROJ-01 — Create a project**
+
1. Sidebar → **Projects** → create one (name it, e.g. "Test Project").
+
- **Expected:** Project is created and opens.
**TC-PROJ-02 — Upload a document & ask about it**
+
1. In the project's **Knowledge base**, upload a **PDF or .txt/.docx** file that contains
some specific fact (e.g. a document that says "The budget is $5,000").
2. Wait for it to finish processing.
3. Start a chat in that project and ask a question only answerable from the doc
(e.g. `What is the budget?`).
+
- **Expected:** The AI answers using the document ($5,000) and shows a **cited source**.
- **Watch for:** upload fails, processing hangs, or the AI ignores the document.
**TC-PROJ-03 — Per-project instructions**
+
1. Set a project instruction (e.g. "Always answer in French").
2. Ask a question in that project.
+
- **Expected:** The AI follows the instruction.
-**TC-PROJ-04 — Audio/image document** *(depends on voice/vision models)*
+**TC-PROJ-04 — Audio/image document** _(depends on voice/vision models)_
+
1. Upload an **image** (with visible text or clear objects) and/or a short **audio** file.
2. Ask about its contents.
+
- **Expected:** Image is described / audio is transcribed and usable in answers.
---
@@ -318,12 +373,16 @@ Windows risk area** — test carefully and capture the console.
reliable on Windows).
**TC-ART-01 — Render an artifact**
+
1. In Chat, ask: `Make a simple HTML page with a button that shows an alert when clicked.`
+
- **Expected:** A rendered **Preview** appears in a canvas, with a **Code / Preview** toggle
and a **Download** option. Clicking the button in the preview shows the alert.
**TC-ART-02 — Mermaid diagram**
+
1. Ask: `Draw a flowchart of making tea, as a mermaid diagram.`
+
- **Expected:** A diagram renders in the canvas.
---
@@ -334,21 +393,27 @@ reliable on Windows).
launch `npx` are a Windows-sensitive area** — test TC-INT-02.
**TC-INT-01 — Add an HTTP connector**
+
1. Sidebar → **Integrations** → add a connector using the **URL** option
(`https://mcp.example.com/endpoint` field). Use any test MCP endpoint you were given, or
- just verify the *form* opens and accepts input if you have no endpoint.
+ just verify the _form_ opens and accepts input if you have no endpoint.
+
- **Expected:** Connector saves; **Connect** attempts a connection.
-**TC-INT-02 — Add a stdio connector (`npx`)** — *Windows red flag area*
+**TC-INT-02 — Add a stdio connector (`npx`)** — _Windows red flag area_
+
1. Add a connector using the **command** option: command = `npx`, args = an MCP server
package you were given (or a known one).
2. Enable / connect it.
+
- **Expected:** The connector shows as **Connected** (the app launched `npx` under the
- hood).
+ hood).
- **Watch for:** "command not found" / spawn errors in the console — capture them exactly.
**TC-INT-03 — Use a connector in chat**
+
1. With a connector connected, ask the AI something that would use its tool.
+
- **Expected:** The AI calls the tool and uses the result in its answer.
---
@@ -356,11 +421,15 @@ launch `npx` are a Windows-sensitive area** — test TC-INT-02.
### Suite J — Tools in chat
**TC-TOOL-01 — Calculator / datetime**
+
1. Ask: `What is 1234 * 5678?` and `What is today's date and time?`
+
- **Expected:** Correct math (7,006,652) and a correct current date/time.
-**TC-TOOL-02 — Web search** *(uses internet)*
+**TC-TOOL-02 — Web search** _(uses internet)_
+
1. Ask something requiring fresh info, e.g. `Search the web for the latest news about NASA.`
+
- **Expected:** The AI performs a search and summarizes results.
---
@@ -368,13 +437,17 @@ launch `npx` are a Windows-sensitive area** — test TC-INT-02.
### Suite K — Settings, persistence & theming
**TC-SET-01 — Theme toggle**
+
1. Sidebar → **Settings**. Toggle between light and dark theme.
+
- **Expected:** The whole app switches theme cleanly (no unreadable text, no broken layout —
check the starry background is visible in both).
-**TC-SET-02 — Data persists across restart** *(also tests encryption-at-rest)*
+**TC-SET-02 — Data persists across restart** _(also tests encryption-at-rest)_
+
1. Have at least one chat with history.
2. **Fully quit** the app, then reopen it.
+
- **Expected:** Your previous chats/projects are **still there**. Downloaded models are still
installed.
- **Watch for:** database errors on startup in the console, or everything wiped.
@@ -384,50 +457,58 @@ launch `npx` are a Windows-sensitive area** — test TC-INT-02.
### Suite L — Stability & cleanup
**TC-STAB-01 — Long session**
+
1. Use chat + image gen + voice over ~15–20 minutes.
+
- **Expected:** No crash, no runaway memory. Note if the app becomes sluggish.
-**TC-STAB-02 — No orphaned processes after quit** — *Windows-specific check*
+**TC-STAB-02 — No orphaned processes after quit** — _Windows-specific check_
+
1. Quit the app fully.
2. Open **Task Manager → Details** and search for **`llama-server.exe`** (and
`sd-cli.exe`, `whisper-cli.exe`).
+
- **Expected:** **None** of these are still running after the app is closed.
- **Watch for:** a leftover `llama-server.exe` — report it (it would block the next launch).
**TC-STAB-03 — Relaunch after quit**
+
1. Reopen the app and send a chat message.
+
- **Expected:** Works first try (no "port in use" / model won't load error from a leftover
process).
---
-### Suite M — Auto-update *(likely N/A right now — confirm and note)*
+### Suite M — Auto-update _(likely N/A right now — confirm and note)_
**TC-UPD-01 — Update check**
+
1. Watch the console at startup for `[update]` lines.
+
- **Expected for this branch:** it's fine if updates **don't** work yet — the Windows update
feed isn't published. **Just record what you see** (e.g. `[update] check failed` or
- nothing). Don't file this as a bug unless the app *crashes* over it.
+ nothing). Don't file this as a bug unless the app _crashes_ over it.
---
## 5. Quick summary sheet (fill and return)
-| Suite | Result (✅/❌/⛔/⏭️) | Bug IDs filed |
-|---|---|---|
-| A — Install & launch | | |
-| B — Models | | |
-| C — Chat | | |
-| D — Gateway | | |
-| E — Image generation | | |
-| F — Voice | | |
-| G — Projects / RAG | | |
-| H — Artifacts | | |
-| I — Integrations (MCP) | | |
-| J — Tools in chat | | |
-| K — Settings & persistence | | |
-| L — Stability & cleanup | | |
-| M — Auto-update | | |
+| Suite | Result (✅/❌/⛔/⏭️) | Bug IDs filed |
+| -------------------------- | -------------------- | ------------- |
+| A — Install & launch | | |
+| B — Models | | |
+| C — Chat | | |
+| D — Gateway | | |
+| E — Image generation | | |
+| F — Voice | | |
+| G — Projects / RAG | | |
+| H — Artifacts | | |
+| I — Integrations (MCP) | | |
+| J — Tools in chat | | |
+| K — Settings & persistence | | |
+| L — Stability & cleanup | | |
+| M — Auto-update | | |
**Overall verdict:** Core app is ☐ usable / ☐ usable with issues / ☐ blocked on Windows.
diff --git a/docs/features/gateway.md b/docs/features/gateway.md
index d04a421e..7973907e 100644
--- a/docs/features/gateway.md
+++ b/docs/features/gateway.md
@@ -7,16 +7,16 @@ you've downloaded. Point any OpenAI SDK at `…/v1` with any (ignored) key.

-| Capability | Method · Endpoint | Notes |
-|---|---|---|
-| Chat (text) | `POST /v1/chat/completions` | streaming via `stream:true` |
-| Vision | `POST /v1/chat/completions` | `image_url` content parts (data URL or http) |
-| Text → Image | `POST /v1/images` · `/v1/images/generations` | `{prompt, aspect_ratio?, resolution?, seed?}` |
-| Image → Image | `POST /v1/images` · `/v1/images/edits` | `input_references:[{image_url:{url}}]` |
-| Speech → Text | `POST /v1/audio/transcriptions` | multipart `file` (whisper) |
-| Text → Speech | `POST /v1/audio/speech` | `{input, voice?}` → `audio/wav` (Kokoro) |
-| Embeddings | `POST /v1/embeddings` | local `all-MiniLM-L6-v2`, 384-dim |
-| Models | `GET /v1/models` | the active model per modality |
+| Capability | Method · Endpoint | Notes |
+| ------------- | -------------------------------------------- | --------------------------------------------- |
+| Chat (text) | `POST /v1/chat/completions` | streaming via `stream:true` |
+| Vision | `POST /v1/chat/completions` | `image_url` content parts (data URL or http) |
+| Text → Image | `POST /v1/images` · `/v1/images/generations` | `{prompt, aspect_ratio?, resolution?, seed?}` |
+| Image → Image | `POST /v1/images` · `/v1/images/edits` | `input_references:[{image_url:{url}}]` |
+| Speech → Text | `POST /v1/audio/transcriptions` | multipart `file` (whisper) |
+| Text → Speech | `POST /v1/audio/speech` | `{input, voice?}` → `audio/wav` (Kokoro) |
+| Embeddings | `POST /v1/embeddings` | local `all-MiniLM-L6-v2`, 384-dim |
+| Models | `GET /v1/models` | the active model per modality |
- **Interactive docs**: `GET /docs` (Scalar) · **spec**: `GET /openapi.json`.
- **Load-on-demand**: models load when a request needs them and offload after — long
diff --git a/e2e/chat-large-collection.spec.ts b/e2e/chat-large-collection.spec.ts
new file mode 100644
index 00000000..8b28ffb3
--- /dev/null
+++ b/e2e/chat-large-collection.spec.ts
@@ -0,0 +1,120 @@
+/**
+ * RELEASE_TEST_CHECKLIST #149 - a large persisted chat collection remains
+ * searchable, scrollable, and usable in the real desktop master-detail layout.
+ * Synthetic records enter through production preload and main-process IPC.
+ */
+import {
+ test,
+ expect,
+ _electron as electron,
+ type ElectronApplication,
+ type Page
+} from '@playwright/test'
+import fs from 'fs'
+import os from 'os'
+import path from 'path'
+
+let app: ElectronApplication
+let page: Page
+let userDataDir: string
+
+async function finishOnboarding(): Promise {
+ for (let step = 0; step < 6; step += 1) {
+ const button = page.getByRole('button', { name: /Continue|Start using Off Grid/i })
+ if (!(await button.isVisible().catch(() => false))) return
+ await button.click()
+ }
+}
+
+test.beforeAll(async () => {
+ userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'offgrid-chat-collection-'))
+ app = await electron.launch({
+ args: ['.'],
+ env: {
+ ...process.env,
+ OFFGRID_USER_DATA: userDataDir,
+ OFFGRID_PRO: '0',
+ NODE_ENV: 'production'
+ }
+ })
+ page = await app.firstWindow()
+ await page.setViewportSize({ width: 1600, height: 900 })
+ await page.waitForLoadState('domcontentloaded')
+ await finishOnboarding()
+ await expect(page.getByRole('heading', { name: 'Models' })).toBeVisible()
+
+ await page.evaluate(async () => {
+ for (let index = 1; index <= 120; index += 1) {
+ const ordinal = String(index).padStart(3, '0')
+ const conversationId = `synthetic-large-chat-${ordinal}`
+ await window.api.createRagConversation(conversationId, `Synthetic chat ${ordinal}`, null)
+ if (index === 120) {
+ await window.api.addRagMessage(
+ conversationId,
+ 'user',
+ 'Only this conversation contains body-search-needle-120.'
+ )
+ await window.api.addRagMessage(
+ conversationId,
+ 'assistant',
+ 'The selected result loaded its persisted message detail.'
+ )
+ }
+ }
+ })
+
+ await page.getByTitle('Chat').click()
+ await expect(page.getByPlaceholder('Search conversations…')).toBeVisible()
+})
+
+test.afterAll(async () => {
+ await app?.close()
+ fs.rmSync(userDataDir, { recursive: true, force: true })
+})
+
+test('120 persisted chats keep search, scroll, and detail usable on desktop (#149)', async () => {
+ const rail = page.locator('aside')
+ const list = rail.locator('.overflow-y-auto')
+ const detail = rail.locator('xpath=following-sibling::div[1]')
+ const titles = rail.getByText(/^Synthetic chat \d{3}$/)
+
+ await expect(titles).toHaveCount(120)
+ const railBox = await rail.boundingBox()
+ const detailBox = await detail.boundingBox()
+ expect(railBox).not.toBeNull()
+ expect(detailBox).not.toBeNull()
+ if (!railBox || !detailBox) return
+
+ const railShare = railBox.width / (railBox.width + detailBox.width)
+ expect(railShare).toBeGreaterThan(0.1)
+ expect(railShare).toBeLessThan(0.25)
+ expect(Math.abs(detailBox.x - (railBox.x + railBox.width))).toBeLessThanOrEqual(2)
+ expect(detailBox.width).toBeGreaterThan(railBox.width * 3)
+
+ const search = page.getByPlaceholder('Search conversations…')
+ await search.fill('body-search-needle-120')
+ await expect(rail.getByText('Synthetic chat 120', { exact: true })).toBeVisible()
+ await expect(rail.getByText(/^Synthetic chat \d{3}$/)).toHaveCount(1)
+
+ await rail.getByText('Synthetic chat 120', { exact: true }).click()
+ await expect(
+ detail.getByText('Only this conversation contains body-search-needle-120.', { exact: true })
+ ).toBeVisible()
+ await expect(
+ detail.getByText('The selected result loaded its persisted message detail.', { exact: true })
+ ).toBeVisible()
+
+ await search.fill('')
+ await expect(titles).toHaveCount(120)
+ const oldest = rail.getByText('Synthetic chat 001', { exact: true })
+ await oldest.click()
+
+ const listBox = await list.boundingBox()
+ const oldestBox = await oldest.boundingBox()
+ expect(listBox).not.toBeNull()
+ expect(oldestBox).not.toBeNull()
+ if (!listBox || !oldestBox) return
+ expect(oldestBox.y).toBeGreaterThanOrEqual(listBox.y)
+ expect(oldestBox.y + oldestBox.height).toBeLessThanOrEqual(listBox.y + listBox.height)
+ await expect(detail.getByText('Start a conversation', { exact: true })).toBeVisible()
+})
diff --git a/e2e/chat-memory.spec.ts b/e2e/chat-memory.spec.ts
index e91afd45..f7f6d4b8 100644
--- a/e2e/chat-memory.spec.ts
+++ b/e2e/chat-memory.spec.ts
@@ -2,21 +2,30 @@
* Chat + memory regression tests for:
* - No-memory toggle actually sticking (fix: assignProject no longer overrides noMemory)
* - Streaming placeholder appearing immediately (fix: streamConvRef routes tokens to correct conv)
+ * - Conversation, message, scope, and project persistence across a full process relaunch
+ * - Cold recovery and committed-data durability after a forced main-process kill
*
* Runs against the built app with OFFGRID_PRO=1 so the memory dropdown is visible.
* No LLM model is expected — we only assert UI state and IPC plumbing, not model output.
*/
-import { test, expect, _electron as electron, type ElectronApplication, type Page } from '@playwright/test';
-import os from 'os';
-import path from 'path';
-import fs from 'fs';
+import {
+ test,
+ expect,
+ _electron as electron,
+ type ElectronApplication,
+ type Page
+} from '@playwright/test'
+import os from 'os'
+import path from 'path'
+import fs from 'fs'
+import type { ChildProcess } from 'child_process'
-let app: ElectronApplication;
-let page: Page;
-let userDataDir: string;
+let app: ElectronApplication
+let page: Page
+let userDataDir: string
+let modelBoundaryBinDir: string | undefined
-test.beforeAll(async () => {
- userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'offgrid-chat-e2e-'));
+const launchApp = async (): Promise => {
app = await electron.launch({
args: ['.'],
env: {
@@ -24,155 +33,398 @@ test.beforeAll(async () => {
OFFGRID_USER_DATA: userDataDir,
OFFGRID_PRO: '1',
NODE_ENV: 'production',
- },
- });
- page = await app.firstWindow();
- await page.waitForLoadState('domcontentloaded');
+ ...(modelBoundaryBinDir ? { OFFGRID_BIN_DIR: modelBoundaryBinDir } : {})
+ }
+ })
+ page = await app.firstWindow()
+ await page.waitForLoadState('domcontentloaded')
+}
- // Skip onboarding so we land in the app shell.
- for (let i = 0; i < 8; i++) {
- const btn = page.getByRole('button', { name: /Continue|Start using Off Grid/i });
- if (!(await btn.isVisible().catch(() => false))) break;
- await btn.click();
- await page.waitForTimeout(300);
- }
-});
+const waitForExit = async (child: ChildProcess): Promise => {
+ if (child.exitCode !== null) return
+ await new Promise((resolve) => {
+ child.once('exit', () => resolve())
+ })
+}
-test.afterAll(async () => {
- await app?.close();
- try { fs.rmSync(userDataDir, { recursive: true, force: true }); } catch { /* ignore */ }
-});
+const closeApp = async (): Promise => {
+ const child = app.process()
+ await app.close()
+ await waitForExit(child)
+}
-test('navigates to the chat screen', async () => {
- // Find the chat/mind-share nav item and click it.
- const chatNav = page.getByRole('button', { name: /chat|mind|ask/i }).first();
- if (await chatNav.isVisible().catch(() => false)) {
- await chatNav.click();
- await page.waitForTimeout(500);
+const forceCloseApp = async (): Promise => {
+ const child = app.process()
+ child.kill('SIGKILL')
+ await waitForExit(child)
+}
+
+const enterChat = async (): Promise => {
+ for (let i = 0; i < 8; i++) {
+ const btn = page.getByRole('button', { name: /Continue|Start using Off Grid/i })
+ if (!(await btn.isVisible().catch(() => false))) break
+ await btn.click()
+ await page.waitForTimeout(300)
}
- await page.screenshot({ path: 'e2e/screenshots/chat-screen.png', fullPage: false });
-});
-test('memory toggle: No memory sticks after selection', async () => {
- // Dismiss the "Set up your local AI" banner (and any other overlays) that block clicks.
- const dismissBtns = page.locator('button').filter({ hasText: '' }).filter({ has: page.locator('svg') });
- const banner = page.locator('div').filter({ hasText: /set up your local ai/i }).first();
+ const chatNav = page.getByRole('button', { name: /chat|mind|ask/i }).first()
+ await expect(chatNav).toBeVisible()
+ await chatNav.click()
+ await page.waitForTimeout(500)
+
+ const banner = page
+ .locator('div')
+ .filter({ hasText: /set up your local ai/i })
+ .first()
if (await banner.isVisible().catch(() => false)) {
- const closeBtn = banner.locator('button').last();
- await closeBtn.click().catch(() => {});
- await page.waitForTimeout(300);
+ await banner.locator('button').last().click()
+ await page.waitForTimeout(300)
}
- void dismissBtns; // suppress unused warning
- await page.keyboard.press('Escape');
- await page.waitForTimeout(200);
+ await page.keyboard.press('Escape')
+}
- // Open the memory/scope dropdown.
- const memoryBtn = page.locator('button').filter({ hasText: /memory|no memory/i }).first();
- if (!(await memoryBtn.isVisible().catch(() => false))) {
- test.skip(true, 'Memory toggle not visible — may be on a different screen');
- return;
+const dismissCapturePrompt = async (): Promise => {
+ const dismiss = page.getByRole('button', { name: 'Dismiss', exact: true })
+ await expect(dismiss).toBeVisible()
+ await dismiss.click()
+ await expect(dismiss).toBeHidden()
+}
+
+test.beforeEach(async () => {
+ userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'offgrid-chat-e2e-'))
+ modelBoundaryBinDir = undefined
+ await launchApp()
+ await enterChat()
+})
+
+test.afterEach(async () => {
+ if (app) await closeApp()
+ try {
+ fs.rmSync(userDataDir, { recursive: true, force: true })
+ } catch {
+ /* ignore */
}
+})
- await memoryBtn.click({ force: true });
- await page.waitForTimeout(500);
- await page.screenshot({ path: 'e2e/screenshots/memory-dropdown-open.png' });
+test('navigates to the chat screen', async () => {
+ await expect(page.getByPlaceholder(/ask anything/i)).toBeVisible()
+ await page.screenshot({ path: 'e2e/screenshots/chat-screen.png', fullPage: false })
+})
+
+test('memory toggle: No memory sticks after selection', async () => {
+ // Open the memory/scope dropdown.
+ const memoryBtn = page
+ .locator('button')
+ .filter({ hasText: /memory|no memory/i })
+ .first()
+ await expect(memoryBtn).toBeVisible()
+ await memoryBtn.click({ force: true })
+ await page.waitForTimeout(500)
+ await page.screenshot({ path: 'e2e/screenshots/memory-dropdown-open.png' })
// Click "No memory" — Radix DropdownMenuItem renders as role="menuitem".
// Fall back to text match if the role selector doesn't resolve (portal timing).
- const noMemoryItem = page.getByRole('menuitem', { name: /no memory/i })
- .or(page.locator('[data-radix-dropdown-menu-content] *').filter({ hasText: /^No memory/i }));
- await expect(noMemoryItem.first()).toBeVisible({ timeout: 5000 });
- await noMemoryItem.first().click();
- await page.waitForTimeout(300);
+ const noMemoryItem = page
+ .getByRole('menuitem', { name: /no memory/i })
+ .or(page.locator('[data-radix-dropdown-menu-content] *').filter({ hasText: /^No memory/i }))
+ await expect(noMemoryItem.first()).toBeVisible({ timeout: 5000 })
+ await noMemoryItem.first().click()
+ await page.waitForTimeout(300)
- await page.screenshot({ path: 'e2e/screenshots/memory-no-memory-selected.png' });
+ await page.screenshot({ path: 'e2e/screenshots/memory-no-memory-selected.png' })
// The trigger button should now say "No memory", confirming the state stuck.
- const triggerAfter = page.locator('button').filter({ hasText: /no memory/i }).first();
- await expect(triggerAfter).toBeVisible();
-});
+ const triggerAfter = page
+ .locator('button')
+ .filter({ hasText: /no memory/i })
+ .first()
+ await expect(triggerAfter).toBeVisible()
+})
test('memory toggle: All memory sticks after selection', async () => {
// Open dropdown and switch to All memory to verify the round-trip.
- const memoryBtn = page.locator('button').filter({ hasText: /no memory|memory/i }).first();
- if (!(await memoryBtn.isVisible().catch(() => false))) {
- test.skip(true, 'Memory toggle not visible');
- return;
- }
-
- await memoryBtn.click({ force: true });
- await page.waitForTimeout(200);
+ const memoryBtn = page
+ .locator('button')
+ .filter({ hasText: /no memory|memory/i })
+ .first()
+ await expect(memoryBtn).toBeVisible()
+ await memoryBtn.click({ force: true })
+ await page.waitForTimeout(200)
- const allMemoryItem = page.getByRole('menuitem', { name: /all memory/i });
- if (!(await allMemoryItem.isVisible().catch(() => false))) {
- test.skip(true, 'All memory item not in dropdown (non-pro build?)');
- return;
- }
- await allMemoryItem.click();
- await page.waitForTimeout(300);
+ const allMemoryItem = page.getByRole('menuitem', { name: /all memory/i })
+ await expect(allMemoryItem).toBeVisible()
+ await allMemoryItem.click()
+ await page.waitForTimeout(300)
- await page.screenshot({ path: 'e2e/screenshots/memory-all-memory-selected.png' });
+ await page.screenshot({ path: 'e2e/screenshots/memory-all-memory-selected.png' })
// Button should now reflect "All memory".
- const triggerAfter = page.locator('button').filter({ hasText: /all memory/i }).first();
- await expect(triggerAfter).toBeVisible();
-});
+ const triggerAfter = page
+ .locator('button')
+ .filter({ hasText: /all memory/i })
+ .first()
+ await expect(triggerAfter).toBeVisible()
+})
test('chat composer renders and accepts input', async () => {
- const composer = page.getByPlaceholder(/ask anything/i);
- if (!(await composer.isVisible().catch(() => false))) {
- test.skip(true, 'Chat composer not visible');
- return;
- }
-
- await composer.fill('Hello, test message');
- await page.screenshot({ path: 'e2e/screenshots/chat-composer-filled.png' });
+ const composer = page.getByPlaceholder(/ask anything/i)
+ await expect(composer).toBeVisible()
+ await composer.fill('Hello, test message')
+ await page.screenshot({ path: 'e2e/screenshots/chat-composer-filled.png' })
// Verify the send button is present.
- const sendBtn = page.locator('button[type="submit"], button').filter({ has: page.locator('svg') }).last();
- await expect(sendBtn).toBeVisible();
+ const sendBtn = page
+ .locator('button[type="submit"], button')
+ .filter({ has: page.locator('svg') })
+ .last()
+ await expect(sendBtn).toBeVisible()
// Clear without sending — we don't have a model running.
- await composer.fill('');
-});
+ await composer.fill('')
+})
test('streaming placeholder appears immediately after send', async () => {
// This test captures the streaming UI state: the user bubble + the assistant
// placeholder bubble that appears instantly (before any model response).
// It validates the fix — previously nothing appeared until the full response
// resolved because stream tokens routed to the wrong conversation bucket.
- const composer = page.getByPlaceholder(/ask anything/i);
- if (!(await composer.isVisible().catch(() => false))) {
- test.skip(true, 'Chat composer not visible');
- return;
- }
-
- await composer.fill('What did I work on today?');
- await page.keyboard.press('Enter');
+ const composer = page.getByPlaceholder(/ask anything/i)
+ await expect(composer).toBeVisible()
+ await composer.fill('What did I work on today?')
+ await page.keyboard.press('Enter')
// The user bubble + assistant streaming placeholder should appear within ~500 ms
// regardless of whether a model is running — the placeholder is added synchronously
// before ragChat is even called. Screenshot right after send to capture it.
- await page.waitForTimeout(600);
- await page.screenshot({ path: 'e2e/screenshots/streaming-placeholder.png' });
+ await page.waitForTimeout(600)
+ await page.screenshot({ path: 'e2e/screenshots/streaming-placeholder.png' })
// Assert the user message rendered immediately.
- const userBubble = page.locator('text=What did I work on today?').first();
- await expect(userBubble).toBeVisible({ timeout: 3000 });
+ const userBubble = page.locator('text=What did I work on today?').first()
+ await expect(userBubble).toBeVisible({ timeout: 3000 })
// Assert exactly one assistant bubble appeared — the streaming placeholder
// must transition to the final state (error when no model), never duplicate into two.
- const assistantBubble = page.locator('div').filter({
- hasText: /searching|working|sorry|error|off grid/i,
- }).first();
- await expect(assistantBubble).toBeVisible({ timeout: 5000 }).catch(() => {
- // No model running is acceptable — the user bubble alone proves routing.
- });
+ const assistantBubble = page
+ .locator('div')
+ .filter({
+ hasText: /searching|working|sorry|error|off grid/i
+ })
+ .first()
+ await expect(assistantBubble)
+ .toBeVisible({ timeout: 5000 })
+ .catch(() => {
+ // No model running is acceptable — the user bubble alone proves routing.
+ })
// Confirm there is NOT a second stale streaming bubble (the animated dots)
// alongside the error — before the fix there would be two assistant bubbles.
// Target the innermost text nodes to avoid counting wrapper divs.
- const errorBubbleCount = await page.locator('p, span').filter({ hasText: /^Sorry, something went wrong/i }).count();
- expect(errorBubbleCount).toBeLessThanOrEqual(1);
+ const errorBubbleCount = await page
+ .locator('p, span')
+ .filter({ hasText: /^Sorry, something went wrong/i })
+ .count()
+ expect(errorBubbleCount).toBeLessThanOrEqual(1)
+
+ await page.screenshot({ path: 'e2e/screenshots/streaming-after-send.png' })
+})
+
+test('conversations, messages, scopes, and project associations survive relaunch', async () => {
+ const seeded = await page.evaluate(async () => {
+ const projectId = await window.api.createProject({ name: 'Relaunch Project' })
+ await window.api.createRagConversation('persist-general', 'Persistent General', null)
+ await window.api.addRagMessage('persist-general', 'user', 'general question')
+ await window.api.addRagMessage('persist-general', 'assistant', 'general answer', {
+ sources: ['local-memory']
+ })
+ await window.api.createRagConversation('persist-project', 'Persistent Project', projectId)
+ await window.api.addRagMessage('persist-project', 'user', 'project question')
+ await window.api.addRagMessage('persist-project', 'assistant', 'project answer', {
+ projectId
+ })
+ return { projectId }
+ })
+
+ await closeApp()
+ await launchApp()
+
+ const persisted = await page.evaluate(async ({ projectId }) => {
+ const conversations = await window.api.getRagConversations()
+ const projects = await window.api.listProjects()
+ const generalMessages = await window.api.getRagMessages('persist-general')
+ const projectMessages = await window.api.getRagMessages('persist-project')
+ const selectConversation = (
+ id: string
+ ): {
+ id: string
+ title: string | null
+ projectId: string | null | undefined
+ messageCount: number | undefined
+ } | null => {
+ const conversation = conversations.find((item) => item.id === id)
+ return conversation
+ ? {
+ id: conversation.id,
+ title: conversation.title,
+ projectId: conversation.project_id,
+ messageCount: conversation.message_count
+ }
+ : null
+ }
+ const selectMessages = (
+ messages: typeof generalMessages
+ ): Array<{ role: string; content: string; context: string | null }> =>
+ messages.map((message) => ({
+ role: message.role,
+ content: message.content,
+ context: message.context
+ }))
+ return {
+ projectExists: projects.some((project) => project.id === projectId),
+ general: selectConversation('persist-general'),
+ project: selectConversation('persist-project'),
+ generalMessages: selectMessages(generalMessages),
+ projectMessages: selectMessages(projectMessages)
+ }
+ }, seeded)
+
+ expect(persisted.projectExists).toBe(true)
+ expect(persisted.general).toEqual({
+ id: 'persist-general',
+ title: 'Persistent General',
+ projectId: null,
+ messageCount: 2
+ })
+ expect(persisted.project).toEqual({
+ id: 'persist-project',
+ title: 'Persistent Project',
+ projectId: seeded.projectId,
+ messageCount: 2
+ })
+ expect(persisted.generalMessages).toEqual([
+ { role: 'user', content: 'general question', context: null },
+ {
+ role: 'assistant',
+ content: 'general answer',
+ context: JSON.stringify({ sources: ['local-memory'] })
+ }
+ ])
+ expect(persisted.projectMessages).toEqual([
+ { role: 'user', content: 'project question', context: null },
+ {
+ role: 'assistant',
+ content: 'project answer',
+ context: JSON.stringify({ projectId: seeded.projectId })
+ }
+ ])
+})
+
+test('cancelling a tool-owned image keeps its text answer after a full relaunch', async () => {
+ // Re-launch against faithful native-process boundaries. The production LLMService
+ // spawns the fake llama executable and speaks real HTTP/SSE; imagegen spawns the
+ // fake sd-cli and must kill it through the rendered Stop control. SQLite, IPC,
+ // toolChat, MemoryChat, and the process relaunch are all real Off Grid code.
+ await closeApp()
+
+ const modelsDir = path.join(userDataDir, 'models')
+ modelBoundaryBinDir = path.join(userDataDir, 'e2e-model-bin')
+ const llamaDir = path.join(modelBoundaryBinDir, 'llama')
+ const sdDir = path.join(modelBoundaryBinDir, 'sd')
+ fs.mkdirSync(modelsDir, { recursive: true })
+ fs.mkdirSync(llamaDir, { recursive: true })
+ fs.mkdirSync(sdDir, { recursive: true })
+
+ const writeGguf = (filename: string, marker = ''): void => {
+ const bytes = Buffer.alloc(2048)
+ bytes.write('GGUF')
+ bytes.write(marker, 16)
+ fs.writeFileSync(path.join(modelsDir, filename), bytes)
+ }
+ writeGguf('fake-chat.gguf')
+ writeGguf('sdxl-lightning-e2e.gguf', 'first_stage_model text_encoder')
+ fs.writeFileSync(
+ path.join(modelsDir, 'active-model.json'),
+ JSON.stringify({ id: 'e2e-chat', primary: 'fake-chat.gguf' })
+ )
+
+ const installExecutable = (fixture: string, destination: string): void => {
+ fs.copyFileSync(path.join(process.cwd(), 'e2e', 'fixtures', fixture), destination)
+ fs.chmodSync(destination, 0o755)
+ }
+ installExecutable('fake-llama-server.mjs', path.join(llamaDir, 'llama-server'))
+ installExecutable('fake-sd-cli.mjs', path.join(sdDir, 'sd-cli'))
+
+ await launchApp()
+ await page.evaluate(async () => {
+ await window.api.saveSetting('composerToolsOn', true)
+ await window.api.setActiveModalModel('image', 'sdxl-lightning-e2e.gguf')
+ })
+ await enterChat()
+ await dismissCapturePrompt()
+
+ const composer = page.getByPlaceholder(/ask anything/i)
+ await composer.fill('Summarize my week and draw a chart')
+ await page.keyboard.press('Enter')
+
+ const answer = page.getByText('Here is your weekly summary.', { exact: true })
+ await expect(answer).toBeVisible()
+ const stopImage = page.getByRole('button', { name: 'Stop', exact: true })
+ await expect(stopImage).toBeVisible()
+ await stopImage.click()
+ await expect(stopImage).toBeHidden()
+
+ const readPersistedTurn = async (): Promise =>
+ page.evaluate(async () => {
+ const conversations = await window.api.getRagConversations()
+ for (const conversation of conversations) {
+ const messages = await window.api.getRagMessages(conversation.id)
+ if (messages.some((message) => message.content === 'Summarize my week and draw a chart')) {
+ return messages.map((message) => [message.role, message.content])
+ }
+ }
+ return []
+ })
+ await expect.poll(readPersistedTurn).toEqual([
+ ['user', 'Summarize my week and draw a chart'],
+ ['assistant', 'Here is your weekly summary.']
+ ])
+
+ await closeApp()
+ await launchApp()
+ await enterChat()
+
+ // Terminal artifact: a newly created renderer, backed by the re-opened SQLite
+ // database in a new Electron main process, paints the exact completed text turn.
+ await expect(page.getByText('Here is your weekly summary.', { exact: true })).toBeVisible()
+ await expect(
+ page
+ .locator('p')
+ .filter({ hasText: /^Summarize my week and draw a chart$/ })
+ .last()
+ ).toBeVisible()
+})
+
+test('cold relaunch after a forced quit boots cleanly and keeps committed chat data', async () => {
+ await page.evaluate(async () => {
+ await window.api.createRagConversation('forced-quit-chat', 'Forced Quit Chat', null)
+ await window.api.addRagMessage('forced-quit-chat', 'user', 'committed before forced quit')
+ })
+ await page.getByPlaceholder(/ask anything/i).fill('uncommitted draft during forced quit')
+
+ await forceCloseApp()
+ await launchApp()
+
+ await expect(page.locator('#root')).not.toBeEmpty()
+ expect(await page.evaluate(() => typeof window.api === 'object')).toBe(true)
+ const persisted = await page.evaluate(async () => ({
+ conversation: await window.api.getRagConversation('forced-quit-chat'),
+ messages: await window.api.getRagMessages('forced-quit-chat')
+ }))
+ expect(persisted.conversation?.title).toBe('Forced Quit Chat')
+ expect(persisted.messages.map((message) => message.content)).toEqual([
+ 'committed before forced quit'
+ ])
- await page.screenshot({ path: 'e2e/screenshots/streaming-after-send.png' });
-});
+ await enterChat()
+ await expect(page.getByPlaceholder(/ask anything/i)).toBeEnabled()
+ await expect(page.getByText('committed before forced quit', { exact: true })).toBeVisible()
+})
diff --git a/e2e/desktop-polish.spec.ts b/e2e/desktop-polish.spec.ts
new file mode 100644
index 00000000..ffac615a
--- /dev/null
+++ b/e2e/desktop-polish.spec.ts
@@ -0,0 +1,208 @@
+import {
+ test,
+ expect,
+ _electron as electron,
+ type ElectronApplication,
+ type Page,
+ type Locator
+} from '@playwright/test'
+import fs from 'fs'
+import os from 'os'
+import path from 'path'
+
+let app: ElectronApplication
+let page: Page
+let userDataDir: string
+
+async function finishOnboarding(): Promise {
+ for (let step = 0; step < 6; step += 1) {
+ const button = page.getByRole('button', { name: /Continue|Start using Off Grid/i })
+ if (!(await button.isVisible().catch(() => false))) return
+ await button.click()
+ }
+}
+
+async function expectVisibleKeyboardFocus(locator: Locator, label: string): Promise {
+ await expect(locator, `${label} receives focus in the expected order`).toBeFocused()
+ const primaryColor = await page.evaluate(() => {
+ const probe = document.createElement('span')
+ probe.style.color = 'var(--og-primary)'
+ document.body.append(probe)
+ const color = getComputedStyle(probe).color
+ probe.remove()
+ return color
+ })
+ await expect
+ .poll(
+ () =>
+ locator.evaluate((element) => {
+ const style = getComputedStyle(element)
+ return {
+ focusVisible: element.matches(':focus-visible'),
+ outlineStyle: style.outlineStyle,
+ outlineWidth: style.outlineWidth,
+ outlineColor: style.outlineColor,
+ outlineOffset: style.outlineOffset
+ }
+ }),
+ { message: `${label} settles to the exact token-based focus treatment` }
+ )
+ .toEqual({
+ focusVisible: true,
+ outlineStyle: 'solid',
+ outlineWidth: '2px',
+ outlineColor: primaryColor,
+ outlineOffset: '2px'
+ })
+}
+
+async function tabUntilFocused(locator: Locator, label: string, maxTabs: number): Promise {
+ for (let index = 0; index < maxTabs; index += 1) {
+ await page.keyboard.press('Tab')
+ if (await locator.evaluate((element) => element === document.activeElement)) {
+ await expectVisibleKeyboardFocus(locator, label)
+ return
+ }
+ }
+ throw new Error(`${label} was not reached within ${maxTabs} Tab presses`)
+}
+
+test.beforeAll(async () => {
+ userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'offgrid-desktop-polish-'))
+ app = await electron.launch({
+ args: ['.'],
+ env: {
+ ...process.env,
+ OFFGRID_USER_DATA: userDataDir,
+ OFFGRID_PRO: '0',
+ NODE_ENV: 'production'
+ }
+ })
+ page = await app.firstWindow()
+ await page.waitForLoadState('domcontentloaded')
+ await finishOnboarding()
+ await expect(page.getByRole('heading', { name: 'Models' })).toBeVisible()
+})
+
+test.afterAll(async () => {
+ await app?.close()
+ fs.rmSync(userDataDir, { recursive: true, force: true })
+})
+
+test('window resize adds collection columns while filter context remains reachable (#150)', async () => {
+ const collection = page.getByRole('list', { name: 'Models available to download' })
+ await expect(collection).toBeVisible()
+
+ await page.setViewportSize({ width: 1280, height: 760 })
+ await expect
+ .poll(() =>
+ collection.evaluate(
+ (element) => getComputedStyle(element).gridTemplateColumns.split(' ').length
+ )
+ )
+ .toBe(3)
+
+ await collection.evaluate((element) => {
+ const scroller = element.parentElement
+ if (scroller) scroller.scrollTop = scroller.scrollHeight
+ })
+ await expect(page.getByPlaceholder('Search HuggingFace…')).toBeVisible()
+
+ await page.setViewportSize({ width: 1800, height: 900 })
+ await expect
+ .poll(() =>
+ collection.evaluate(
+ (element) => getComputedStyle(element).gridTemplateColumns.split(' ').length
+ )
+ )
+ .toBe(4)
+})
+
+test('keyboard focus follows navigation, form, dialog, and primary-action order (#151)', async () => {
+ await page.locator('body').click({ position: { x: 2, y: 2 } })
+ await page.keyboard.press('Tab')
+
+ const expandSidebar = page.getByRole('button', { name: 'Expand sidebar' })
+ const searchNavigation = page.getByRole('button', { name: 'Search', exact: true })
+ const dayNavigation = page.getByRole('button', { name: 'Day', exact: true })
+ await expectVisibleKeyboardFocus(expandSidebar, 'sidebar toggle')
+ await page.keyboard.press('Tab')
+ await expectVisibleKeyboardFocus(searchNavigation, 'first navigation destination')
+ await page.keyboard.press('Tab')
+ await expectVisibleKeyboardFocus(dayNavigation, 'second navigation destination')
+
+ // Continue through the rest of the real navigation and Models header. This keeps
+ // Chromium in keyboard modality; a programmatic focus jump would not prove that
+ // :focus-visible survives the user's actual traversal.
+ await tabUntilFocused(page.getByRole('button', { name: /^Storage\b/ }), 'Storage tab', 24)
+ await page.keyboard.press('Tab')
+ const modelSearch = page.getByPlaceholder('Search HuggingFace…')
+ await expectVisibleKeyboardFocus(modelSearch, 'model search field')
+
+ for (const name of ['All sources', 'Any size', 'Sort: Recommended']) {
+ await page.keyboard.press('Tab')
+ await expectVisibleKeyboardFocus(
+ page.getByRole('button', { name, exact: true }),
+ `${name} filter`
+ )
+ }
+ for (const size of [2, 4, 6, 8, 16]) {
+ await page.keyboard.press('Tab')
+ await expectVisibleKeyboardFocus(
+ page.getByRole('button', { name: `≤${size}GB`, exact: true }),
+ `${size}GB quick filter`
+ )
+ }
+ for (const useCase of ['General', 'Coding', 'Writing', 'Legal', 'Vision', 'Lightweight']) {
+ await page.keyboard.press('Tab')
+ await expectVisibleKeyboardFocus(
+ page.getByRole('button', { name: useCase, exact: true }),
+ `${useCase} use-case filter`
+ )
+ }
+
+ const firstCard = page
+ .getByRole('list', { name: 'Models available to download' })
+ .getByRole('listitem')
+ .first()
+ const cardButtons = firstCard.getByRole('button')
+ await page.keyboard.press('Tab')
+ await expectVisibleKeyboardFocus(cardButtons.nth(0), 'model detail trigger')
+ await page.keyboard.press('Tab')
+ await expectVisibleKeyboardFocus(cardButtons.nth(1), 'model details action')
+ await page.keyboard.press('Tab')
+ await expectVisibleKeyboardFocus(
+ firstCard.getByRole('button', { name: 'Download', exact: true }),
+ 'primary download action'
+ )
+
+ // The production command palette is the app's real modal dialog. It must move
+ // focus into its form, keep keyboard focus inside, and retain the visible ring.
+ await page.keyboard.press('Meta+K')
+ const dialog = page.getByRole('dialog', { name: 'Search Off Grid' })
+ await expect(dialog).toBeVisible()
+ const dialogSearch = dialog.getByPlaceholder('Search everything…')
+ await expectVisibleKeyboardFocus(dialogSearch, 'dialog search field')
+ await page.keyboard.press('Tab')
+ await expectVisibleKeyboardFocus(dialogSearch, 'dialog focus trap')
+ await page.keyboard.press('Escape')
+ await expect(dialog).toBeHidden()
+})
+
+test('reduced motion keeps the detail layer reachable and Escape preserves the collection (#152, #153)', async () => {
+ await page.emulateMedia({ reducedMotion: 'reduce' })
+ const firstCard = page.getByRole('listitem').first()
+ await firstCard.getByRole('button').first().click()
+
+ const detail = page.locator('.fixed.inset-0.z-50.flex.justify-end > div.relative')
+ await expect(detail).toBeVisible()
+ const transitionDuration = await detail.evaluate(
+ (element) => getComputedStyle(element).transitionDuration
+ )
+ expect(Number.parseFloat(transitionDuration)).toBeLessThanOrEqual(0.001)
+
+ await page.keyboard.press('Escape')
+ await expect(detail).toBeHidden()
+ await expect(page.getByRole('heading', { name: 'Models' })).toBeVisible()
+ await expect(page.getByRole('list', { name: 'Models available to download' })).toBeVisible()
+})
diff --git a/e2e/fixtures/fake-llama-server.mjs b/e2e/fixtures/fake-llama-server.mjs
new file mode 100644
index 00000000..881843fb
--- /dev/null
+++ b/e2e/fixtures/fake-llama-server.mjs
@@ -0,0 +1,88 @@
+#!/usr/bin/env node
+
+// Behaviour-faithful native-model boundary for Electron E2E. The app launches
+// this executable through the real LLMService and talks to it over llama.cpp's
+// OpenAI-compatible HTTP/SSE contract. Everything above the native process stays
+// real: IPC, toolChat, tool dispatch, renderer orchestration, and persistence.
+
+import http from 'node:http'
+
+const args = process.argv.slice(2)
+const portFlag = Math.max(args.indexOf('--port'), args.indexOf('-p'))
+const port = portFlag >= 0 ? Number(args[portFlag + 1]) : 8439
+let completionCount = 0
+
+// Plain executable JavaScript cannot carry a TypeScript return annotation.
+// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
+const delta = (payload) => `data: ${JSON.stringify({ choices: [{ delta: payload }] })}\n\n`
+
+const server = http.createServer((request, response) => {
+ if (request.method === 'GET' && request.url === '/health') {
+ response.writeHead(200, { 'Content-Type': 'application/json' })
+ response.end(JSON.stringify({ status: 'ok' }))
+ return
+ }
+ if (request.method === 'GET' && request.url === '/v1/models') {
+ response.writeHead(200, { 'Content-Type': 'application/json' })
+ response.end(JSON.stringify({ data: [{ id: 'e2e-tool-model' }] }))
+ return
+ }
+ if (request.method !== 'POST' || request.url !== '/v1/chat/completions') {
+ response.writeHead(404)
+ response.end()
+ return
+ }
+
+ let body = ''
+ request.on('data', (chunk) => {
+ body += chunk
+ })
+ request.on('end', () => {
+ const payload = JSON.parse(body)
+ response.writeHead(200, {
+ 'Content-Type': payload.stream ? 'text/event-stream' : 'application/json'
+ })
+
+ const turn = completionCount++
+ if (!payload.stream) {
+ response.end(
+ JSON.stringify({
+ choices: [{ message: { content: 'Here is your weekly summary.' } }],
+ usage: { total_tokens: 0 }
+ })
+ )
+ return
+ }
+
+ if (turn === 0) {
+ response.write(
+ delta({
+ tool_calls: [
+ {
+ index: 0,
+ id: 'call_generate_image',
+ type: 'function',
+ function: {
+ name: 'generate_image',
+ arguments: JSON.stringify({ prompt: 'a weekly activity chart' })
+ }
+ }
+ ]
+ })
+ )
+ } else {
+ response.write(delta({ content: 'Here is your ' }))
+ response.write(delta({ content: 'weekly summary.' }))
+ }
+ response.write('data: [DONE]\n\n')
+ response.end()
+ })
+})
+
+server.listen(port, '127.0.0.1')
+
+// Plain executable JavaScript cannot carry a TypeScript return annotation.
+// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
+const close = () => server.close(() => process.exit(0))
+process.on('SIGTERM', close)
+process.on('SIGINT', close)
diff --git a/e2e/fixtures/fake-sd-cli.mjs b/e2e/fixtures/fake-sd-cli.mjs
new file mode 100644
index 00000000..41c4cb52
--- /dev/null
+++ b/e2e/fixtures/fake-sd-cli.mjs
@@ -0,0 +1,10 @@
+#!/usr/bin/env node
+
+// Native image-runtime boundary for cancellation E2E. It behaves like a running
+// sd-cli job and deliberately never completes; the real imagegen owner must kill
+// this process when the user presses Stop.
+
+process.stderr.write('sampling step 1 / 20\n')
+setInterval(() => {
+ process.stderr.write('sampling step 2 / 20\n')
+}, 1000)
diff --git a/e2e/onboarding-resume.spec.ts b/e2e/onboarding-resume.spec.ts
new file mode 100644
index 00000000..83ac29da
--- /dev/null
+++ b/e2e/onboarding-resume.spec.ts
@@ -0,0 +1,162 @@
+/**
+ * RELEASE_TEST_CHECKLIST #12 - persisted onboarding and interrupted setup work
+ * survive a real Electron process relaunch. The interrupted transfer fixture is
+ * written at the network/filesystem boundary in the exact registry and partial-
+ * file format owned by the production model manager; all recovery reads and UI
+ * behavior run through the production app.
+ */
+import {
+ test,
+ expect,
+ _electron as electron,
+ type ElectronApplication,
+ type Page
+} from '@playwright/test'
+import type { ChildProcess } from 'child_process'
+import fs from 'fs'
+import os from 'os'
+import path from 'path'
+
+interface InterruptedFixture {
+ modelId: string
+ fileName: string
+ partial: Buffer
+}
+
+let app: ElectronApplication
+let page: Page
+let userDataDir: string
+
+async function waitForExit(child: ChildProcess): Promise {
+ if (child.exitCode !== null) return
+ await new Promise((resolve) => child.once('exit', () => resolve()))
+}
+
+async function launchApp(): Promise {
+ app = await electron.launch({
+ args: ['.'],
+ env: {
+ ...process.env,
+ OFFGRID_USER_DATA: userDataDir,
+ OFFGRID_DATA_DIR: userDataDir,
+ OFFGRID_PRO: '0',
+ NODE_ENV: 'production'
+ }
+ })
+ page = await app.firstWindow()
+ await page.waitForLoadState('domcontentloaded')
+}
+
+async function finishOnboarding(): Promise {
+ for (let step = 0; step < 6; step += 1) {
+ const button = page.getByRole('button', { name: /Continue|Start using Off Grid/i })
+ if (!(await button.isVisible().catch(() => false))) return
+ await button.click()
+ }
+}
+
+async function closeApp(): Promise {
+ const child = app.process()
+ await app.close()
+ await waitForExit(child)
+}
+
+test.beforeEach(async () => {
+ userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'offgrid-onboarding-resume-'))
+ await launchApp()
+})
+
+test.afterEach(async () => {
+ if (app?.process().exitCode === null) await app.close()
+ fs.rmSync(userDataDir, { recursive: true, force: true })
+})
+
+test('relaunch resumes onboarding progress and one interrupted transfer (#12)', async () => {
+ await expect(page.getByRole('heading', { name: 'Off Grid AI' })).toBeVisible()
+ await page.getByRole('button', { name: 'Continue' }).click()
+ await expect.poll(() => page.evaluate(() => localStorage.getItem('onboarding_step'))).toBe('1')
+ for (const capability of ['Chat', 'Vision', 'Image', 'Voice', 'Speech', 'Projects']) {
+ await expect(page.getByText(capability, { exact: true })).toBeVisible()
+ }
+ await page.getByRole('button', { name: 'Continue' }).click()
+ await expect(page.getByText('Replay', { exact: true })).toBeVisible()
+ expect(await page.evaluate(() => localStorage.getItem('onboarding_step'))).toBe('2')
+
+ await closeApp()
+ await launchApp()
+
+ await expect(page.getByText('Replay', { exact: true })).toBeVisible()
+ await expect(page.getByRole('heading', { name: 'Off Grid AI' })).toHaveCount(0)
+
+ await finishOnboarding()
+ await expect(page.getByRole('heading', { name: 'Models' })).toBeVisible()
+ expect(await page.evaluate(() => localStorage.getItem('onboarding_step'))).toBeNull()
+ const modelsDir = await page.evaluate(async () => (await window.api.checkModelStatus()).modelsDir)
+ expect(path.resolve(modelsDir).startsWith(path.resolve(userDataDir))).toBe(true)
+
+ await page.getByRole('button', { name: 'Configure', exact: true }).click()
+ await expect(page.getByRole('heading', { name: 'Set up your local AI' })).toBeVisible()
+
+ const fixture = await page.evaluate(async (): Promise> => {
+ const [plan, catalog] = await Promise.all([
+ window.api.setupPlan('balanced'),
+ window.api.getModelCatalog()
+ ])
+ const selected = plan.items.find((item) => !item.installed) ?? plan.items[0]
+ const entry = catalog.models.find((model) => model.id === selected?.id)
+ const fileName = entry?.files[0]?.name
+ if (!selected || !fileName) throw new Error('Setup plan did not expose a downloadable model')
+ return { modelId: selected.id, fileName }
+ })
+ const interrupted: InterruptedFixture = {
+ ...fixture,
+ partial: Buffer.from('synthetic partial model payload for relaunch recovery')
+ }
+ fs.mkdirSync(modelsDir, { recursive: true })
+ fs.writeFileSync(path.join(modelsDir, `${interrupted.fileName}.part`), interrupted.partial)
+ fs.writeFileSync(
+ path.join(modelsDir, 'downloads.json'),
+ JSON.stringify([
+ {
+ modelId: interrupted.modelId,
+ status: 'downloading',
+ percent: 37,
+ currentFile: interrupted.fileName,
+ downloadedMB: '1',
+ totalMB: '3'
+ }
+ ])
+ )
+
+ await closeApp()
+ await launchApp()
+
+ await expect(page.getByRole('heading', { name: 'Models' })).toBeVisible()
+ await expect(page.getByRole('button', { name: /Continue|Start using Off Grid/i })).toHaveCount(0)
+
+ const downloads = await page.evaluate(async () => window.api.listDownloads())
+ expect(downloads).toEqual([
+ expect.objectContaining({
+ modelId: interrupted.modelId,
+ status: 'failed',
+ percent: 37,
+ error: 'interrupted — retry to resume'
+ })
+ ])
+ expect(fs.readFileSync(path.join(modelsDir, `${interrupted.fileName}.part`))).toEqual(
+ interrupted.partial
+ )
+
+ await page.getByRole('button', { name: 'Expand sidebar' }).click()
+ await page.getByRole('button', { name: 'Settings', exact: true }).click()
+ await expect(page.getByRole('heading', { name: 'Settings' })).toBeVisible()
+ await page.getByRole('button', { name: /Setup & health/ }).click()
+
+ await expect(page.getByText(interrupted.modelId, { exact: true })).toBeVisible()
+ await expect(page.getByText('interrupted — retry to resume', { exact: true })).toBeVisible()
+ await expect(page.getByRole('button', { name: 'Retry', exact: true })).toBeVisible()
+ expect(await page.evaluate(async () => (await window.api.listDownloads()).length)).toBe(1)
+ expect(fs.readFileSync(path.join(modelsDir, `${interrupted.fileName}.part`))).toEqual(
+ interrupted.partial
+ )
+})
diff --git a/e2e/pro.spec.ts b/e2e/pro.spec.ts
index 2da751b8..ff2d7005 100644
--- a/e2e/pro.spec.ts
+++ b/e2e/pro.spec.ts
@@ -1,222 +1,533 @@
/**
- * Pro-tier E2E for the two production fixes this change ships. Launches the REAL built
+ * Pro-tier E2E for behavior that crosses the Electron and OS boundaries. Launches the real built
* app with pro features active (OFFGRID_PRO=1, no license needed) against a fresh temp
* profile, seeded with deterministic pro data (OFFGRID_SEED_PRO=force on the TEMP
- * profile — never the real one). Drives both fixes end-to-end through the real main
+ * profile - never the real one). Drives these paths end-to-end through the real main
* process:
*
* 1. Replay only surfaces moments backed by a captured SCREEN — connector-only
* observations (Attio/Linear/Gmail with no screenshot) must not appear. Asserted
* against the live crm:replay-* IPC: every moment an entity returns lines up with a
* real captured frame.
- * 2. Restoring a copied FILE (of any type, including images) puts a file-url (Finder
+ * 2. Text and pixel images cross the real OS clipboard, encrypted history, and restore IPC.
+ * 3. Restoring a copied FILE (of any type, including images) puts a file-url (Finder
* pastes the file), the path as plain text (terminal pastes the path), and the file's
* native bytes (an editor pastes the content) on the OS clipboard. Driven through the
* real capture → restore loop, asserting the resulting NSPasteboard flavors.
+ * 4. Vault copy controls write through the reliable main-process clipboard bridge.
*
- * macOS only (the app is macOS; fix 2 uses NSPasteboard via osascript). Requires the pro
- * package to be present — skipped in a core-only checkout, exactly as the build gates pro.
+ * The file-flavor assertions are macOS-only because they use NSPasteboard via osascript. Requires
+ * the pro package to be present - skipped in a core-only checkout, exactly as the build gates pro.
*/
-import { test, expect, _electron as electron, type ElectronApplication, type Page } from '@playwright/test';
-import os from 'os';
-import path from 'path';
-import fs from 'fs';
+import {
+ test,
+ expect,
+ _electron as electron,
+ type ElectronApplication,
+ type Page
+} from '@playwright/test'
+import os from 'os'
+import path from 'path'
+import fs from 'fs'
// Mirrors electron.vite.config's proExists gate: without the pro package, OFFGRID_PRO=1
// can't activate pro features, so these surfaces would render the free upgrade screen.
-const PRO_PRESENT = fs.existsSync(path.resolve('pro/package.json'));
+const PRO_PRESENT = fs.existsSync(path.resolve('pro/package.json'))
-let app: ElectronApplication;
-let page: Page;
-let userDataDir: string;
+let app: ElectronApplication
+let page: Page
+let userDataDir: string
const nav = async (label: string): Promise => {
- await page.getByRole('button', { name: label, exact: true }).first().click();
- await page.waitForTimeout(500);
-};
+ await page.getByRole('button', { name: label, exact: true }).first().click()
+ await page.waitForTimeout(500)
+}
+
+const waitForCapturedClip = async (
+ contentType: 'text' | 'image' | 'file',
+ textContent: string | null,
+ excludedIds: string[] = []
+): Promise =>
+ page.evaluate(
+ async ({ expectedContentType, expectedTextContent, excludedClipIds }) => {
+ const api = (
+ window as unknown as {
+ api: { proInvoke: (c: string, ...a: unknown[]) => Promise }
+ }
+ ).api
+ const deadline = Date.now() + 12000
+ while (Date.now() < deadline) {
+ const items = (await api.proInvoke('clipboard:list', 50)) as {
+ id: string
+ contentType: string
+ textContent: string | null
+ }[]
+ const hit = items.find(
+ (item) =>
+ item.contentType === expectedContentType &&
+ item.textContent === expectedTextContent &&
+ !excludedClipIds.includes(item.id)
+ )
+ if (hit) return hit.id
+ await new Promise((resolve) => setTimeout(resolve, 300))
+ }
+ throw new Error(
+ `Timed out waiting for ${expectedContentType} clipboard item ${String(expectedTextContent)}`
+ )
+ },
+ {
+ expectedContentType: contentType,
+ expectedTextContent: textContent,
+ excludedClipIds: excludedIds
+ }
+ )
+
+const restoreCapturedClip = async (id: string): Promise =>
+ page.evaluate(async (clipId) => {
+ const api = (
+ window as unknown as { api: { proInvoke: (c: string, ...a: unknown[]) => Promise } }
+ ).api
+ return (await api.proInvoke('clipboard:restore', clipId)) as boolean
+ }, id)
+
+const expectSystemClipboardText = async (expected: string): Promise => {
+ await expect
+ .poll(() => app.evaluate(async ({ clipboard }) => clipboard.readText()), { timeout: 3000 })
+ .toBe(expected)
+}
test.beforeAll(async () => {
- test.skip(!PRO_PRESENT, 'pro package not present — pro features cannot activate');
- userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'offgrid-pro-'));
+ test.skip(!PRO_PRESENT, 'pro package not present — pro features cannot activate')
+ userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'offgrid-pro-'))
app = await electron.launch({
args: ['.'],
env: {
...process.env,
OFFGRID_USER_DATA: userDataDir,
OFFGRID_PRO: '1', // force pro on without a license (pro code is bundled in this checkout)
+ OFFGRID_SEED: 'force', // core chats, knowledge, and RAG schema used by cross-surface Search
OFFGRID_SEED_PRO: 'force', // deterministic observations + entities + replay frames (TEMP profile only)
- NODE_ENV: 'production',
- },
- });
- page = await app.firstWindow();
- await page.waitForLoadState('domcontentloaded');
+ NODE_ENV: 'production'
+ }
+ })
+ page = await app.firstWindow()
+ await page.waitForLoadState('domcontentloaded')
// Click through onboarding into the app shell.
for (let i = 0; i < 8; i++) {
- const btn = page.getByRole('button', { name: /Continue|Start using Off Grid/i });
- if (!(await btn.isVisible().catch(() => false))) break;
- await btn.click();
- await page.waitForTimeout(400);
+ const btn = page.getByRole('button', { name: /Continue|Start using Off Grid/i })
+ if (!(await btn.isVisible().catch(() => false))) break
+ await btn.click()
+ await page.waitForTimeout(400)
+ }
+ try {
+ await page.getByRole('button', { name: 'Expand sidebar' }).click({ timeout: 4000 })
+ } catch {
+ /* already open */
}
- try { await page.getByRole('button', { name: 'Expand sidebar' }).click({ timeout: 4000 }); } catch { /* already open */ }
- await page.waitForTimeout(500);
+ await page.waitForTimeout(500)
// Seeding runs async on the main process after IPC setup — give it a beat to land.
- await page.waitForTimeout(1500);
-});
+ await page.waitForTimeout(1500)
+})
test.afterAll(async () => {
- await app?.close();
- try { fs.rmSync(userDataDir, { recursive: true, force: true }); } catch { /* ignore */ }
-});
+ await app?.close()
+ try {
+ fs.rmSync(userDataDir, { recursive: true, force: true })
+ } catch {
+ /* ignore */
+ }
+})
test('Replay is unlocked in the pro build (renders the manager, not the upgrade screen)', async () => {
- await nav('Replay');
- await expect(page.getByText('Off Grid Pro · Available now')).toHaveCount(0);
+ await nav('Replay')
+ await expect(page.getByText('Off Grid Pro · Available now')).toHaveCount(0)
// The seeded day has frames, so the film + scrubber render.
- await expect(page.getByText(/frames?$/).first()).toBeVisible();
-});
+ await expect(page.getByText(/frames?$/).first()).toBeVisible()
+})
test('Replay moments are backed by a captured screen — connector-only moments are dropped', async () => {
// Exercise the live crm:replay-* IPC exactly as the screen does (same day window).
const result = await page.evaluate(async () => {
- const api = (window as unknown as { api: Record Promise> }).api;
- const sec = (await api.crmReplayDefaultDay()) as number;
- const d = new Date(sec * 1000);
- const start = new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
- const s = Math.floor(start / 1000);
- const e = Math.floor((start + 86400000) / 1000);
- const frames = (await api.crmReplayFrames(s, e)) as { ts: number }[];
- const threads = (await api.crmReplayThreads(s, e)) as { entityId?: number }[];
- const seg = threads.find((t) => typeof t.entityId === 'number');
- if (!seg || seg.entityId == null) return { ok: false, reason: 'no entity thread', frames: frames.length };
- const day = (await api.crmReplayEntityDay(seg.entityId, s, e)) as { scenes: { ts: number; surface: string | null }[] };
- return { ok: true, entityId: seg.entityId, frameTs: frames.map((f) => f.ts), scenes: day.scenes };
- });
-
- expect(result.ok, `setup: ${(result as { reason?: string }).reason ?? ''}`).toBe(true);
- const r = result as { frameTs: number[]; scenes: { ts: number; surface: string | null }[] };
+ const api = (
+ window as unknown as { api: Record Promise> }
+ ).api
+ const sec = (await api.crmReplayDefaultDay()) as number
+ const d = new Date(sec * 1000)
+ const start = new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime()
+ const s = Math.floor(start / 1000)
+ const e = Math.floor((start + 86400000) / 1000)
+ const frames = (await api.crmReplayFrames(s, e)) as { ts: number }[]
+ const threads = (await api.crmReplayThreads(s, e)) as { entityId?: number }[]
+ const seg = threads.find((t) => typeof t.entityId === 'number')
+ if (!seg || seg.entityId == null)
+ return { ok: false, reason: 'no entity thread', frames: frames.length }
+ const day = (await api.crmReplayEntityDay(seg.entityId, s, e)) as {
+ scenes: { ts: number; surface: string | null }[]
+ }
+ return {
+ ok: true,
+ entityId: seg.entityId,
+ frameTs: frames.map((f) => f.ts),
+ scenes: day.scenes
+ }
+ })
+
+ expect(result.ok, `setup: ${(result as { reason?: string }).reason ?? ''}`).toBe(true)
+ const r = result as { frameTs: number[]; scenes: { ts: number; surface: string | null }[] }
// The entity (drawn from a work thread) has on-screen activity this day.
- expect(r.scenes.length).toBeGreaterThan(0);
+ expect(r.scenes.length).toBeGreaterThan(0)
// The invariant the fix enforces: every moment shown lines up with a captured
// screen frame. A connector-only observation (no screenshot) would have a ts with
// no matching frame — before the fix it leaked through here.
- const frameTs = new Set(r.frameTs);
+ const frameTs = new Set(r.frameTs)
for (const sc of r.scenes) {
- const hasFrame = r.frameTs.some((t) => Math.abs(t - sc.ts) <= 5) || frameTs.has(sc.ts);
- expect(hasFrame, `moment at ${sc.ts} (${sc.surface}) has no captured screen`).toBe(true);
+ const hasFrame = r.frameTs.some((t) => Math.abs(t - sc.ts) <= 5) || frameTs.has(sc.ts)
+ expect(hasFrame, `moment at ${sc.ts} (${sc.surface}) has no captured screen`).toBe(true)
}
-});
+})
+
+test('Replay renders every synthetic capture in chronological order with usable timestamps', async () => {
+ await nav('Replay')
+
+ const capturePaths = fs
+ .readdirSync(path.join(userDataDir, 'captures'))
+ .filter((name) => /^capture-\d+\.png$/.test(name))
+ .sort((a, b) => {
+ const timestamp = (name: string): number => Number(/^capture-(\d+)\.png$/.exec(name)![1])
+ return timestamp(a) - timestamp(b)
+ })
+ .map((name) => path.join(userDataDir, 'captures', name))
+
+ const frames = await page.evaluate(async () => {
+ const api = (
+ window as unknown as { api: Record Promise> }
+ ).api
+ const dayStartSec = (await api.crmReplayDefaultDay()) as number
+ const day = new Date(dayStartSec * 1000)
+ const startSec = Math.floor(
+ new Date(day.getFullYear(), day.getMonth(), day.getDate()).getTime() / 1000
+ )
+ const replayFrames = (await api.crmReplayFrames(startSec, startSec + 86400)) as {
+ ts: number
+ path: string
+ app: string | null
+ caption: string | null
+ }[]
+ return replayFrames.map((frame) => ({
+ ...frame,
+ fullTime: new Date(frame.ts * 1000).toLocaleTimeString([], {
+ hour: 'numeric',
+ minute: '2-digit',
+ second: '2-digit'
+ }),
+ shortTime: new Date(frame.ts * 1000).toLocaleTimeString([], {
+ hour: 'numeric',
+ minute: '2-digit'
+ })
+ }))
+ })
+
+ expect(frames.length).toBeGreaterThan(2)
+ expect(frames.map((frame) => frame.path)).toEqual(capturePaths)
+
+ const currentImage = page.locator('img[src^="ogcapture://"]')
+ const commentary = page.locator('aside')
+ const assertCurrentFrame = async (index: number): Promise => {
+ const frame = frames[index]!
+ await expect(currentImage).toHaveAttribute('src', `ogcapture://${frame.path}`)
+ await expect(commentary.getByText(frame.app ?? 'Screen', { exact: true })).toBeVisible()
+ await expect(commentary.getByText(frame.fullTime, { exact: true })).toBeVisible()
+ if (frame.caption) {
+ await expect(commentary.getByText(frame.caption, { exact: true })).toBeVisible()
+ }
+ await expect(page.getByText(frame.shortTime, { exact: true }).first()).toBeVisible()
+ await expect(page.getByText(`${index + 1}/${frames.length}`, { exact: true })).toBeVisible()
+ }
+
+ await assertCurrentFrame(frames.length - 1)
+ for (let index = frames.length - 2; index >= 0; index--) {
+ await page.keyboard.press('ArrowLeft')
+ await assertCurrentFrame(index)
+ }
+ for (let index = 1; index < frames.length; index++) {
+ await page.keyboard.press('ArrowRight')
+ await assertCurrentFrame(index)
+ }
+})
+
+test('Search opens Replay at the selected captured moment instead of a timeline boundary', async () => {
+ const expected = await page.evaluate(async () => {
+ const api = (
+ window as unknown as { api: Record Promise> }
+ ).api
+ const defaultDaySec = (await api.crmReplayDefaultDay()) as number
+ const day = new Date(defaultDaySec * 1000)
+ const startSec = Math.floor(
+ new Date(day.getFullYear(), day.getMonth(), day.getDate()).getTime() / 1000
+ )
+ const frames = (await api.crmReplayFrames(startSec, startSec + 86400)) as {
+ ts: number
+ path: string
+ app: string | null
+ caption: string | null
+ }[]
+ const middle = (frames.length - 1) / 2
+ const interiorIndices = frames
+ .map((_, index) => index)
+ .filter((index) => index > 0 && index < frames.length - 1)
+ .sort((a, b) => Math.abs(a - middle) - Math.abs(b - middle))
+
+ for (const selectedIndex of interiorIndices) {
+ const selected = frames[selectedIndex]!
+ const terms = (selected.caption?.match(/[A-Za-z0-9]+(?:\.[A-Za-z0-9]+)*/g) ?? [])
+ .filter((term) => term.length >= 7)
+ .sort((a, b) => b.length - a.length)
+ for (const query of terms) {
+ const hits = (await api.universalSearch(query, {
+ limit: 8,
+ semantic: false
+ })) as {
+ kind: string
+ title: string
+ snippet: string
+ ts: number
+ imagePath: string | null
+ }[]
+ const hit = hits.find(
+ (candidate) => candidate.kind === 'screen' && candidate.imagePath === selected.path
+ )
+ if (hit) {
+ return {
+ query,
+ hit,
+ selected,
+ selectedIndex,
+ frameCount: frames.length,
+ fullTime: new Date(selected.ts * 1000).toLocaleTimeString([], {
+ hour: 'numeric',
+ minute: '2-digit',
+ second: '2-digit'
+ })
+ }
+ }
+ }
+ }
+ return null
+ })
+
+ expect(expected).not.toBeNull()
+ if (!expected) throw new Error('Expected the synthetic search hit to have a Replay frame')
+ expect(expected.selectedIndex).toBeGreaterThan(0)
+ expect(expected.selectedIndex).toBeLessThan(expected.frameCount - 1)
+ expect(expected.hit.imagePath).toBe(expected.selected.path)
+
+ await page.keyboard.press('Meta+K')
+ const search = page.getByRole('dialog', { name: 'Search Off Grid' })
+ await expect(search).toBeVisible()
+ await search.getByPlaceholder('Search everything…').fill(expected.query)
+ const result = search.getByText(expected.hit.snippet, { exact: true })
+ await expect(result).toBeVisible()
+ await result.click()
+
+ await expect(search).toBeHidden()
+ await expect(page.getByRole('heading', { name: 'Replay', exact: true })).toBeVisible()
+ await expect(page.locator('img[src^="ogcapture://"]')).toHaveAttribute(
+ 'src',
+ `ogcapture://${expected.selected.path}`
+ )
+ const commentary = page.locator('aside')
+ await expect(
+ commentary.getByText(expected.selected.app ?? 'Screen', { exact: true })
+ ).toBeVisible()
+ await expect(commentary.getByText(expected.fullTime, { exact: true })).toBeVisible()
+ await expect(commentary.getByText(expected.hit.snippet, { exact: true })).toBeVisible()
+ await expect(
+ page.getByText(`${expected.selectedIndex + 1}/${expected.frameCount}`, { exact: true })
+ ).toBeVisible()
+})
test('Clipboard is unlocked in the pro build', async () => {
- await nav('Clipboard');
- await expect(page.getByText('Off Grid Pro · Available now')).toHaveCount(0);
- await expect(page.getByPlaceholder('Search content or tags…')).toBeVisible();
-});
+ await nav('Clipboard')
+ await expect(page.getByText('Off Grid Pro · Available now')).toHaveCount(0)
+ await expect(page.getByPlaceholder('Search content or tags…')).toBeVisible()
+})
test('Voice is unlocked in the pro build (renders the dictation library)', async () => {
- await nav('Voice');
- await expect(page.getByText('Off Grid Pro · Available now')).toHaveCount(0);
+ await nav('Voice')
+ await expect(page.getByText('Off Grid Pro · Available now')).toHaveCount(0)
// The real screen: a search box, the dictation CTA, and the file-transcribe entry.
- await expect(page.getByPlaceholder('Search transcripts')).toBeVisible();
- await expect(page.getByRole('button', { name: 'Start dictation' })).toBeVisible();
- await expect(page.getByRole('button', { name: 'Transcribe file' })).toBeVisible();
-});
+ await expect(page.getByPlaceholder('Search transcripts')).toBeVisible()
+ await expect(page.getByRole('button', { name: 'Start dictation' })).toBeVisible()
+ await expect(page.getByRole('button', { name: 'Transcribe file' })).toBeVisible()
+})
+
+test('Vault copy actions write username, revealed password, and URL to the OS clipboard', async () => {
+ const entry = {
+ title: 'E2E Vault Login',
+ username: 'vault-user@offgrid.test',
+ password: 'vault-password-e2e',
+ url: 'https://vault-e2e.offgrid.test'
+ }
+ const seeded = await page.evaluate(async (input) => {
+ const api = (
+ window as unknown as { api: { proInvoke: (c: string, ...a: unknown[]) => Promise } }
+ ).api
+ const initialized = (await api.proInvoke('vault:init', 'vault-master-password')) as {
+ ok: boolean
+ error?: string
+ }
+ if (!initialized.ok) return initialized
+ return (await api.proInvoke('vault:entries:add', { ...input, type: 'login' })) as {
+ ok: boolean
+ error?: string
+ }
+ }, entry)
+ expect(seeded.ok, seeded.error).toBe(true)
+
+ await nav('Vault')
+ await page.getByRole('button', { name: new RegExp(entry.title) }).click()
+
+ const usernameRow = page.getByText('Username / Email', { exact: true }).locator('..')
+ await usernameRow.getByRole('button', { name: 'Copy', exact: true }).click()
+ await expectSystemClipboardText(entry.username)
+
+ const passwordRow = page.getByText('Password', { exact: true }).locator('..')
+ await passwordRow.getByTitle('Reveal').click()
+ await passwordRow.getByRole('button', { name: 'Copy', exact: true }).click()
+ await expectSystemClipboardText(entry.password)
+
+ const websiteRow = page.getByText('Website', { exact: true }).locator('..')
+ await websiteRow.getByRole('button', { name: 'Copy', exact: true }).click()
+ await expectSystemClipboardText(entry.url)
+})
+
+test('Capturing and restoring text crosses the real OS clipboard and encrypted history', async () => {
+ const payload = `off-grid clipboard text ${Date.now()}-${process.pid}`
+ await app.evaluate(async ({ clipboard }, text) => {
+ clipboard.clear()
+ clipboard.writeText(text)
+ }, payload)
+
+ const capturedId = await waitForCapturedClip('text', payload)
+
+ await app.evaluate(async ({ clipboard }) => {
+ clipboard.writeText('clipboard overwritten before restore')
+ })
+ expect(await restoreCapturedClip(capturedId)).toBe(true)
+ await expectSystemClipboardText(payload)
+})
+
+test('Capturing a pixel image persists its bytes and restores the bitmap', async () => {
+ const existingImageIds = await page.evaluate(async () => {
+ const api = (
+ window as unknown as { api: { proInvoke: (c: string, ...a: unknown[]) => Promise } }
+ ).api
+ const items = (await api.proInvoke('clipboard:list', 50, 'image')) as { id: string }[]
+ return items.map((item) => item.id)
+ })
+ const sharp = (await import('sharp')).default
+ const png = await sharp({
+ create: { width: 37, height: 29, channels: 3, background: { r: 28, g: 211, b: 153 } }
+ })
+ .png()
+ .toBuffer()
+ await app.evaluate(async ({ clipboard, nativeImage }, base64) => {
+ clipboard.clear()
+ clipboard.writeImage(nativeImage.createFromBuffer(Buffer.from(base64, 'base64')))
+ }, png.toString('base64'))
+
+ const capturedId = await waitForCapturedClip('image', null, existingImageIds)
+ await app.evaluate(async ({ clipboard }) =>
+ clipboard.writeText('image overwritten before restore')
+ )
+ expect(await restoreCapturedClip(capturedId)).toBe(true)
+
+ const restored = await app.evaluate(async ({ clipboard }) => {
+ const image = clipboard.readImage()
+ return { empty: image.isEmpty(), size: image.getSize() }
+ })
+ expect(restored.empty).toBe(false)
+ expect(restored.size).toEqual({ width: 37, height: 29 })
+})
test('Restoring a copied file puts BOTH the path text and the file-url on the clipboard', async () => {
// 1. A real file on disk (written from the test process — Playwright's evaluate
// sandbox has no `require`), then simulate a Finder "copy file" by putting its
// file-url on the OS clipboard. The 500ms poller captures it into history.
- const { pathToFileURL } = await import('url');
- const fp = path.join(userDataDir, 'e2e-clip-sample.txt');
- fs.writeFileSync(fp, 'off-grid clipboard e2e payload');
- const copied = { fp, basename: path.basename(fp), fileUrl: pathToFileURL(fp).href };
+ const { pathToFileURL } = await import('url')
+ const fp = path.join(userDataDir, 'e2e-clip-sample.txt')
+ fs.writeFileSync(fp, 'off-grid clipboard e2e payload')
+ const copied = { fp, basename: path.basename(fp), fileUrl: pathToFileURL(fp).href }
await app.evaluate(async ({ clipboard }, fileUrl) => {
- clipboard.clear();
- clipboard.writeBuffer('public.file-url', Buffer.from(fileUrl, 'utf8'));
- }, copied.fileUrl);
+ clipboard.clear()
+ clipboard.writeBuffer('public.file-url', Buffer.from(fileUrl, 'utf8'))
+ }, copied.fileUrl)
// 2. Wait for capture, then restore that item via the real IPC.
- const restoredId = await page.evaluate(async (basename) => {
- const api = (window as unknown as { api: { proInvoke: (c: string, ...a: unknown[]) => Promise } }).api;
- const deadline = Date.now() + 12000;
- while (Date.now() < deadline) {
- const items = (await api.proInvoke('clipboard:list', 50)) as { id: string; contentType: string; textContent: string | null }[];
- const hit = items.find((it) => it.contentType === 'file' && it.textContent === basename);
- if (hit) { await api.proInvoke('clipboard:restore', hit.id); return hit.id; }
- await new Promise((res) => setTimeout(res, 300));
- }
- return null;
- }, copied.basename);
+ const restoredId = await waitForCapturedClip('file', copied.basename)
- expect(restoredId, 'file clip was captured and restored').not.toBeNull();
+ expect(await restoreCapturedClip(restoredId)).toBe(true)
// 3. The multi-flavor write runs through osascript (async); poll the pasteboard.
const pb = await app.evaluate(async ({ clipboard }) => {
- const deadline = Date.now() + 6000;
- let formats: string[] = [];
- let text = '';
+ const deadline = Date.now() + 6000
+ let formats: string[] = []
+ let text = ''
while (Date.now() < deadline) {
- formats = clipboard.availableFormats();
- text = clipboard.readText();
- if (formats.includes('text/plain') && formats.includes('text/uri-list')) break;
- await new Promise((res) => setTimeout(res, 200));
+ formats = clipboard.availableFormats()
+ text = clipboard.readText()
+ if (formats.includes('text/plain') && formats.includes('text/uri-list')) break
+ await new Promise((res) => setTimeout(res, 200))
}
- return { formats, text };
- });
+ return { formats, text }
+ })
// Finder flavor (file-url) AND terminal flavor (plain-text path) both present.
- expect(pb.formats).toContain('text/uri-list');
- expect(pb.formats).toContain('text/plain');
+ expect(pb.formats).toContain('text/uri-list')
+ expect(pb.formats).toContain('text/plain')
// The plain text is the file's path (so a terminal paste yields the path).
- expect(pb.text).toContain(copied.basename);
-});
+ expect(pb.text).toContain(copied.basename)
+})
test('Restoring a copied IMAGE file still gives the terminal a path (plus pixels)', async () => {
// An image file is still a file: pasting it into a terminal must yield the path, not
// nothing. Regression for the image-only branch that wrote pixels and no text.
- const { pathToFileURL } = await import('url');
+ const { pathToFileURL } = await import('url')
// A real 48x48 PNG (a 1x1 is too small for readImage to register as a bitmap).
- const sharp = (await import('sharp')).default;
- const png = await sharp({ create: { width: 48, height: 48, channels: 3, background: { r: 200, g: 30, b: 90 } } })
+ const sharp = (await import('sharp')).default
+ const png = await sharp({
+ create: { width: 48, height: 48, channels: 3, background: { r: 200, g: 30, b: 90 } }
+ })
.png()
- .toBuffer();
- const fp = path.join(userDataDir, 'e2e-clip-image.png');
- fs.writeFileSync(fp, png);
- const copied = { basename: path.basename(fp), fileUrl: pathToFileURL(fp).href };
+ .toBuffer()
+ const fp = path.join(userDataDir, 'e2e-clip-image.png')
+ fs.writeFileSync(fp, png)
+ const copied = { basename: path.basename(fp), fileUrl: pathToFileURL(fp).href }
await app.evaluate(async ({ clipboard }, fileUrl) => {
- clipboard.clear();
- clipboard.writeBuffer('public.file-url', Buffer.from(fileUrl, 'utf8'));
- }, copied.fileUrl);
+ clipboard.clear()
+ clipboard.writeBuffer('public.file-url', Buffer.from(fileUrl, 'utf8'))
+ }, copied.fileUrl)
- const restoredId = await page.evaluate(async (basename) => {
- const api = (window as unknown as { api: { proInvoke: (c: string, ...a: unknown[]) => Promise } }).api;
- const deadline = Date.now() + 12000;
- while (Date.now() < deadline) {
- const items = (await api.proInvoke('clipboard:list', 50)) as { id: string; contentType: string; textContent: string | null }[];
- const hit = items.find((it) => it.contentType === 'file' && it.textContent === basename);
- if (hit) { await api.proInvoke('clipboard:restore', hit.id); return hit.id; }
- await new Promise((res) => setTimeout(res, 300));
- }
- return null;
- }, copied.basename);
- expect(restoredId, 'image clip was captured and restored').not.toBeNull();
+ const restoredId = await waitForCapturedClip('file', copied.basename)
+ expect(await restoreCapturedClip(restoredId)).toBe(true)
const pb = await app.evaluate(async ({ clipboard }) => {
- const deadline = Date.now() + 6000;
- let formats: string[] = [];
- let text = '';
- let imageEmpty = true;
+ const deadline = Date.now() + 6000
+ let formats: string[] = []
+ let text = ''
+ let imageEmpty = true
while (Date.now() < deadline) {
- formats = clipboard.availableFormats();
- text = clipboard.readText();
- imageEmpty = clipboard.readImage().isEmpty();
- if (formats.includes('text/plain') && !imageEmpty) break;
- await new Promise((res) => setTimeout(res, 200));
+ formats = clipboard.availableFormats()
+ text = clipboard.readText()
+ imageEmpty = clipboard.readImage().isEmpty()
+ if (formats.includes('text/plain') && !imageEmpty) break
+ await new Promise((res) => setTimeout(res, 200))
}
- return { formats, text, imageEmpty };
- });
+ return { formats, text, imageEmpty }
+ })
// Terminal path AND the file-url AND the image pixels — all from one copied image.
- expect(pb.formats).toContain('text/plain');
- expect(pb.text).toContain(copied.basename);
- expect(pb.imageEmpty, 'image pixels still on the clipboard for image editors').toBe(false);
-});
+ expect(pb.formats).toContain('text/plain')
+ expect(pb.text).toContain(copied.basename)
+ expect(pb.imageEmpty, 'image pixels still on the clipboard for image editors').toBe(false)
+})
diff --git a/e2e/projects-layout.spec.ts b/e2e/projects-layout.spec.ts
new file mode 100644
index 00000000..1df3d1e9
--- /dev/null
+++ b/e2e/projects-layout.spec.ts
@@ -0,0 +1,145 @@
+/**
+ * RELEASE_TEST_CHECKLIST #59 - a populated Projects workspace uses the desktop
+ * canvas as a dense master-detail surface, with project chats laid out in a
+ * multi-column collection. Synthetic records enter through the production IPC
+ * handlers and are rendered by the production Electron UI.
+ */
+import {
+ test,
+ expect,
+ _electron as electron,
+ type ElectronApplication,
+ type Page
+} from '@playwright/test'
+import fs from 'fs'
+import os from 'os'
+import path from 'path'
+
+let app: ElectronApplication
+let page: Page
+let userDataDir: string
+
+async function finishOnboarding(): Promise {
+ for (let step = 0; step < 6; step += 1) {
+ const button = page.getByRole('button', { name: /Continue|Start using Off Grid/i })
+ if (!(await button.isVisible().catch(() => false))) return
+ await button.click()
+ }
+}
+
+test.beforeAll(async () => {
+ userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'offgrid-projects-layout-'))
+ app = await electron.launch({
+ args: ['.'],
+ env: {
+ ...process.env,
+ OFFGRID_USER_DATA: userDataDir,
+ OFFGRID_PRO: '0',
+ NODE_ENV: 'production'
+ }
+ })
+ page = await app.firstWindow()
+ await page.setViewportSize({ width: 1600, height: 900 })
+ await page.waitForLoadState('domcontentloaded')
+ await finishOnboarding()
+ await expect(page.getByRole('heading', { name: 'Models' })).toBeVisible()
+
+ await page.evaluate(async () => {
+ const projectIds: string[] = []
+ for (let index = 1; index <= 12; index += 1) {
+ const ordinal = String(index).padStart(2, '0')
+ const id = await window.api.createProject({
+ name: `Synthetic Project ${ordinal}`,
+ description: `Synthetic desktop-layout fixture ${ordinal}`
+ })
+ projectIds.push(id)
+ }
+
+ const detailProjectId = projectIds.at(-1)
+ if (!detailProjectId) throw new Error('Synthetic project setup failed')
+ for (let index = 1; index <= 8; index += 1) {
+ const ordinal = String(index).padStart(2, '0')
+ await window.api.createRagConversation(
+ `synthetic-project-chat-${ordinal}`,
+ `Synthetic chat ${ordinal}`,
+ detailProjectId
+ )
+ }
+ })
+
+ await page.getByTitle('Projects').click()
+ await expect(page.getByRole('heading', { name: 'Projects' })).toBeVisible()
+})
+
+test.afterAll(async () => {
+ await app?.close()
+ fs.rmSync(userDataDir, { recursive: true, force: true })
+})
+
+test('populated projects stay dense, adjacent, and reachable on a wide desktop (#59)', async () => {
+ const heading = page.getByRole('heading', { name: 'Projects' })
+ const master = heading.locator('xpath=../..')
+ const projectList = master.locator('.overflow-y-auto')
+ const detail = master.locator('xpath=following-sibling::div[1]')
+ const lastProject = master.getByRole('button', { name: 'Synthetic Project 12' })
+
+ await expect(master.getByRole('button', { name: /^Synthetic Project \d{2}$/ })).toHaveCount(12)
+ await lastProject.click()
+ await expect(detail.getByText('Synthetic Project 12', { exact: true })).toBeVisible()
+ await expect(detail.getByText('8 chats', { exact: true })).toBeVisible()
+
+ const geometry = await Promise.all([
+ master.boundingBox(),
+ detail.boundingBox(),
+ projectList.boundingBox(),
+ lastProject.boundingBox()
+ ])
+ const [masterBox, detailBox, listBox, selectedBox] = geometry
+ expect(masterBox).not.toBeNull()
+ expect(detailBox).not.toBeNull()
+ expect(listBox).not.toBeNull()
+ expect(selectedBox).not.toBeNull()
+ if (!masterBox || !detailBox || !listBox || !selectedBox) return
+
+ const masterShare = masterBox.width / (masterBox.width + detailBox.width)
+ expect(masterBox.width).toBeGreaterThanOrEqual(180)
+ expect(masterBox.width).toBeLessThanOrEqual(280)
+ expect(masterShare).toBeGreaterThan(0.1)
+ expect(masterShare).toBeLessThan(0.22)
+ expect(Math.abs(detailBox.x - (masterBox.x + masterBox.width))).toBeLessThanOrEqual(2)
+ expect(detailBox.width).toBeGreaterThan(masterBox.width * 3)
+ expect(Math.abs(detailBox.height - masterBox.height)).toBeLessThanOrEqual(2)
+ expect(selectedBox.x).toBeGreaterThanOrEqual(listBox.x)
+ expect(selectedBox.x + selectedBox.width).toBeLessThanOrEqual(listBox.x + listBox.width)
+ expect(selectedBox.y).toBeGreaterThanOrEqual(listBox.y)
+ expect(selectedBox.y + selectedBox.height).toBeLessThanOrEqual(listBox.y + listBox.height)
+
+ const viewControls = ['Chats', 'Artifacts', 'Knowledge & settings'].map((name) =>
+ detail.getByRole('button', { name, exact: true })
+ )
+ for (const control of viewControls) {
+ await expect(control).toBeVisible()
+ const box = await control.boundingBox()
+ expect(box).not.toBeNull()
+ if (!box) continue
+ expect(box.x).toBeGreaterThanOrEqual(detailBox.x)
+ expect(box.x + box.width).toBeLessThanOrEqual(detailBox.x + detailBox.width)
+ expect(box.y).toBeGreaterThanOrEqual(detailBox.y)
+ }
+
+ const chatGrid = detail.locator('.grid').first()
+ await expect(chatGrid.getByRole('button', { name: /^Synthetic chat \d{2}/ })).toHaveCount(8)
+ const columns = await chatGrid.evaluate(
+ (element) => getComputedStyle(element).gridTemplateColumns.split(' ').length
+ )
+ expect(columns).toBeGreaterThanOrEqual(3)
+
+ const gridBox = await chatGrid.boundingBox()
+ const firstCardBox = await chatGrid.getByRole('button').first().boundingBox()
+ expect(gridBox).not.toBeNull()
+ expect(firstCardBox).not.toBeNull()
+ if (!gridBox || !firstCardBox) return
+ expect(firstCardBox.width).toBeLessThan(gridBox.width / 2)
+ expect(firstCardBox.x).toBeGreaterThanOrEqual(detailBox.x)
+ expect(firstCardBox.x + firstCardBox.width).toBeLessThanOrEqual(detailBox.x + detailBox.width)
+})
diff --git a/e2e/resilience-single-instance.spec.ts b/e2e/resilience-single-instance.spec.ts
new file mode 100644
index 00000000..e4fb1d01
--- /dev/null
+++ b/e2e/resilience-single-instance.spec.ts
@@ -0,0 +1,124 @@
+import {
+ expect,
+ test,
+ _electron as electron,
+ type ElectronApplication,
+ type Page
+} from '@playwright/test'
+import { spawn, type ChildProcess } from 'node:child_process'
+import fs from 'node:fs'
+import os from 'node:os'
+import path from 'node:path'
+import electronExecutable from 'electron'
+import { GATEWAY_PORT, MEDIA_PORT } from '../src/shared/ports'
+
+const executable = electronExecutable as unknown as string
+
+let app: ElectronApplication
+let page: Page
+let userDataDir: string
+let secondProcess: ChildProcess | null = null
+
+const waitForExit = async (child: ChildProcess, timeoutMs = 15_000): Promise => {
+ if (child.exitCode !== null) return child.exitCode
+ return new Promise((resolve, reject) => {
+ const timeout = setTimeout(() => reject(new Error('Electron process did not exit')), timeoutMs)
+ child.once('exit', (code) => {
+ clearTimeout(timeout)
+ resolve(code)
+ })
+ })
+}
+
+const waitForOwnedPortsToClose = async (): Promise => {
+ const deadline = Date.now() + 10_000
+ while (Date.now() < deadline) {
+ const reachable = await Promise.all(
+ [GATEWAY_PORT, MEDIA_PORT].map((port) =>
+ fetch(`http://127.0.0.1:${port}/`, { signal: AbortSignal.timeout(250) })
+ .then(() => true)
+ .catch(() => false)
+ )
+ )
+ if (reachable.every((value) => !value)) return
+ await new Promise((resolve) => setTimeout(resolve, 100))
+ }
+ throw new Error('Electron model ports remained reachable after app teardown')
+}
+
+test.beforeEach(async () => {
+ userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'offgrid-single-instance-e2e-'))
+ app = await electron.launch({
+ args: ['.'],
+ env: {
+ ...process.env,
+ OFFGRID_USER_DATA: userDataDir,
+ OFFGRID_PRO: '0',
+ NODE_ENV: 'production'
+ }
+ })
+ page = await app.firstWindow()
+ await page.waitForLoadState('domcontentloaded')
+})
+
+test.afterEach(async () => {
+ const ownerProcess = app?.process()
+ try {
+ if (secondProcess?.exitCode === null) {
+ secondProcess.kill('SIGKILL')
+ await waitForExit(secondProcess)
+ }
+ await app?.close()
+ if (ownerProcess) await waitForExit(ownerProcess)
+ await waitForOwnedPortsToClose()
+ } catch (error) {
+ if (ownerProcess?.exitCode === null) ownerProcess.kill('SIGKILL')
+ throw error
+ } finally {
+ secondProcess = null
+ fs.rmSync(userDataDir, { recursive: true, force: true })
+ }
+})
+
+test('redirects a second launch to the running owner without starting competing model ports', async () => {
+ await app.evaluate(({ BrowserWindow }) => BrowserWindow.getAllWindows()[0]?.minimize())
+ await expect
+ .poll(() =>
+ app.evaluate(({ BrowserWindow }) => BrowserWindow.getAllWindows()[0]?.isMinimized())
+ )
+ .toBe(true)
+
+ secondProcess = spawn(executable, ['.'], {
+ cwd: process.cwd(),
+ env: {
+ ...process.env,
+ OFFGRID_USER_DATA: userDataDir,
+ OFFGRID_PRO: '0',
+ NODE_ENV: 'production'
+ },
+ stdio: ['ignore', 'pipe', 'pipe']
+ })
+ const second = secondProcess
+ let secondOutput = ''
+ second.stdout?.on('data', (chunk) => {
+ secondOutput += String(chunk)
+ })
+ second.stderr?.on('data', (chunk) => {
+ secondOutput += String(chunk)
+ })
+
+ expect(await waitForExit(second)).toBe(0)
+ await expect
+ .poll(() =>
+ app.evaluate(({ BrowserWindow }) => BrowserWindow.getAllWindows()[0]?.isMinimized())
+ )
+ .toBe(false)
+ await expect(page.locator('#root')).not.toBeEmpty()
+ expect(app.process().exitCode).toBeNull()
+ expect(secondOutput).not.toMatch(/EADDRINUSE|model[^\n]*corrupt/i)
+
+ const gatewayHealthy = await fetch('http://127.0.0.1:7878/health')
+ .then((response) => response.ok)
+ .catch(() => false)
+ expect(gatewayHealthy).toBe(true)
+})
diff --git a/e2e/settings-residency.spec.ts b/e2e/settings-residency.spec.ts
new file mode 100644
index 00000000..60b81b22
--- /dev/null
+++ b/e2e/settings-residency.spec.ts
@@ -0,0 +1,119 @@
+import {
+ test,
+ expect,
+ _electron as electron,
+ type ElectronApplication,
+ type Locator,
+ type Page
+} from '@playwright/test'
+import type { ChildProcess } from 'child_process'
+import fs from 'fs'
+import os from 'os'
+import path from 'path'
+
+let app: ElectronApplication | null = null
+let page: Page
+let userDataDir: string
+
+const launchApp = async (): Promise => {
+ app = await electron.launch({
+ args: ['.'],
+ env: {
+ ...process.env,
+ OFFGRID_USER_DATA: userDataDir,
+ OFFGRID_PRO: '0',
+ NODE_ENV: 'production'
+ }
+ })
+ page = await app.firstWindow()
+ await page.waitForLoadState('domcontentloaded')
+}
+
+const waitForExit = async (child: ChildProcess): Promise => {
+ if (child.exitCode !== null) return
+ await new Promise((resolve) => child.once('exit', () => resolve()))
+}
+
+const closeApp = async (): Promise => {
+ const running = app
+ if (!running) return
+ app = null
+ const child = running.process()
+ await running.close()
+ await waitForExit(child)
+}
+
+const completeOnboarding = async (): Promise => {
+ for (let step = 0; step < 8; step += 1) {
+ const button = page.getByRole('button', { name: /Continue|Start using Off Grid/i })
+ if (!(await button.isVisible().catch(() => false))) return
+ await button.click()
+ }
+}
+
+const openModelMemory = async (): Promise => {
+ const expandSidebar = page.getByRole('button', { name: 'Expand sidebar' })
+ if (await expandSidebar.isVisible().catch(() => false)) await expandSidebar.click()
+ await page.getByRole('button', { name: 'Settings', exact: true }).first().click()
+ await page.getByRole('button', { name: /Model memory/ }).click()
+}
+
+const persistedResidency = async (): Promise> =>
+ page.evaluate(() => window.api.residencyGet())
+
+const residencyControls = (): {
+ chat: Locator
+ unlocked: Locator[]
+} => ({
+ chat: page.getByRole('switch', { name: 'Chat model residency' }),
+ unlocked: [
+ page.getByRole('switch', { name: 'Image generation residency' }),
+ page.getByRole('switch', { name: 'Dictation (speech-to-text) residency' }),
+ page.getByRole('switch', { name: 'Text-to-speech residency' })
+ ]
+})
+
+test.beforeEach(async () => {
+ userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'offgrid-residency-e2e-'))
+ await launchApp()
+ await completeOnboarding()
+})
+
+test.afterEach(async () => {
+ await closeApp()
+ fs.rmSync(userDataDir, { recursive: true, force: true })
+})
+
+test('runtime residency controls persist across relaunch while chat stays required', async () => {
+ await openModelMemory()
+
+ const { chat, unlocked } = residencyControls()
+ await expect(chat).toBeChecked()
+ await expect(chat).toBeDisabled()
+ await expect(page.getByText('in-memory (required)')).toBeVisible()
+
+ for (const control of unlocked) {
+ await expect(control).not.toBeChecked()
+ await control.click()
+ await expect(control).toBeChecked()
+ }
+
+ await expect
+ .poll(persistedResidency)
+ .toEqual({ llm: 'resident', image: 'resident', stt: 'resident', tts: 'resident' })
+
+ await closeApp()
+ await launchApp()
+ await openModelMemory()
+
+ const relaunched = residencyControls()
+ await expect(relaunched.chat).toBeChecked()
+ await expect(relaunched.chat).toBeDisabled()
+ for (const control of relaunched.unlocked) await expect(control).toBeChecked()
+ await expect(persistedResidency()).resolves.toEqual({
+ llm: 'resident',
+ image: 'resident',
+ stt: 'resident',
+ tts: 'resident'
+ })
+})
diff --git a/e2e/smoke.spec.ts b/e2e/smoke.spec.ts
index 3b30ed2d..5f4e9559 100644
--- a/e2e/smoke.spec.ts
+++ b/e2e/smoke.spec.ts
@@ -10,86 +10,188 @@
*
* Requires a build first: `npm run build` (the test:e2e script does this).
*/
-import { test, expect, _electron as electron, type ElectronApplication, type Page } from '@playwright/test';
-import os from 'os';
-import path from 'path';
-import fs from 'fs';
+import {
+ test,
+ expect,
+ _electron as electron,
+ type ElectronApplication,
+ type Page
+} from '@playwright/test'
+import os from 'os'
+import path from 'path'
+import fs from 'fs'
-let app: ElectronApplication;
-let page: Page;
-let userDataDir: string;
+let app: ElectronApplication
+let page: Page
+let userDataDir: string
-test.beforeAll(async () => {
- userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'offgrid-e2e-'));
+test.beforeEach(async () => {
+ userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'offgrid-e2e-'))
app = await electron.launch({
args: ['.'],
env: {
...process.env,
OFFGRID_USER_DATA: userDataDir, // pristine first-run
OFFGRID_PRO: '0', // deterministic free tier (no permission gate)
- NODE_ENV: 'production',
- },
- });
- page = await app.firstWindow();
- await page.waitForLoadState('domcontentloaded');
-});
+ NODE_ENV: 'production'
+ }
+ })
+ page = await app.firstWindow()
+ await page.waitForLoadState('domcontentloaded')
+})
-test.afterAll(async () => {
- await app?.close();
- try { fs.rmSync(userDataDir, { recursive: true, force: true }); } catch { /* ignore */ }
-});
+test.afterEach(async () => {
+ await app?.close()
+ try {
+ fs.rmSync(userDataDir, { recursive: true, force: true })
+ } catch {
+ /* ignore */
+ }
+})
test('boots fresh without a white screen and exposes the preload bridge', async () => {
// Renderer mounted with real content (not a blank/crashed page).
- await expect(page.locator('#root')).not.toBeEmpty();
+ await expect(page.locator('#root')).not.toBeEmpty()
// Preload contextBridge is wired.
- const hasApi = await page.evaluate(() => typeof (window as { api?: unknown }).api === 'object');
- expect(hasApi).toBe(true);
-});
+ const hasApi = await page.evaluate(() => typeof (window as { api?: unknown }).api === 'object')
+ expect(hasApi).toBe(true)
+})
test('shows onboarding on a fresh install', async () => {
- await expect(page.getByText(/Off Grid/i).first()).toBeVisible();
- await expect(page.getByRole('button', { name: /Continue|Start using Off Grid/i })).toBeVisible();
-});
+ await expect(page.getByText(/Off Grid/i).first()).toBeVisible()
+ await expect(page.getByRole('button', { name: /Continue|Start using Off Grid/i })).toBeVisible()
+})
test('onboarding surfaces the Pro capability grid', async () => {
// Advance until the Pro step renders its capability cards, then assert a few
// capabilities are shown by name (Replay, Meetings, Vault). Regression guard
// for the onboarding redesign that showcases the Pro layer.
- const btn = page.getByRole('button', { name: /Continue|Start using Off Grid/i });
+ const btn = page.getByRole('button', { name: /Continue|Start using Off Grid/i })
for (let i = 0; i < 6; i++) {
- if (await page.getByText('Meetings').isVisible().catch(() => false)) break;
- if (!(await btn.isVisible().catch(() => false))) break;
- await btn.click();
- await page.waitForTimeout(400);
+ if (
+ await page
+ .getByText('Meetings')
+ .isVisible()
+ .catch(() => false)
+ )
+ break
+ if (!(await btn.isVisible().catch(() => false))) break
+ await btn.click()
+ await page.waitForTimeout(400)
}
- await expect(page.getByText('Replay')).toBeVisible();
- await expect(page.getByText('Meetings')).toBeVisible();
- await expect(page.getByText('Vault')).toBeVisible();
- await page.screenshot({ path: 'e2e/screenshots/onboarding-pro-grid.png' });
-});
+ await expect(page.getByText('Replay')).toBeVisible()
+ await expect(page.getByText('Meetings')).toBeVisible()
+ await expect(page.getByText('Vault')).toBeVisible()
+ await page.screenshot({ path: 'e2e/screenshots/onboarding-pro-grid.png' })
+})
test('completes onboarding and lands in the app shell', async () => {
// Click through every onboarding step (Continue × N, then "Start using Off Grid").
for (let i = 0; i < 6; i++) {
- const btn = page.getByRole('button', { name: /Continue|Start using Off Grid/i });
- if (!(await btn.isVisible().catch(() => false))) break;
- await btn.click();
- await page.waitForTimeout(400);
+ const btn = page.getByRole('button', { name: /Continue|Start using Off Grid/i })
+ if (!(await btn.isVisible().catch(() => false))) break
+ await btn.click()
+ await page.waitForTimeout(400)
}
// Free tier defaults to the Models screen — assert the app shell rendered.
- await expect(page.getByRole('heading', { name: 'Models' })).toBeVisible();
-});
+ await expect(page.getByRole('heading', { name: 'Models' })).toBeVisible()
+})
test('system:health IPC returns the component list', async () => {
const health = await page.evaluate(async () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
- return (window as any).api?.systemHealth?.();
- });
- expect(health).toBeTruthy();
- expect(Array.isArray(health.components)).toBe(true);
+ return (window as any).api?.systemHealth?.()
+ })
+ expect(health).toBeTruthy()
+ expect(Array.isArray(health.components)).toBe(true)
// The chat + gateway components are always reported.
- const ids = health.components.map((c: { id: string }) => c.id);
- expect(ids).toContain('chat');
- expect(ids).toContain('gateway');
-});
+ const ids = health.components.map((c: { id: string }) => c.id)
+ expect(ids).toContain('chat')
+ expect(ids).toContain('gateway')
+
+ let gateway: { modalities?: Record } = {}
+ await expect
+ .poll(
+ async () => {
+ try {
+ const response = await fetch('http://127.0.0.1:7878/health')
+ if (!response.ok) return false
+ gateway = await response.json()
+ return true
+ } catch {
+ return false
+ }
+ },
+ { timeout: 10000 }
+ )
+ .toBe(true)
+
+ const byId = new Map(
+ health.components.map((component: { id: string; status: string }) => [component.id, component])
+ )
+ expect(health.activeModel).toBeNull()
+ expect(byId.get('gateway')?.status).toBe('ready')
+ expect(byId.get('chat')?.status).toBe('not_installed')
+ const llamaReachable = await fetch('http://127.0.0.1:8439/health')
+ .then((response) => response.ok)
+ .catch(() => false)
+ expect(llamaReachable).toBe(false)
+
+ const gatewayBackedComponents = {
+ vision: 'vision_understanding',
+ embeddings: 'embeddings',
+ transcription: 'transcription',
+ speech: 'speech'
+ }
+ for (const [componentId, modalityId] of Object.entries(gatewayBackedComponents)) {
+ expect(byId.get(componentId)?.status).toBe(gateway.modalities?.[modalityId])
+ }
+ expect(byId.get('image')?.status).toBe(gateway.modalities?.image_generation)
+})
+
+test('gateway /v1/models serves active local models with modality metadata', async () => {
+ const modelsDir = path.join(userDataDir, 'models')
+ fs.mkdirSync(modelsDir, { recursive: true })
+ fs.writeFileSync(path.join(modelsDir, 'e2e-active.gguf'), 'synthetic gateway model fixture')
+ fs.writeFileSync(
+ path.join(modelsDir, 'active-model.json'),
+ JSON.stringify({ id: 'e2e-active-model', primary: 'e2e-active.gguf', mmproj: null })
+ )
+
+ let catalog: {
+ object?: string
+ data?: Array<{ id?: string; object?: string; kind?: string }>
+ models?: Array<{ name?: string; model?: string; kind?: string }>
+ } = {}
+ await expect
+ .poll(
+ async () => {
+ try {
+ const response = await fetch('http://127.0.0.1:7878/v1/models')
+ if (!response.ok) return false
+ catalog = await response.json()
+ return catalog.data?.some((model) => model.id === 'e2e-active-model') ?? false
+ } catch {
+ return false
+ }
+ },
+ { timeout: 10000 }
+ )
+ .toBe(true)
+
+ expect(catalog.object).toBe('list')
+ expect(catalog.data).toContainEqual(
+ expect.objectContaining({
+ id: 'e2e-active-model',
+ object: 'model',
+ kind: 'chat'
+ })
+ )
+ expect(catalog.models).toContainEqual(
+ expect.objectContaining({
+ name: 'e2e-active-model',
+ model: 'e2e-active-model',
+ kind: 'chat'
+ })
+ )
+})
diff --git a/e2e/tour.spec.ts b/e2e/tour.spec.ts
index e72de525..97bd18b6 100644
--- a/e2e/tour.spec.ts
+++ b/e2e/tour.spec.ts
@@ -5,102 +5,163 @@
* real one) and only navigates / reads — no destructive clicks — so it's safe.
* OFFGRID_PRO=0 forces deterministic free-tier UI.
*/
-import { test, expect, _electron as electron, type ElectronApplication, type Page } from '@playwright/test';
-import os from 'os';
-import path from 'path';
-import fs from 'fs';
+import {
+ test,
+ expect,
+ _electron as electron,
+ type ElectronApplication,
+ type Page
+} from '@playwright/test'
+import os from 'os'
+import path from 'path'
+import fs from 'fs'
+import { OFF_GRID_MOBILE_URL } from '../src/renderer/src/constants/links'
-let app: ElectronApplication;
-let page: Page;
-let userDataDir: string;
+let app: ElectronApplication
+let page: Page
+let userDataDir: string
const nav = async (label: string): Promise => {
- await page.getByRole('button', { name: label, exact: true }).first().click();
- await page.waitForTimeout(500);
-};
+ await page.getByRole('button', { name: label, exact: true }).first().click()
+ await page.waitForTimeout(500)
+}
test.beforeAll(async () => {
- userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'offgrid-tour-'));
+ userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'offgrid-tour-'))
app = await electron.launch({
args: ['.'],
- env: { ...process.env, OFFGRID_USER_DATA: userDataDir, OFFGRID_PRO: '0', NODE_ENV: 'production' },
- });
- page = await app.firstWindow();
- await page.waitForLoadState('domcontentloaded');
+ env: {
+ ...process.env,
+ OFFGRID_USER_DATA: userDataDir,
+ OFFGRID_PRO: '0',
+ NODE_ENV: 'production'
+ }
+ })
+ page = await app.firstWindow()
+ await page.waitForLoadState('domcontentloaded')
// Click through onboarding into the app shell.
for (let i = 0; i < 6; i++) {
- const btn = page.getByRole('button', { name: /Continue|Start using Off Grid/i });
- if (!(await btn.isVisible().catch(() => false))) break;
- await btn.click();
- await page.waitForTimeout(400);
+ const btn = page.getByRole('button', { name: /Continue|Start using Off Grid/i })
+ if (!(await btn.isVisible().catch(() => false))) break
+ await btn.click()
+ await page.waitForTimeout(400)
}
// Expand the sidebar so nav items have visible labels.
- try { await page.getByRole('button', { name: 'Expand sidebar' }).click({ timeout: 4000 }); } catch { /* already open */ }
- await page.waitForTimeout(500);
-});
+ try {
+ await page.getByRole('button', { name: 'Expand sidebar' }).click({ timeout: 4000 })
+ } catch {
+ /* already open */
+ }
+ await page.waitForTimeout(500)
+})
test.afterAll(async () => {
- await app?.close();
- try { fs.rmSync(userDataDir, { recursive: true, force: true }); } catch { /* ignore */ }
-});
+ await app?.close()
+ try {
+ fs.rmSync(userDataDir, { recursive: true, force: true })
+ } catch {
+ /* ignore */
+ }
+})
+
+test('window and runtime use the canonical desktop product name', async () => {
+ await expect(page).toHaveTitle('Off Grid AI Desktop')
+ expect(await app.evaluate(({ app: electronApp }) => electronApp.getName())).toBe(
+ 'Off Grid AI Desktop'
+ )
+})
test('Models: merged tab + use-cases + import', async () => {
- await expect(page.getByRole('heading', { name: 'Models' })).toBeVisible();
- await expect(page.getByRole('button', { name: /Import \.gguf/i })).toBeVisible();
- await expect(page.getByRole('button', { name: 'Coding', exact: true })).toBeVisible(); // use-case chip
-});
+ await expect(page.getByRole('heading', { name: 'Models' })).toBeVisible()
+ await expect(page.getByRole('button', { name: /Import \.gguf/i })).toBeVisible()
+ await expect(page.getByRole('button', { name: 'Coding', exact: true })).toBeVisible() // use-case chip
+})
test('Settings: setup, resource modes, storage, data & privacy all render', async () => {
- await nav('Settings');
+ await nav('Settings')
// Sections are collapsed accordions — the titles (headings) are always visible;
// expand a card to reveal its body before asserting the body content.
- await expect(page.getByRole('heading', { name: 'Setup & health' })).toBeVisible();
- await expect(page.getByRole('heading', { name: 'Data & privacy' })).toBeVisible();
+ await expect(page.getByRole('heading', { name: 'Setup & health' })).toBeVisible()
+ await expect(page.getByRole('heading', { name: 'Data & privacy' })).toBeVisible()
- await page.getByRole('button', { name: /Setup & health/ }).click(); // expand
- await expect(page.getByText('Configure it for me')).toBeVisible();
+ await page.getByRole('button', { name: /Setup & health/ }).click() // expand
+ await expect(page.getByText('Configure it for me')).toBeVisible()
// The resource-mode selector now lives inside the Configure card.
for (const m of ['Conservative', 'Balanced', 'Extreme']) {
// The mode button's accessible name includes its description, so match by substring.
- await expect(page.getByRole('button', { name: m }).first()).toBeVisible();
+ await expect(page.getByRole('button', { name: m }).first()).toBeVisible()
}
- await page.getByRole('button', { name: /Data & privacy/ }).click(); // expand
- await expect(page.getByText('Screen captures')).toBeVisible();
- await expect(page.getByText('Your data on this device')).toBeVisible();
-});
+ await page.getByRole('button', { name: /Data & privacy/ }).click() // expand
+ await expect(page.getByText('Screen captures')).toBeVisible()
+ await expect(page.getByText('Your data on this device')).toBeVisible()
+})
test('Resource mode is selectable (Conservative)', async () => {
// Ensure the Setup & health accordion is expanded (the modes live in its body).
- const cons = page.getByRole('button', { name: 'Conservative' }).first();
+ const cons = page.getByRole('button', { name: 'Conservative' }).first()
if (!(await cons.isVisible().catch(() => false))) {
- await page.getByRole('button', { name: /Setup & health/ }).click();
+ await page.getByRole('button', { name: /Setup & health/ }).click()
+ }
+ await cons.click()
+ await expect(cons).toHaveAttribute('aria-pressed', 'true')
+})
+
+test('every locked Pro navigation item renders its matching upgrade screen', async () => {
+ // Discover the lock-bearing buttons rendered from the production Pro catalog. This
+ // avoids duplicating its route list in the test and fails when a new locked item is
+ // added without a working upgrade destination.
+ const lockedButtons = page.locator('button').filter({
+ has: page.locator('svg title').filter({ hasText: /^Pro$/ })
+ })
+ const count = await lockedButtons.count()
+ expect(count).toBeGreaterThan(2)
+
+ const labels: string[] = []
+ for (let index = 0; index < count; index += 1) {
+ const button = lockedButtons.nth(index)
+ const label = (await button.locator('span.flex-1').innerText()).trim()
+ labels.push(label)
+ await button.click()
+ await expect(page.getByText('Off Grid Pro · Available now')).toBeVisible()
+ await expect(page.getByRole('heading', { name: label, exact: true })).toBeVisible()
}
- await cons.click();
- await expect(cons).toHaveAttribute('aria-pressed', 'true');
-});
-
-test('Clipboard is a Pro tab in the core build (shows upgrade)', async () => {
- // Clipboard moved to Pro: in the core/free tour (OFFGRID_PRO=0) the tab is
- // locked and renders the upgrade screen, not the clipboard manager/settings.
- // (Locked items carry a "Pro" lock label, so match by prefix, not exact.)
- await page.getByRole('button', { name: /Clipboard/ }).first().click();
- await page.waitForTimeout(500);
- await expect(page.getByText('Off Grid Pro · Available now')).toBeVisible();
- await expect(page.getByRole('heading', { name: 'Clipboard' })).toBeVisible();
-});
-
-test('Voice is a Pro tab in the core build (shows upgrade)', async () => {
- // Voice/dictation is Pro: in the free tour (OFFGRID_PRO=0) the tab is locked and
- // renders the upgrade screen, not the dictation library.
- await page.getByRole('button', { name: /Voice/ }).first().click();
- await page.waitForTimeout(500);
- await expect(page.getByText('Off Grid Pro · Available now')).toBeVisible();
- await expect(page.getByRole('heading', { name: 'Voice' })).toBeVisible();
-});
+
+ expect(new Set(labels).size).toBe(labels.length)
+ expect(await page.evaluate(() => window.api.isPro)).toBe(false)
+})
+
+test('purchase and product links open externally without navigating Electron', async () => {
+ await page.getByRole('button', { name: 'Replay Pro', exact: true }).click()
+ await expect(page.getByText('Off Grid Pro · Available now')).toBeVisible()
+
+ const expectedPayUrl = await page.evaluate(() => window.api.license.payUrl())
+ const electronUrl = page.url()
+ await app.evaluate(({ shell }) => {
+ const capture = globalThis as typeof globalThis & { __offgridOpenedExternal?: string[] }
+ capture.__offgridOpenedExternal = []
+ shell.openExternal = async (url: string): Promise => {
+ capture.__offgridOpenedExternal?.push(url)
+ }
+ })
+
+ await page.getByRole('button', { name: /Get Pro/ }).click()
+ await page.getByRole('button', { name: /Get Off Grid AI Mobile/ }).click()
+
+ await expect
+ .poll(() =>
+ app.evaluate(
+ () =>
+ (globalThis as typeof globalThis & { __offgridOpenedExternal?: string[] })
+ .__offgridOpenedExternal ?? []
+ )
+ )
+ .toEqual([expectedPayUrl, OFF_GRID_MOBILE_URL])
+ expect(page.url()).toBe(electronUrl)
+})
test('Gateway screen renders', async () => {
- await nav('Settings'); // leave the upgrade screen first
- await nav('Gateway');
- await expect(page.getByText(/OpenAI-compatible/i).first()).toBeVisible();
-});
+ await nav('Settings') // leave the upgrade screen first
+ await nav('Gateway')
+ await expect(page.getByText(/OpenAI-compatible/i).first()).toBeVisible()
+})
diff --git a/e2e/tts-speak.spec.ts b/e2e/tts-speak.spec.ts
new file mode 100644
index 00000000..339635b5
--- /dev/null
+++ b/e2e/tts-speak.spec.ts
@@ -0,0 +1,129 @@
+/**
+ * RELEASE_TEST_CHECKLIST #105 - the rendered Speak action reaches the production
+ * TTS path, strips markdown for speech, returns playable local WAV audio, and can
+ * be stopped. The heavyweight ONNX worker is the only replaced boundary.
+ */
+import {
+ test,
+ expect,
+ _electron as electron,
+ type ElectronApplication,
+ type Page
+} from '@playwright/test'
+import fs from 'node:fs'
+import os from 'node:os'
+import path from 'node:path'
+
+let app: ElectronApplication
+let page: Page
+let userDataDir: string
+let resourceDir: string
+let spokenTextPath: string
+
+async function finishOnboarding(): Promise {
+ for (let step = 0; step < 8; step += 1) {
+ const button = page.getByRole('button', { name: /Continue|Start using Off Grid/i })
+ if (!(await button.isVisible().catch(() => false))) return
+ await button.click()
+ }
+}
+
+async function dismissCapturePrompt(): Promise {
+ const dismiss = page.getByRole('button', { name: 'Dismiss', exact: true })
+ if (await dismiss.isVisible().catch(() => false)) await dismiss.click()
+}
+
+function writeWorkerFixture(): void {
+ fs.mkdirSync(resourceDir, { recursive: true })
+ fs.writeFileSync(
+ path.join(resourceDir, 'tts-worker.mjs'),
+ [
+ "import fs from 'node:fs'",
+ 'const [, , command, output] = process.argv',
+ "if (command !== 'speak' || !output) process.exit(2)",
+ "let input = ''",
+ "process.stdin.setEncoding('utf8')",
+ "process.stdin.on('data', chunk => { input += chunk })",
+ "process.stdin.on('end', () => {",
+ ' fs.writeFileSync(process.env.OFFGRID_TTS_CAPTURE, input)',
+ ' const sampleRate = 16000',
+ ' const sampleCount = sampleRate * 2',
+ ' const wav = Buffer.alloc(44 + sampleCount * 2)',
+ " wav.write('RIFF', 0)",
+ ' wav.writeUInt32LE(36 + sampleCount * 2, 4)',
+ " wav.write('WAVE', 8)",
+ " wav.write('fmt ', 12)",
+ ' wav.writeUInt32LE(16, 16)',
+ ' wav.writeUInt16LE(1, 20)',
+ ' wav.writeUInt16LE(1, 22)',
+ ' wav.writeUInt32LE(sampleRate, 24)',
+ ' wav.writeUInt32LE(sampleRate * 2, 28)',
+ ' wav.writeUInt16LE(2, 32)',
+ ' wav.writeUInt16LE(16, 34)',
+ " wav.write('data', 36)",
+ ' wav.writeUInt32LE(sampleCount * 2, 40)',
+ ' for (let index = 0; index < sampleCount; index += 1) {',
+ ' const sample = Math.sin((index / sampleRate) * Math.PI * 440) * 4000',
+ ' wav.writeInt16LE(Math.round(sample), 44 + index * 2)',
+ ' }',
+ ' fs.writeFileSync(output, wav)',
+ '})'
+ ].join('\n'),
+ { mode: 0o755 }
+ )
+}
+
+test.beforeAll(async () => {
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'offgrid-tts-speak-'))
+ userDataDir = path.join(root, 'profile')
+ resourceDir = path.join(root, 'resources')
+ spokenTextPath = path.join(root, 'spoken.txt')
+ writeWorkerFixture()
+
+ app = await electron.launch({
+ args: ['.'],
+ env: {
+ ...process.env,
+ OFFGRID_USER_DATA: userDataDir,
+ OFFGRID_RESOURCE_DIR: resourceDir,
+ OFFGRID_TTS_CAPTURE: spokenTextPath,
+ OFFGRID_PRO: '1',
+ NODE_ENV: 'production'
+ }
+ })
+ page = await app.firstWindow()
+ await page.waitForLoadState('domcontentloaded')
+ await finishOnboarding()
+ await expect(page.getByRole('button', { name: 'Chat', exact: true })).toBeVisible()
+
+ await page.evaluate(async () => {
+ await window.api.createRagConversation('tts-speak-release', 'Speak release reply', null)
+ await window.api.addRagMessage(
+ 'tts-speak-release',
+ 'assistant',
+ '## A **local** [reply](https://example.invalid) with `code`'
+ )
+ })
+ await page
+ .getByRole('button', { name: /chat|mind|ask/i })
+ .first()
+ .click()
+ await dismissCapturePrompt()
+})
+
+test.afterAll(async () => {
+ await app?.close()
+ fs.rmSync(path.dirname(userDataDir), { recursive: true, force: true })
+})
+
+test('Speak sends clean text through production TTS and plays local WAV audio (#105)', async () => {
+ await expect(page.getByText('A local reply with code', { exact: true })).toBeVisible()
+
+ await page.getByRole('button', { name: 'Speak', exact: true }).click()
+ await expect(page.getByRole('button', { name: 'Stop', exact: true })).toBeVisible()
+ await expect.poll(() => fs.existsSync(spokenTextPath)).toBe(true)
+ expect(fs.readFileSync(spokenTextPath, 'utf8')).toBe('A local reply with code')
+
+ await page.getByRole('button', { name: 'Stop', exact: true }).click()
+ await expect(page.getByRole('button', { name: 'Speak', exact: true })).toBeVisible()
+})
diff --git a/electron-builder.yml b/electron-builder.yml
index fbebeb0d..bb0b67df 100644
--- a/electron-builder.yml
+++ b/electron-builder.yml
@@ -1,12 +1,20 @@
# Single build is the pro-capable one (license-gated). Keep the `.pro` bundle id so
# existing Pro installs auto-update into it (bundle id + granted TCC permissions match).
appId: co.getoffgridai.desktop.pro
-productName: Off Grid AI
+productName: Off Grid AI Desktop
copyright: © Off Grid AI
+# This hook runs before electron-builder emits artifactCreated, which is the event
+# that schedules publishing. A truncated DMG therefore cannot reach GitHub.
+artifactBuildCompleted: scripts/verify-electron-builder-artifact.js
directories:
buildResources: build
files:
- '!**/.vscode/*'
+ # Local profiles and agent worktrees may contain private user data, downloaded
+ # multi-GB models, nested builds, or symlinks outside the repository. They are
+ # never application inputs. Keep this boundary here rather than relying on
+ # .gitignore: electron-builder packages filesystem contents, not only git files.
+ - '!{.demo-profile,.offgrid,.claude,.Codex,coverage}/**'
- '!src/*'
- '!pro/**'
- '!electron.vite.config.{js,ts,mjs,cjs}'
@@ -25,8 +33,8 @@ extraResources:
- from: resources
to: .
filter:
- - "**/*"
- - "!models/**"
+ - '**/*'
+ - '!models/**'
win:
executableName: off-grid-ai
# Without this, electron-builder ships the default Electron icon on Windows (the
@@ -63,6 +71,12 @@ mac:
# electron-builder's signing stand.
dmg:
artifactName: ${name}-${version}.${ext}
+ # The app is currently ~3.3 GB before compression. macOS 26 adds an EFI/GPT
+ # partition to large HFS images, so dmgbuild's computed size can leave less usable
+ # space than the bundle needs. Allocate deterministic headroom, then shrink after
+ # the copy so the published download remains compressed to its actual contents.
+ size: 5g
+ shrink: true
linux:
target:
- AppImage
diff --git a/electron.vite.config.ts b/electron.vite.config.ts
index cdafc596..db14eaa7 100644
--- a/electron.vite.config.ts
+++ b/electron.vite.config.ts
@@ -1,8 +1,10 @@
-import { resolve } from 'path'
-import { existsSync } from 'fs'
+import { resolve } from 'node:path'
+import { existsSync } from 'node:fs'
+import { randomBytes } from 'node:crypto'
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
+import { createRendererContentSecurityPolicy } from './src/shared/renderer-csp'
// Open-core seam: the private `pro/` git submodule is present only in paid
// builds. When it's missing (free / contributor build) we alias the pro entry
@@ -20,6 +22,8 @@ const proRenderer = proExists ? resolve('pro/renderer/index.tsx') : stub
// Baked into every bundle so runtime code can tell a pro build from a free build
// without relying on an env var default (which can't distinguish "unset" from "pro").
const proDefine = { __OFFGRID_PRO__: JSON.stringify(proExists) }
+const rendererStyleNonce = randomBytes(18).toString('base64url')
+const rendererContentSecurityPolicy = createRendererContentSecurityPolicy(rendererStyleNonce)
export default defineConfig({
main: {
@@ -43,6 +47,7 @@ export default defineConfig({
},
renderer: {
define: proDefine,
+ html: { cspNonce: rendererStyleNonce },
resolve: {
alias: {
'@renderer': resolve('src/renderer/src'),
@@ -51,6 +56,25 @@ export default defineConfig({
'@offgrid/pro/renderer': proRenderer
}
},
- plugins: [react(), tailwindcss()]
+ plugins: [
+ {
+ name: 'offgrid-renderer-csp',
+ transformIndexHtml: {
+ order: 'pre',
+ handler: () => [
+ {
+ tag: 'meta',
+ attrs: {
+ 'http-equiv': 'Content-Security-Policy',
+ content: rendererContentSecurityPolicy
+ },
+ injectTo: 'head-prepend'
+ }
+ ]
+ }
+ },
+ react(),
+ tailwindcss()
+ ]
}
})
diff --git a/eslint.config.mjs b/eslint.config.mjs
index aff5d3f9..d1a69ec4 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -4,12 +4,115 @@ import eslintConfigPrettier from '@electron-toolkit/eslint-config-prettier'
import eslintPluginReact from 'eslint-plugin-react'
import eslintPluginReactHooks from 'eslint-plugin-react-hooks'
import eslintPluginReactRefresh from 'eslint-plugin-react-refresh'
+import sonarjs from 'eslint-plugin-sonarjs'
+import tsESLint from 'typescript-eslint'
+
+// Typed dead-BRANCH gate: no-unnecessary-condition uses the type-checker to flag
+// conditions that are always truthy/falsy given the types — the exact AI pattern
+// (defensive `if (x && x.y)` after x is already non-null, dead `===` branches,
+// `x?.y` where x can't be null). Requires typed linting (projectService). The
+// backlog is fully ground to zero, so this is now ERROR: a new dead branch fails
+// the build. When the fix is a legit guard at an untyped boundary (JSON.parse /
+// IPC / external data), correct the TYPE so the guard becomes necessary — do NOT
+// weaken this back to warn or blanket-disable. Never auto-fixed (suggestion-only).
+// Scoped to the dirs the tsconfigs cover so projectService never errors on a stray file.
+const typedDeadBranchWarn = {
+ name: 'typed no-unnecessary-condition (error)',
+ files: [
+ 'src/main/**/*.ts',
+ 'src/preload/**/*.ts',
+ 'src/renderer/src/**/*.{ts,tsx}',
+ 'pro/main/**/*.ts',
+ 'pro/renderer/**/*.{ts,tsx}'
+ ],
+ ignores: ['**/*.{test,spec,dbtest}.{ts,tsx}', '**/__tests__/**', '**/*.d.ts'],
+ languageOptions: {
+ parser: tsESLint.parser,
+ parserOptions: { projectService: true, tsconfigRootDir: import.meta.dirname }
+ },
+ plugins: { '@typescript-eslint': tsESLint.plugin },
+ rules: { '@typescript-eslint/no-unnecessary-condition': 'error' }
+}
+
+// Sonar-grade rules (bugs, cognitive complexity, duplicated branches, dead code)
+// scoped to pro/** ONLY. Core src is covered by SonarCloud Automatic Analysis, so
+// running sonarjs there too would be redundant — but SonarCloud (public project)
+// never sees the private pro submodule, so this is how pro gets the same class of
+// checks. pro has no own toolchain; it's linted by this root config. Introduced at
+// WARN (ratchet, per CLAUDE.md "Pending hygiene adoption") with sonarjs's purely-
+// stylistic / already-owned rules turned off so what's left is real defect signal.
+const sonarProWarn = {
+ ...sonarjs.configs.recommended,
+ name: 'sonarjs on pro (warn ratchet)',
+ // Product code only — test files are intentionally explicit/repetitive; linting
+ // them for duplicate-string / complexity is noise (SonarCloud separates test from
+ // main sources for the same reason). Not suppression — correct scoping.
+ files: ['pro/**/*.{ts,tsx}'],
+ ignores: ['pro/**/*.{test,spec}.{ts,tsx}', 'pro/**/__tests__/**'],
+ rules: {
+ ...Object.fromEntries(
+ Object.keys(sonarjs.configs.recommended.rules ?? {}).map((rule) => [rule, 'warn'])
+ ),
+ // Pure style — not a defect:
+ 'sonarjs/arrow-function-convention': 'off',
+ 'sonarjs/file-header': 'off',
+ 'sonarjs/shorthand-property-grouping': 'off',
+ 'sonarjs/no-wildcard-import': 'off', // `import * as fs/http/path` is intentional here
+ 'sonarjs/void-use': 'off', // `void promise` is our intentional fire-and-forget idiom
+ // Owned by another gate / genuine false positives on this codebase:
+ 'sonarjs/no-implicit-dependencies': 'off', // dependency-cruiser owns dep boundaries
+ 'sonarjs/no-reference-error': 'off', // type-unaware; fires on the valid `NodeJS.Timeout` type — tsc is the real ref-error gate
+ 'sonarjs/publicly-writable-directories': 'off' // os.tmpdir() scratch files are legitimate
+ }
+}
+
+// Wednesday-solutions gold-standard structural + style rules (CLAUDE.md "Pending hygiene
+// adoption", part 2), introduced at WARN as a RATCHET: many current files exceed the caps
+// (MemoryChat ~2.6k lines, ipc.ts ~1.7k, …), so failing the build on them now would be
+// pointless noise. They surface as warnings and tighten to `error` as the god-files
+// decompose — never loosened to pass. `complexity` starts loose (15) per CLAUDE.md and
+// ratchets toward the gold standard (5). Product code only; tests are exempt (intentionally
+// explicit/repetitive). Formatting is prettier's job (eslintConfigPrettier), not these.
+const goldStandardRatchet = {
+ name: 'wednesday gold-standard (warn ratchet)',
+ files: ['src/**/*.{ts,tsx}', 'pro/**/*.{ts,tsx}'],
+ ignores: ['**/*.{test,spec,dbtest}.{ts,tsx}', '**/__tests__/**', '**/*.d.ts'],
+ rules: {
+ curly: ['warn', 'all'],
+ 'no-else-return': 'warn',
+ 'no-empty': 'warn',
+ 'prefer-template': 'warn',
+ 'no-console': ['warn', { allow: ['error', 'warn'] }],
+ 'max-params': ['warn', 3],
+ complexity: ['warn', 15],
+ 'max-lines-per-function': ['warn', 250],
+ 'max-lines': ['warn', 350],
+ '@typescript-eslint/no-shadow': 'warn'
+ }
+}
export default defineConfig(
- { ignores: ['**/node_modules', '**/dist', '**/out'] },
+ {
+ ignores: [
+ '**/node_modules/**',
+ '**/dist/**',
+ '**/out/**',
+ '**/coverage/**',
+ '.claude/**',
+ '.offgrid/**',
+ '.demo-profile/**',
+ 'component-library-animations/**',
+ 'resources/artifacts/**',
+ '**/*.min.js',
+ '**/*.min.css'
+ ]
+ },
tseslint.configs.recommended,
eslintPluginReact.configs.flat.recommended,
eslintPluginReact.configs.flat['jsx-runtime'],
+ sonarProWarn,
+ goldStandardRatchet,
+ typedDeadBranchWarn,
{
settings: {
react: {
diff --git a/integration-tests/conversation-rename.dbtest.ts b/integration-tests/conversation-rename.dbtest.ts
new file mode 100644
index 00000000..1f380e45
--- /dev/null
+++ b/integration-tests/conversation-rename.dbtest.ts
@@ -0,0 +1,148 @@
+// @vitest-environment jsdom
+
+/**
+ * RELEASE_TEST_CHECKLIST #44 - the production chat surface renames scoped and
+ * unscoped conversations through the real SQLite owner. Electron IPC and model
+ * processes are the only controlled boundaries. Remounting the surface proves
+ * the stored name survives navigation and is rendered in both sidebar and tab.
+ */
+import React from 'react'
+import { cleanup, render, screen, waitFor } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+import fs from 'fs'
+import os from 'os'
+import path from 'path'
+import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+
+const TMP_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'offgrid-conversation-rename-'))
+
+vi.mock('electron', () => ({
+ app: { getPath: () => TMP_DIR, isPackaged: false, getAppPath: () => process.cwd() },
+ safeStorage: {
+ isEncryptionAvailable: () => false,
+ encryptString: (value: string) => Buffer.from(value),
+ decryptString: (value: Buffer) => value.toString()
+ }
+}))
+
+const database = await import('@offgrid/core/main/database')
+const skills = await import('@offgrid/core/main/skills')
+const { MemoryChat } = await import('@renderer/components/MemoryChat')
+const { TooltipProvider } = await import('@renderer/components/ui/tooltip')
+
+function installApi(): void {
+ Object.assign(window, {
+ api: {
+ getRagConversations: async () => database.getRagConversations(),
+ getRagConversation: async (id: string) => database.getRagConversation(id),
+ getRagMessages: async (id: string) => database.getRagMessages(id),
+ updateRagConversationTitle: async (id: string, title: string) =>
+ database.updateRagConversationTitle(id, title),
+ getSettings: async () => database.getSettings(),
+ saveSetting: async (key: string, value: unknown) => database.saveSetting(key, value),
+ listSkills: async () => skills.listSkills(),
+ imageGenStatus: async () => ({ available: false, models: [], active: '' }),
+ onRagStream: () => () => undefined,
+ onImageGenProgress: () => () => undefined
+ }
+ })
+}
+
+function renderChat(): void {
+ render(React.createElement(TooltipProvider, null, React.createElement(MemoryChat)))
+}
+
+async function beginRename(user: ReturnType, title: string): Promise {
+ await user.click(await screen.findByRole('button', { name: `Conversation actions for ${title}` }))
+ await user.click(screen.getByRole('menuitem', { name: 'Rename' }))
+}
+
+beforeEach(() => {
+ database.getDB().exec('DELETE FROM rag_messages; DELETE FROM rag_conversations;')
+ installApi()
+ ;(Element.prototype as unknown as { scrollIntoView(): void }).scrollIntoView = () => {}
+ globalThis.requestAnimationFrame = (callback: FrameRequestCallback): number => {
+ callback(0)
+ return 1
+ }
+})
+
+afterEach(() => cleanup())
+
+afterAll(() => {
+ database.getDB().close()
+ fs.rmSync(TMP_DIR, { recursive: true, force: true })
+})
+
+describe(' conversation rename', () => {
+ it.each([
+ ['unscoped', null],
+ ['project-scoped', 'project-alpha']
+ ])(
+ 'persists a %s title in the sidebar and open tab after remount (#44)',
+ async (_, projectId) => {
+ database.createRagConversation('conversation-target', 'Before rename', projectId)
+ const user = userEvent.setup()
+ renderChat()
+
+ await beginRename(user, 'Before rename')
+ const input = screen.getByRole('textbox', {
+ name: 'Rename conversation'
+ }) as HTMLInputElement
+ expect(input.value).toBe('Before rename')
+ expect(document.activeElement).toBe(input)
+ expect(input.selectionStart).toBe(0)
+ expect(input.selectionEnd).toBe('Before rename'.length)
+
+ await user.clear(input)
+ await user.keyboard('{Enter}')
+ expect((await screen.findByRole('alert')).textContent).toContain('Enter a conversation name.')
+ expect(database.getRagConversation('conversation-target')?.title).toBe('Before rename')
+
+ await user.type(input, ' After rename ')
+ await user.keyboard('{Enter}')
+ await waitFor(() => expect(screen.getAllByText('After rename')).toHaveLength(2))
+ expect(database.getRagConversation('conversation-target')?.title).toBe('After rename')
+
+ cleanup()
+ installApi()
+ renderChat()
+ await waitFor(() => expect(screen.getAllByText('After rename')).toHaveLength(2))
+ expect(screen.queryByText('Before rename')).toBeNull()
+ }
+ )
+
+ it('keeps the inline editor open with a retry message when persistence fails (#44)', async () => {
+ database.createRagConversation('conversation-target', 'Stored name')
+ const user = userEvent.setup()
+ renderChat()
+ await beginRename(user, 'Stored name')
+
+ database.deleteRagConversation('conversation-target')
+ const input = screen.getByRole('textbox', { name: 'Rename conversation' })
+ await user.clear(input)
+ await user.type(input, 'Cannot persist')
+ await user.keyboard('{Enter}')
+
+ expect((await screen.findByRole('alert')).textContent).toContain('Rename failed. Try again.')
+ expect(
+ (screen.getByRole('textbox', { name: 'Rename conversation' }) as HTMLInputElement).value
+ ).toBe('Cannot persist')
+ })
+
+ it('cancels inline rename with Escape without writing (#44)', async () => {
+ database.createRagConversation('conversation-target', 'Keep this name')
+ const user = userEvent.setup()
+ renderChat()
+ await beginRename(user, 'Keep this name')
+
+ const input = screen.getByRole('textbox', { name: 'Rename conversation' })
+ await user.clear(input)
+ await user.type(input, 'Discard this draft')
+ await user.keyboard('{Escape}')
+
+ expect(screen.queryByRole('textbox', { name: 'Rename conversation' })).toBeNull()
+ expect(screen.getAllByText('Keep this name')).toHaveLength(2)
+ expect(database.getRagConversation('conversation-target')?.title).toBe('Keep this name')
+ })
+})
diff --git a/integration-tests/manual-model-setup.test.ts b/integration-tests/manual-model-setup.test.ts
new file mode 100644
index 00000000..f0bdbcbe
--- /dev/null
+++ b/integration-tests/manual-model-setup.test.ts
@@ -0,0 +1,256 @@
+// @vitest-environment jsdom
+/**
+ * RELEASE_TEST_CHECKLIST #11 - manual onboarding through the real product seam.
+ *
+ * PermissionGate, setup surface, Models screen, catalog, model manager, integrity checks,
+ * filesystem promotion, installed discovery, and activation remain real. The harness owns only
+ * the App shell's `og:navigate` handoff so unrelated app subsystems do not pollute this seam.
+ * Electron APIs and HTTP model delivery are the only controlled boundaries.
+ */
+import { cleanup, render, screen, waitFor, within } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+import fs from 'node:fs'
+import os from 'node:os'
+import path from 'node:path'
+import React from 'react'
+import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
+
+const originalDataDir = process.env.OFFGRID_DATA_DIR
+const testRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'offgrid-manual-setup-'))
+const dataDir = path.join(testRoot, 'data')
+process.env.OFFGRID_DATA_DIR = dataDir
+
+vi.mock('electron', () => ({
+ app: {
+ getPath: () => dataDir,
+ isPackaged: false,
+ getAppPath: () => process.cwd(),
+ getVersion: () => 'test'
+ },
+ safeStorage: {
+ isEncryptionAvailable: () => false,
+ encryptString: (value: string) => Buffer.from(value),
+ decryptString: (value: Buffer) => value.toString()
+ }
+}))
+
+const manager = await import('@offgrid/core/main/models-manager')
+const setup = await import('@offgrid/core/main/setup')
+const { CATALOG } = await import('@offgrid/models')
+
+const chosenModel = CATALOG.find(
+ (model) =>
+ model.kind === 'text' && model.files.length === 1 && model.files[0]?.name.endsWith('.gguf')
+)
+const unchosenModel = CATALOG.find(
+ (model) =>
+ model.kind === 'text' &&
+ model.files.length === 1 &&
+ model.files[0]?.name.endsWith('.gguf') &&
+ model.id !== chosenModel?.id
+)
+if (!chosenModel || !unchosenModel) {
+ throw new Error('Model catalog needs two single-file text models for manual setup coverage')
+}
+
+type Progress = import('@offgrid/core/main/models-manager').DownloadProgress
+
+function installStorage(): void {
+ const values = new Map([['onboarding_completed', 'true']])
+ const storage: Storage = {
+ get length() {
+ return values.size
+ },
+ clear: () => values.clear(),
+ getItem: (key) => values.get(key) ?? null,
+ key: (index) => [...values.keys()][index] ?? null,
+ removeItem: (key) => values.delete(key),
+ setItem: (key, value) => values.set(key, String(value))
+ }
+ Object.defineProperty(window, 'localStorage', { configurable: true, value: storage })
+ vi.stubGlobal('localStorage', storage)
+}
+
+function installApi(): { requestedUrls: string[] } {
+ const requestedUrls: string[] = []
+ const progressListeners = new Set<(progress: Progress) => void>()
+ vi.stubGlobal(
+ 'fetch',
+ vi.fn(async (input: string | URL | Request) => {
+ const url = String(input)
+ requestedUrls.push(url)
+ const bytes = Buffer.concat([Buffer.from('GGUF', 'ascii'), Buffer.alloc(2_044, 19)])
+ return new Response(new Uint8Array(bytes), {
+ status: 200,
+ headers: { 'content-length': String(bytes.length) }
+ })
+ })
+ )
+
+ const eventSubscription = (): (() => void) => () => {}
+ const values: Record = {
+ isPro: false,
+ platform: 'darwin',
+ getPermissionStatus: async () => ({
+ accessibility: true,
+ screenRecording: true,
+ allGranted: true
+ }),
+ checkModelStatus: async () => ({
+ downloaded: (await manager.listInstalled()).length > 0,
+ modelsDir: path.join(dataDir, 'models')
+ }),
+ getModelCatalog: manager.getCatalog,
+ getInstalledModels: manager.listInstalled,
+ getActiveModelIds: manager.getActiveModelIds,
+ activateModel: manager.activateModel,
+ estimateModelFit: setup.estimateModelFit,
+ downloadModel: async (modelId: string) =>
+ manager.downloadModel(modelId, (progress) => {
+ for (const listener of progressListeners) listener(progress)
+ }),
+ cancelModelDownload: async (modelId: string) => manager.cancelDownload(modelId),
+ onModelProgress: (listener: (progress: Progress) => void) => {
+ progressListeners.add(listener)
+ return () => progressListeners.delete(listener)
+ },
+ getLlmSettings: async () => ({ performanceMode: 'balanced' }),
+ setLlmSettings: async () => true,
+ setupPlan: setup.getSetupPlan,
+ systemHealth: async () => ({ ramGb: 64, components: [{ id: 'chat', status: 'ready' }] }),
+ imageGenStatus: async () => ({ available: false, models: [], active: '' }),
+ getStagedUpdateVersion: async () => null,
+ getSettings: async () => ({}),
+ listProjects: async () => [],
+ getRagConversations: async () => [],
+ meetingGetState: async () => ({
+ recording: false,
+ busy: false,
+ platform: null,
+ startedAt: 0,
+ warnUntil: 0,
+ error: ''
+ }),
+ onNewApproval: eventSubscription,
+ onNewAction: eventSubscription,
+ onUpdateDownloaded: eventSubscription,
+ onReprocessProgress: eventSubscription,
+ onSetupProgress: eventSubscription,
+ onNavigate: eventSubscription,
+ onMeetingState: eventSubscription,
+ onRagStream: eventSubscription
+ }
+ const api = new Proxy(values, {
+ get(target, property: string) {
+ if (property in target) return target[property]
+ return async () => undefined
+ }
+ })
+ Object.assign(window, { api })
+ return { requestedUrls }
+}
+
+// Electron installs preload before renderer modules evaluate. Preserve that ordering here because
+// ModelsScreen intentionally captures the stable preload bridge at module scope.
+const apiBoundary = installApi()
+const [{ PermissionGate }, { ModelsScreen }] = await Promise.all([
+ import('@renderer/components/PermissionGate'),
+ import('@renderer/components/ModelsScreen')
+])
+
+beforeAll(() => {
+ fs.mkdirSync(path.join(dataDir, 'models'), { recursive: true })
+})
+
+beforeEach(async () => {
+ installStorage()
+ window.history.replaceState(null, '', '/models')
+ window.matchMedia = vi.fn().mockReturnValue({
+ matches: false,
+ addEventListener: vi.fn(),
+ removeEventListener: vi.fn()
+ })
+ ;(globalThis as unknown as { ResizeObserver: unknown }).ResizeObserver = class {
+ observe(): void {}
+ unobserve(): void {}
+ disconnect(): void {}
+ }
+ ;(Element.prototype as unknown as { scrollIntoView: () => void }).scrollIntoView = () => {}
+ await manager.clearDownload(chosenModel.id)
+ await manager.clearDownload(unchosenModel.id)
+ await manager.deleteModel(chosenModel.id)
+ await manager.deleteModel(unchosenModel.id)
+})
+
+afterEach(() => {
+ cleanup()
+ vi.unstubAllGlobals()
+})
+
+afterAll(async () => {
+ await manager.clearDownload(chosenModel.id)
+ await manager.clearDownload(unchosenModel.id)
+ await manager.deleteModel(chosenModel.id)
+ await manager.deleteModel(unchosenModel.id)
+ if (originalDataDir === undefined) delete process.env.OFFGRID_DATA_DIR
+ else process.env.OFFGRID_DATA_DIR = originalDataDir
+ fs.rmSync(testRoot, { recursive: true, force: true })
+})
+
+describe('manual model setup', () => {
+ it('downloads and activates only the chosen model through manual setup (#11)', async () => {
+ const { requestedUrls } = apiBoundary
+ function ManualSetupHarness(): React.ReactElement {
+ const [view, setView] = React.useState<'workspace' | 'models'>('workspace')
+ React.useEffect(() => {
+ const navigate = (event: Event): void => {
+ if ((event as CustomEvent).detail === 'models') setView('models')
+ }
+ window.addEventListener('og:navigate', navigate)
+ return () => window.removeEventListener('og:navigate', navigate)
+ }, [])
+ return React.createElement(
+ PermissionGate,
+ null,
+ view === 'models'
+ ? React.createElement(ModelsScreen)
+ : React.createElement('main', null, 'Application workspace')
+ )
+ }
+ const user = userEvent.setup()
+ render(React.createElement(ManualSetupHarness))
+
+ await user.click(await screen.findByRole('button', { name: 'Configure' }))
+ await user.click(
+ await screen.findByRole('button', { name: 'or browse & pick a model yourself' })
+ )
+ expect(await screen.findByRole('heading', { name: 'Models' })).not.toBeNull()
+
+ const chosenCard = (await screen.findByText(chosenModel.name)).closest('[role="listitem"]')
+ if (!(chosenCard instanceof HTMLElement)) throw new Error('Chosen model card did not render')
+ await user.click(within(chosenCard).getByRole('button', { name: 'Download' }))
+
+ let installedCard: HTMLElement | null = null
+ await waitFor(() => {
+ installedCard = screen.getByText(chosenModel.name).closest('[role="listitem"]')
+ if (!(installedCard instanceof HTMLElement)) throw new Error('Installed model card missing')
+ expect(within(installedCard).getByRole('button', { name: 'Use' })).not.toBeNull()
+ })
+ expect(await manager.listInstalled()).toEqual([chosenModel.id])
+ expect(requestedUrls).toEqual(chosenModel.files.map((file) => file.url))
+ expect(requestedUrls).not.toEqual(expect.arrayContaining(unchosenModel.files.map((f) => f.url)))
+ expect(fs.existsSync(path.join(dataDir, 'models', chosenModel.files[0]!.name))).toBe(true)
+ expect(fs.existsSync(path.join(dataDir, 'models', unchosenModel.files[0]!.name))).toBe(false)
+
+ if (!(installedCard instanceof HTMLElement)) throw new Error('Installed model card missing')
+ await user.click(within(installedCard).getByRole('button', { name: 'Use' }))
+ await waitFor(() => expect(manager.getActiveModel()).toBe(chosenModel.id))
+ await expect(manager.getActiveModelIds()).resolves.toContain(chosenModel.id)
+ await expect(manager.listInstalled()).resolves.not.toContain(unchosenModel.id)
+ await waitFor(() => {
+ const activeCard = screen.getByText(chosenModel.name).closest('[role="listitem"]')
+ if (!(activeCard instanceof HTMLElement)) throw new Error('Active model card missing')
+ expect(within(activeCard).getByText('Active')).not.toBeNull()
+ })
+ })
+})
diff --git a/integration-tests/mcp-connector-setup.dbtest.ts b/integration-tests/mcp-connector-setup.dbtest.ts
new file mode 100644
index 00000000..f3e7a96e
--- /dev/null
+++ b/integration-tests/mcp-connector-setup.dbtest.ts
@@ -0,0 +1,113 @@
+// @vitest-environment jsdom
+/**
+ * RELEASE_TEST_CHECKLIST #71 - connector setup through the real product seam.
+ *
+ * The real Integrations screen drives production connector persistence, production MCP discovery,
+ * and a real stdio MCP child process. Electron IPC is the native boundary, represented only by a
+ * direct bridge to the same functions its handlers invoke. Closing and reopening SQLite proves the
+ * connected row and discovered tools survive exactly once.
+ */
+import { cleanup, render, screen, waitFor, within } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+import fs from 'fs'
+import os from 'os'
+import path from 'path'
+import React from 'react'
+import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+
+const TMP_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'offgrid-mcp-setup-'))
+const MCP_SERVER = path.resolve(
+ process.cwd(),
+ 'src/main/__tests__/fixtures/mcp-reachable-server.mjs'
+)
+
+vi.mock('electron', () => ({
+ app: { getPath: () => TMP_DIR, isPackaged: false, getAppPath: () => process.cwd() },
+ safeStorage: {
+ isEncryptionAvailable: () => false,
+ encryptString: (value: string) => Buffer.from(value),
+ decryptString: (value: Buffer) => value.toString()
+ },
+ shell: { openExternal: async () => {} }
+}))
+
+beforeEach(async () => {
+ const { getDB } = await import('@offgrid/core/main/database')
+ const { listConnectors } = await import('@offgrid/core/main/mcp')
+ listConnectors()
+ getDB().exec('DELETE FROM connectors')
+})
+
+afterEach(() => cleanup())
+
+afterAll(async () => {
+ const { getDB } = await import('@offgrid/core/main/database')
+ try {
+ getDB().close()
+ } catch {
+ // The persistence assertion deliberately closes the first connection.
+ }
+ fs.rmSync(TMP_DIR, { recursive: true, force: true })
+})
+
+describe(' connector setup', () => {
+ it('persists one reachable connector and reports its discovered connected state (#71)', async () => {
+ const connectorRepository = await import('@offgrid/core/main/mcp')
+ Object.assign(window, {
+ api: {
+ mcpList: async () => connectorRepository.listConnectors(),
+ mcpAdd: connectorRepository.addConnector,
+ mcpTest: connectorRepository.testConnector,
+ mcpSetEnabled: connectorRepository.setConnectorEnabled,
+ mcpRemove: connectorRepository.removeConnector,
+ mcpItems: async () => [],
+ mcpIngest: async () => ({ ok: true, count: 0 })
+ }
+ })
+
+ const { ConnectorsScreen } = await import('@renderer/components/ConnectorsScreen')
+ const user = userEvent.setup()
+ render(React.createElement(ConnectorsScreen))
+
+ await screen.findByText('Nothing connected yet. Pick one above.')
+ await user.click(screen.getByRole('button', { name: /custom/i }))
+ await user.click(screen.getByRole('button', { name: 'stdio (local)' }))
+ await user.type(screen.getByPlaceholderText('Name'), 'Reachable synthetic MCP')
+ await user.type(screen.getByPlaceholderText('command (e.g. npx)'), process.execPath)
+ await user.type(screen.getByPlaceholderText('args'), MCP_SERVER)
+ await user.click(screen.getByRole('button', { name: 'Add' }))
+
+ const installedRow = await screen.findByRole('button', { name: /Reachable synthetic MCP/ })
+ expect(within(installedRow).getByText('not tested')).not.toBeNull()
+ await user.click(installedRow)
+ await user.click(await screen.findByRole('button', { name: 'Test' }))
+
+ await waitFor(() => {
+ expect(screen.getByText('connected')).not.toBeNull()
+ expect(screen.getByText('read_status')).not.toBeNull()
+ })
+ expect(screen.queryByText('not tested')).toBeNull()
+
+ const { getDB } = await import('@offgrid/core/main/database')
+ getDB().close()
+ vi.resetModules()
+
+ const reopenedDatabase = await import('@offgrid/core/main/database')
+ const reopenedRepository = await import('@offgrid/core/main/mcp')
+ const persisted = reopenedRepository.listConnectors()
+ expect(persisted).toHaveLength(1)
+ expect(persisted[0]).toMatchObject({
+ name: 'Reachable synthetic MCP',
+ status: 'ok',
+ tools: JSON.stringify([
+ { name: 'read_status', description: 'Returns the synthetic connector status.' }
+ ])
+ })
+ const count = reopenedDatabase
+ .getDB()
+ .prepare('SELECT COUNT(*) AS count FROM connectors WHERE name = ?')
+ .get('Reachable synthetic MCP') as { count: number }
+ expect(count.count).toBe(1)
+ reopenedDatabase.getDB().close()
+ })
+})
diff --git a/integration-tests/permission-recovery.test.ts b/integration-tests/permission-recovery.test.ts
new file mode 100644
index 00000000..7f9ac23d
--- /dev/null
+++ b/integration-tests/permission-recovery.test.ts
@@ -0,0 +1,197 @@
+// @vitest-environment jsdom
+
+/**
+ * RELEASE_TEST_CHECKLIST #16 - denied capture permissions recover through the real
+ * permission owner and rendered setup surface. Only macOS TCC, Electron transport,
+ * and opening System Settings are controlled boundaries.
+ */
+import React from 'react'
+import { cleanup, render, screen, waitFor, within } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+
+const boundary = vi.hoisted(() => ({
+ accessibility: false,
+ screenRecording: 'denied' as 'denied' | 'granted',
+ accessibilityChecks: [] as boolean[],
+ screenRequests: 0,
+ openedSettings: [] as string[]
+}))
+
+vi.mock('electron', () => ({
+ systemPreferences: {
+ isTrustedAccessibilityClient: (prompt: boolean) => {
+ boundary.accessibilityChecks.push(prompt)
+ return boundary.accessibility
+ },
+ getMediaAccessStatus: () => boundary.screenRecording
+ },
+ shell: {
+ openExternal: (url: string) => {
+ boundary.openedSettings.push(url)
+ return Promise.resolve()
+ }
+ },
+ desktopCapturer: {
+ getSources: async () => {
+ boundary.screenRequests++
+ return []
+ }
+ }
+}))
+
+const realPlatform = process.platform
+Object.defineProperty(process, 'platform', { configurable: true, value: 'darwin' })
+
+const permissions = await import('@offgrid/core/main/permissions')
+
+function installApi(): void {
+ const values: Record = {
+ isPro: true,
+ platform: 'darwin',
+ getPermissionStatus: async () => permissions.getPermissionStatus(),
+ openAccessibilitySettings: async () => {
+ permissions.openAccessibilitySettings()
+ return true
+ },
+ openScreenRecordingSettings: async () => {
+ permissions.openScreenRecordingSettings()
+ return true
+ },
+ openMicrophoneSettings: async () => {
+ permissions.openMicrophoneSettings()
+ return true
+ },
+ checkModelStatus: async () => ({ downloaded: true, modelsDir: '/synthetic/models' }),
+ getLlmSettings: async () => ({ performanceMode: 'balanced' }),
+ setupPlan: async () => ({
+ mode: 'balanced',
+ ramGb: 16,
+ items: [],
+ totalDownloadGb: 0
+ }),
+ onSetupProgress: () => () => undefined
+ }
+ const api = new Proxy(values, {
+ get(target, property: string) {
+ if (property in target) return target[property]
+ return async () => undefined
+ }
+ })
+ Object.assign(window, { api })
+}
+
+installApi()
+const { PermissionGate } = await import('@renderer/components/PermissionGate')
+
+function permissionCard(title: string): HTMLElement {
+ const card = screen.getByRole('heading', { name: title }).closest('div.relative')
+ if (!(card instanceof HTMLElement)) throw new Error(`${title} permission card was not rendered`)
+ return card
+}
+
+beforeEach(() => {
+ boundary.accessibility = false
+ boundary.screenRecording = 'denied'
+ boundary.accessibilityChecks.length = 0
+ boundary.screenRequests = 0
+ boundary.openedSettings.length = 0
+ installApi()
+})
+
+afterEach(() => cleanup())
+
+afterAll(() => {
+ Object.defineProperty(process, 'platform', { configurable: true, value: realPlatform })
+})
+
+describe('capture permission recovery', () => {
+ it('routes a denied microphone feature to the exact macOS Settings pane (#16)', async () => {
+ await window.api.openMicrophoneSettings()
+
+ expect(boundary.openedSettings).toEqual([
+ 'x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone'
+ ])
+ })
+
+ it('requests TCC explicitly once while repeated health checks stay non-prompting (#15)', async () => {
+ expect(permissions.requestAccessibilityPermission()).toBe(false)
+ await expect(permissions.requestScreenRecordingPermission()).resolves.toBe(false)
+ expect(boundary.accessibilityChecks).toEqual([true])
+ expect(boundary.screenRequests).toBe(1)
+
+ expect(permissions.getPermissionStatus().allGranted).toBe(false)
+ expect(permissions.getPermissionStatus().allGranted).toBe(false)
+ expect(boundary.accessibilityChecks).toEqual([true, false, false])
+
+ boundary.accessibility = true
+ boundary.screenRecording = 'granted'
+ expect(permissions.getPermissionStatus()).toEqual({
+ accessibility: true,
+ screenRecording: true,
+ allGranted: true
+ })
+ expect(boundary.accessibilityChecks.at(-1)).toBe(false)
+ expect(boundary.screenRequests).toBe(1)
+ })
+
+ it('stays honest after a partial grant and becomes ready after rechecking both permissions (#16)', async () => {
+ const user = userEvent.setup()
+ render(
+ React.createElement(
+ PermissionGate,
+ null,
+ React.createElement('main', null, 'Application workspace')
+ )
+ )
+
+ expect(await screen.findByText('Application workspace')).not.toBeNull()
+ expect(await screen.findByText('Finish setting up capture')).not.toBeNull()
+
+ await user.click(screen.getByRole('button', { name: 'Set up' }))
+ const accessibilityCard = permissionCard('Accessibility')
+ const screenRecordingCard = permissionCard('Screen Recording')
+ expect(within(accessibilityCard).getByRole('button', { name: 'Open Settings' })).not.toBeNull()
+ expect(
+ within(screenRecordingCard).getByRole('button', { name: 'Open Settings' })
+ ).not.toBeNull()
+
+ await user.click(within(screenRecordingCard).getByRole('button', { name: 'Open Settings' }))
+ expect(boundary.openedSettings).toEqual([
+ 'x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture'
+ ])
+
+ boundary.screenRecording = 'granted'
+ await user.click(screen.getByRole('button', { name: 'Check permissions again' }))
+ await waitFor(() =>
+ expect(within(permissionCard('Screen Recording')).getByText('Enabled')).not.toBeNull()
+ )
+ expect(
+ within(permissionCard('Accessibility')).getByRole('button', { name: 'Open Settings' })
+ ).not.toBeNull()
+ expect(permissions.getPermissionStatus()).toEqual({
+ accessibility: false,
+ screenRecording: true,
+ allGranted: false
+ })
+
+ await user.click(
+ within(permissionCard('Accessibility')).getByRole('button', { name: 'Open Settings' })
+ )
+ expect(boundary.openedSettings.at(-1)).toBe(
+ 'x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility'
+ )
+
+ boundary.accessibility = true
+ await user.click(screen.getByRole('button', { name: 'Check permissions again' }))
+
+ await waitFor(() => expect(screen.queryByText('Capture permissions')).toBeNull())
+ expect(screen.getByText('Application workspace')).not.toBeNull()
+ expect(screen.queryByText('Finish setting up capture')).toBeNull()
+ expect(permissions.getPermissionStatus()).toEqual({
+ accessibility: true,
+ screenRecording: true,
+ allGranted: true
+ })
+ })
+})
diff --git a/knip.json b/knip.json
new file mode 100644
index 00000000..e10bff63
--- /dev/null
+++ b/knip.json
@@ -0,0 +1,34 @@
+{
+ "$schema": "https://unpkg.com/knip@5/schema.json",
+ "entry": [
+ "src/main/index.ts",
+ "src/preload/index.ts",
+ "src/renderer/src/main.tsx",
+ "pro/main/index.ts",
+ "pro/renderer/index.tsx",
+ "pro/renderer/screens.ts",
+ "e2e/**/*.spec.ts",
+ "scripts/**/*.{ts,mjs,cjs,js}",
+ "src/**/*.{test,dbtest}.{ts,tsx}",
+ "pro/**/*.{test,dbtest}.{ts,tsx}",
+ "src/renderer/src/components/ui/**/*.{ts,tsx}"
+ ],
+ "project": ["src/**/*.{ts,tsx}", "pro/**/*.{ts,tsx}"],
+ "ignore": ["**/*.d.ts"],
+ "tags": ["-public"],
+ "ignoreDependencies": [
+ "tailwindcss",
+ "@tailwindcss/vite",
+ "postcss",
+ "autoprefixer",
+ "@vitejs/plugin-react",
+ "@electron-toolkit/preload",
+ "@offgrid/design",
+ "better-sqlite3",
+ "@types/better-sqlite3",
+ "pdf-parse",
+ "kokoro-js",
+ "ollama"
+ ],
+ "ignoreBinaries": ["swift", "xattr"]
+}
diff --git a/package-lock.json b/package-lock.json
index 12cd4cd4..24c0428f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,35 +12,18 @@
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
- "@dnd-kit/utilities": "^3.2.2",
"@electron-toolkit/preload": "^3.0.2",
"@electron-toolkit/utils": "^4.0.0",
"@lancedb/lancedb": "^0.30.0",
- "@langchain/core": "^1.2.1",
- "@langchain/langgraph": "^1.4.5",
- "@langchain/openai": "^1.5.3",
"@modelcontextprotocol/sdk": "^1.29.0",
"@offgrid/clipboard": "file:./packages/clipboard",
"@offgrid/design": "file:./packages/design",
"@offgrid/models": "file:./packages/models",
"@offgrid/rag": "file:./packages/rag",
"@phosphor-icons/react": "^2.1.10",
- "@radix-ui/react-collapsible": "^1.1.14",
- "@radix-ui/react-dialog": "^1.1.17",
- "@radix-ui/react-dropdown-menu": "^2.1.18",
- "@radix-ui/react-scroll-area": "^1.2.12",
- "@radix-ui/react-select": "^2.3.1",
- "@radix-ui/react-separator": "^1.1.10",
- "@radix-ui/react-slot": "^1.3.0",
- "@radix-ui/react-tabs": "^1.1.13",
- "@radix-ui/react-tooltip": "^1.2.10",
- "@react-three/fiber": "^10.0.0-alpha.1",
"@scure/bip39": "^2.2.0",
"@tabler/icons-react": "^3.36.1",
"@tailwindcss/vite": "^4.1.18",
- "@tsparticles/engine": "^3.9.1",
- "@tsparticles/react": "^3.0.0",
- "@tsparticles/slim": "^3.9.1",
"@xenova/transformers": "^2.17.2",
"apache-arrow": "^18.1.0",
"async-mutex": "^0.5.0",
@@ -50,20 +33,16 @@
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"electron-updater": "^6.8.9",
- "framer-motion": "^12.27.1",
"get-windows": "^9.3.0",
"hash-wasm": "^4.12.0",
"jszip": "^3.10.1",
"kdbxweb": "^2.1.1",
"kokoro-js": "^1.2.1",
"mammoth": "^1.8.0",
- "mini-svg-data-uri": "^1.4.4",
"motion": "^12.27.1",
- "node-llama-cpp": "^3.15.0",
"node-machine-id": "^1.1.12",
"ollama": "^0.6.3",
"pdf-parse": "^1.1.1",
- "posthog-js": "^1.331.0",
"radix-ui": "^1.6.0",
"react-force-graph-3d": "^1.29.0",
"react-markdown": "^10.1.0",
@@ -79,6 +58,8 @@
"@electron-toolkit/eslint-config-ts": "^3.1.0",
"@electron-toolkit/tsconfig": "^2.0.0",
"@playwright/test": "^1.61.1",
+ "@testing-library/react": "^16.3.2",
+ "@testing-library/user-event": "^14.6.1",
"@types/better-sqlite3": "^7.6.13",
"@types/node": "^22.19.1",
"@types/react": "^19.2.7",
@@ -87,20 +68,24 @@
"@vitejs/plugin-react": "^5.1.1",
"@vitest/coverage-v8": "^4.1.10",
"autoprefixer": "^10.4.23",
+ "dependency-cruiser": "^18.0.0",
"electron": "^39.2.6",
- "electron-builder": "^26.0.12",
+ "electron-builder": "26.15.3",
"electron-vite": "^5.0.0",
"eslint": "^9.39.1",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
+ "eslint-plugin-sonarjs": "^4.1.0",
+ "jsdom": "^29.1.1",
+ "knip": "^6.25.0",
"postcss": "^8.5.6",
"prettier": "^3.7.4",
"react": "^19.2.1",
"react-dom": "^19.2.1",
- "simple-icons": "^16.6.0",
"tailwindcss": "^4.1.18",
"typescript": "^5.9.3",
+ "typescript-eslint": "^8.63.0",
"vite": "^7.2.6",
"vitest": "^4.0.17"
}
@@ -123,6 +108,57 @@
"extraneous": true,
"license": "AGPL-3.0-only"
},
+ "node_modules/@asamuzakjp/css-color": {
+ "version": "5.1.11",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz",
+ "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@asamuzakjp/generational-cache": "^1.0.1",
+ "@csstools/css-calc": "^3.2.0",
+ "@csstools/css-color-parser": "^4.1.0",
+ "@csstools/css-parser-algorithms": "^4.0.0",
+ "@csstools/css-tokenizer": "^4.0.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@asamuzakjp/dom-selector": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz",
+ "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@asamuzakjp/generational-cache": "^1.0.1",
+ "@asamuzakjp/nwsapi": "^2.3.9",
+ "bidi-js": "^1.0.3",
+ "css-tree": "^3.2.1",
+ "is-potential-custom-element-name": "^1.0.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@asamuzakjp/generational-cache": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz",
+ "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@asamuzakjp/nwsapi": {
+ "version": "2.3.9",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz",
+ "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@babel/code-frame": {
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz",
@@ -440,28 +476,157 @@
"node": ">=18"
}
},
- "node_modules/@cfworker/json-schema": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/@cfworker/json-schema/-/json-schema-4.1.1.tgz",
- "integrity": "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==",
- "license": "MIT"
+ "node_modules/@bramus/specificity": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz",
+ "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "css-tree": "^3.0.0"
+ },
+ "bin": {
+ "specificity": "bin/cli.js"
+ }
+ },
+ "node_modules/@csstools/color-helpers": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.0.tgz",
+ "integrity": "sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT-0",
+ "engines": {
+ "node": ">=20.19.0"
+ }
+ },
+ "node_modules/@csstools/css-calc": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz",
+ "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^4.0.0",
+ "@csstools/css-tokenizer": "^4.0.0"
+ }
},
- "node_modules/@develar/schema-utils": {
- "version": "2.6.5",
- "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz",
- "integrity": "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==",
+ "node_modules/@csstools/css-color-parser": {
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.9.tgz",
+ "integrity": "sha512-paQcIaOO53Rk5+YrBaBjm/SgrV4INImjo2BT1DtQRYr+XeTRbeAYlS+jxXp9drqvKmtFnWRJKIalDLhZZDu42A==",
"dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
"license": "MIT",
"dependencies": {
- "ajv": "^6.12.0",
- "ajv-keywords": "^3.4.1"
+ "@csstools/color-helpers": "^6.1.0",
+ "@csstools/css-calc": "^3.2.1"
},
"engines": {
- "node": ">= 8.9.0"
+ "node": ">=20.19.0"
},
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/webpack"
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^4.0.0",
+ "@csstools/css-tokenizer": "^4.0.0"
+ }
+ },
+ "node_modules/@csstools/css-parser-algorithms": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz",
+ "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "peerDependencies": {
+ "@csstools/css-tokenizer": "^4.0.0"
+ }
+ },
+ "node_modules/@csstools/css-syntax-patches-for-csstree": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.6.tgz",
+ "integrity": "sha512-TcJCWFbXLPpJYq6z7bfOyjWYJDiDg2/I4gyUC9pqPNqHFRIey0EB0q0L5cSnQDfWJg8Jd6VadakxdIez/3zkqQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT-0",
+ "peerDependencies": {
+ "css-tree": "^3.2.1"
+ },
+ "peerDependenciesMeta": {
+ "css-tree": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@csstools/css-tokenizer": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz",
+ "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.19.0"
}
},
"node_modules/@dimforge/rapier3d-compat": {
@@ -607,9 +772,9 @@
}
},
"node_modules/@electron/asar/node_modules/minimatch": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
- "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -651,9 +816,9 @@
}
},
"node_modules/@electron/fuses/node_modules/jsonfile": {
- "version": "6.2.0",
- "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
- "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz",
+ "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -726,9 +891,9 @@
}
},
"node_modules/@electron/notarize/node_modules/jsonfile": {
- "version": "6.2.0",
- "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
- "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz",
+ "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -799,9 +964,9 @@
}
},
"node_modules/@electron/osx-sign/node_modules/jsonfile": {
- "version": "6.2.0",
- "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
- "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz",
+ "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -822,26 +987,18 @@
}
},
"node_modules/@electron/rebuild": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-4.0.1.tgz",
- "integrity": "sha512-iMGXb6Ib7H/Q3v+BKZJoETgF9g6KMNZVbsO4b7Dmpgb5qTFqyFTzqW9F3TOSHdybv2vKYKzSS9OiZL+dcJb+1Q==",
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-4.2.0.tgz",
+ "integrity": "sha512-RKL/O+jGoXJMxrx/5771y1n0xTKmFuOYGO3gMmwypBM6rsH0kou0mswwdXA2JrhIkE4xyC7v9vGk0n6NPzgOxQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@malept/cross-spawn-promise": "^2.0.0",
- "chalk": "^4.0.0",
"debug": "^4.1.1",
- "detect-libc": "^2.0.1",
- "got": "^11.7.0",
- "graceful-fs": "^4.2.11",
"node-abi": "^4.2.0",
"node-api-version": "^0.2.1",
- "node-gyp": "^11.2.0",
- "ora": "^5.1.0",
- "read-binary-file-arch": "^1.0.6",
- "semver": "^7.3.5",
- "tar": "^6.0.5",
- "yargs": "^17.0.1"
+ "node-gyp": "^12.2.0",
+ "read-binary-file-arch": "^1.0.6"
},
"bin": {
"electron-rebuild": "lib/cli.js"
@@ -850,19 +1007,6 @@
"node": ">=22.12.0"
}
},
- "node_modules/@electron/rebuild/node_modules/semver": {
- "version": "7.7.3",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
- "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
- "dev": true,
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
- },
- "engines": {
- "node": ">=10"
- }
- },
"node_modules/@electron/universal": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.3.tgz",
@@ -883,9 +1027,9 @@
}
},
"node_modules/@electron/universal/node_modules/brace-expansion": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
- "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz",
+ "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -893,9 +1037,9 @@
}
},
"node_modules/@electron/universal/node_modules/fs-extra": {
- "version": "11.3.3",
- "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz",
- "integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==",
+ "version": "11.3.6",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.6.tgz",
+ "integrity": "sha512-w8ZNZr2mKIc7qeNaQ9AVPT1+iFaI+Avd4xudVOvdDJ8VytREi1Ft5Ih7hd9jjehod8vAM5GMsfQ/TpPf4EyoEA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -908,9 +1052,9 @@
}
},
"node_modules/@electron/universal/node_modules/jsonfile": {
- "version": "6.2.0",
- "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
- "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz",
+ "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -921,13 +1065,13 @@
}
},
"node_modules/@electron/universal/node_modules/minimatch": {
- "version": "9.0.5",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
- "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "version": "9.0.9",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
+ "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
"dev": true,
"license": "ISC",
"dependencies": {
- "brace-expansion": "^2.0.1"
+ "brace-expansion": "^2.0.2"
},
"engines": {
"node": ">=16 || 14 >=14.17"
@@ -969,9 +1113,9 @@
}
},
"node_modules/@electron/windows-sign/node_modules/fs-extra": {
- "version": "11.3.3",
- "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz",
- "integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==",
+ "version": "11.3.6",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.6.tgz",
+ "integrity": "sha512-w8ZNZr2mKIc7qeNaQ9AVPT1+iFaI+Avd4xudVOvdDJ8VytREi1Ft5Ih7hd9jjehod8vAM5GMsfQ/TpPf4EyoEA==",
"dev": true,
"license": "MIT",
"optional": true,
@@ -986,9 +1130,9 @@
}
},
"node_modules/@electron/windows-sign/node_modules/jsonfile": {
- "version": "6.2.0",
- "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
- "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz",
+ "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==",
"dev": true,
"license": "MIT",
"optional": true,
@@ -1012,6 +1156,17 @@
"node": ">= 10.0.0"
}
},
+ "node_modules/@emnapi/core": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz",
+ "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/wasi-threads": "1.2.2",
+ "tslib": "^2.4.0"
+ }
+ },
"node_modules/@emnapi/runtime": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz",
@@ -1022,6 +1177,16 @@
"tslib": "^2.4.0"
}
},
+ "node_modules/@emnapi/wasi-threads": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
+ "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
@@ -1647,6 +1812,24 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
+ "node_modules/@exodus/bytes": {
+ "version": "1.15.1",
+ "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz",
+ "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ },
+ "peerDependencies": {
+ "@noble/hashes": "^1.8.0 || ^2.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@noble/hashes": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@floating-ui/core": {
"version": "1.7.5",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz",
@@ -2999,35 +3182,12 @@
"url": "https://opencollective.com/libvips"
}
},
- "node_modules/@isaacs/balanced-match": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
- "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "20 || >=22"
- }
- },
- "node_modules/@isaacs/brace-expansion": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz",
- "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@isaacs/balanced-match": "^4.0.1"
- },
- "engines": {
- "node": "20 || >=22"
- }
- },
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
- "devOptional": true,
"license": "ISC",
+ "optional": true,
"dependencies": {
"string-width": "^5.1.2",
"string-width-cjs": "npm:string-width@^4.2.0",
@@ -3044,8 +3204,8 @@
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
- "devOptional": true,
"license": "MIT",
+ "optional": true,
"engines": {
"node": ">=12"
},
@@ -3057,8 +3217,8 @@
"version": "6.2.3",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
- "devOptional": true,
"license": "MIT",
+ "optional": true,
"engines": {
"node": ">=12"
},
@@ -3070,15 +3230,15 @@
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
- "devOptional": true,
- "license": "MIT"
+ "license": "MIT",
+ "optional": true
},
"node_modules/@isaacs/cliui/node_modules/string-width": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
- "devOptional": true,
"license": "MIT",
+ "optional": true,
"dependencies": {
"eastasianwidth": "^0.2.0",
"emoji-regex": "^9.2.2",
@@ -3095,8 +3255,8 @@
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
"integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
- "devOptional": true,
"license": "MIT",
+ "optional": true,
"dependencies": {
"ansi-regex": "^6.0.1"
},
@@ -3111,8 +3271,8 @@
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
- "devOptional": true,
"license": "MIT",
+ "optional": true,
"dependencies": {
"ansi-styles": "^6.1.0",
"string-width": "^5.0.1",
@@ -3182,21 +3342,6 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
- "node_modules/@kwsites/file-exists": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz",
- "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==",
- "license": "MIT",
- "dependencies": {
- "debug": "^4.1.1"
- }
- },
- "node_modules/@kwsites/promise-deferred": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz",
- "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==",
- "license": "MIT"
- },
"node_modules/@lancedb/lancedb": {
"version": "0.30.0",
"resolved": "https://registry.npmjs.org/@lancedb/lancedb/-/lancedb-0.30.0.tgz",
@@ -3354,145 +3499,6 @@
"node": ">= 18"
}
},
- "node_modules/@langchain/core": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/@langchain/core/-/core-1.2.1.tgz",
- "integrity": "sha512-NNG/cC5FGuHDOAP56h0ddp8Rfk8p+othWzEK5RV9JIG6RvnF5vGa5r0AEGtKfQieed7s1kC42GuIzVOBvMBL/g==",
- "license": "MIT",
- "dependencies": {
- "@cfworker/json-schema": "^4.0.2",
- "@standard-schema/spec": "^1.1.0",
- "js-tiktoken": "^1.0.12",
- "langsmith": ">=0.5.0 <1.0.0",
- "mustache": "^4.2.0",
- "p-queue": "^6.6.2",
- "zod": "^3.25.76 || ^4"
- },
- "engines": {
- "node": ">=20"
- }
- },
- "node_modules/@langchain/langgraph": {
- "version": "1.4.5",
- "resolved": "https://registry.npmjs.org/@langchain/langgraph/-/langgraph-1.4.5.tgz",
- "integrity": "sha512-V+o29JPBaMoK/e+8R/m81XaC8h5iNuwWymvgLFhXfJbf7E2xt2mQUkcVXTi4cudGRHbRd14kidCpfaQbfPoYCw==",
- "license": "MIT",
- "dependencies": {
- "@langchain/langgraph-checkpoint": "^1.1.2",
- "@langchain/langgraph-sdk": "~1.9.24",
- "@langchain/protocol": "^0.0.18",
- "@standard-schema/spec": "1.1.0"
- },
- "engines": {
- "node": ">=18"
- },
- "peerDependencies": {
- "@langchain/core": "^1.1.48",
- "zod": "^3.25.32 || ^4.2.0",
- "zod-to-json-schema": "^3.x"
- },
- "peerDependenciesMeta": {
- "zod-to-json-schema": {
- "optional": true
- }
- }
- },
- "node_modules/@langchain/langgraph-checkpoint": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/@langchain/langgraph-checkpoint/-/langgraph-checkpoint-1.1.2.tgz",
- "integrity": "sha512-m5Xd7W3G9JrlEhFZ5WAcqZPgE46R9gr1gFDFaVqEKeuwin3tgEp0jlPbru+iFXCug338DcQjFS/Kuuci21ydvw==",
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "peerDependencies": {
- "@langchain/core": "^1.1.48"
- }
- },
- "node_modules/@langchain/langgraph-sdk": {
- "version": "1.9.24",
- "resolved": "https://registry.npmjs.org/@langchain/langgraph-sdk/-/langgraph-sdk-1.9.24.tgz",
- "integrity": "sha512-WhM6QdxNipndQjl5nkvqnBt9Wl16oO2p0KiVhndAFLJMwO3bZLEx++lwtbqUFQu1sHyNxiWixgRGm8qZsuHCeA==",
- "license": "MIT",
- "dependencies": {
- "@langchain/protocol": "^0.0.18",
- "@types/json-schema": "^7.0.15",
- "p-queue": "^9.0.1",
- "p-retry": "^7.1.1"
- },
- "peerDependencies": {
- "@langchain/core": "^1.1.48",
- "react": "^18 || ^19",
- "react-dom": "^18 || ^19",
- "svelte": "^4.0.0 || ^5.0.0",
- "vue": "^3.0.0"
- },
- "peerDependenciesMeta": {
- "react": {
- "optional": true
- },
- "react-dom": {
- "optional": true
- },
- "svelte": {
- "optional": true
- },
- "vue": {
- "optional": true
- }
- }
- },
- "node_modules/@langchain/langgraph-sdk/node_modules/p-queue": {
- "version": "9.3.0",
- "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.3.0.tgz",
- "integrity": "sha512-7NED7xhQ74Ngp4JP/2e0VZHp7vSWfJfqeiR92jPgxsz6m0Se4P03YoTKa9dDXyZ3r6P616gUXttrB6nnHYKang==",
- "license": "MIT",
- "dependencies": {
- "eventemitter3": "^5.0.4",
- "p-timeout": "^7.0.0"
- },
- "engines": {
- "node": ">=20"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/@langchain/langgraph-sdk/node_modules/p-timeout": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-7.0.1.tgz",
- "integrity": "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==",
- "license": "MIT",
- "engines": {
- "node": ">=20"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/@langchain/openai": {
- "version": "1.5.3",
- "resolved": "https://registry.npmjs.org/@langchain/openai/-/openai-1.5.3.tgz",
- "integrity": "sha512-OStS2AUvy9oe/hEf/3ndBOFztUDOfuJYLNXh89m3iiJAI2Cp5Dp0n/pvpO27MO0b+VgENd+xSHVyQZ7fe+ulxg==",
- "license": "MIT",
- "dependencies": {
- "js-tiktoken": "^1.0.12",
- "openai": "^6.41.0",
- "zod": "^3.25.76 || ^4"
- },
- "engines": {
- "node": ">=20"
- },
- "peerDependencies": {
- "@langchain/core": "^1.2.1"
- }
- },
- "node_modules/@langchain/protocol": {
- "version": "0.0.18",
- "resolved": "https://registry.npmjs.org/@langchain/protocol/-/protocol-0.0.18.tgz",
- "integrity": "sha512-XW1egQtPfsGI41w2AMZNFZrUIwFSQHTjVMZs0OaTpCAvht/QLoaPN8FQcsysMVypOhupG28J29yOorrc70otBQ==",
- "license": "MIT"
- },
"node_modules/@malept/cross-spawn-promise": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz",
@@ -3549,9 +3555,9 @@
}
},
"node_modules/@malept/flatpak-bundler/node_modules/jsonfile": {
- "version": "6.2.0",
- "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
- "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz",
+ "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3785,6 +3791,24 @@
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"license": "MIT"
},
+ "node_modules/@napi-rs/wasm-runtime": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz",
+ "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@tybys/wasm-util": "^0.10.3"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ },
+ "peerDependencies": {
+ "@emnapi/core": "^1.7.1",
+ "@emnapi/runtime": "^1.7.1"
+ }
+ },
"node_modules/@noble/hashes": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz",
@@ -3797,899 +3821,766 @@
"url": "https://paulmillr.com/funding/"
}
},
- "node_modules/@node-llama-cpp/linux-arm64": {
- "version": "3.15.0",
- "resolved": "https://registry.npmjs.org/@node-llama-cpp/linux-arm64/-/linux-arm64-3.15.0.tgz",
- "integrity": "sha512-IaHIllWlj6tGjhhCtyp1w6xA7AHaGJiVaXAZ+78hDs8X1SL9ySBN2Qceju8AQJALePtynbAfjgjTqjQ7Hyk+IQ==",
+ "node_modules/@offgrid/clipboard": {
+ "resolved": "packages/clipboard",
+ "link": true
+ },
+ "node_modules/@offgrid/design": {
+ "resolved": "packages/design",
+ "link": true
+ },
+ "node_modules/@offgrid/models": {
+ "resolved": "packages/models",
+ "link": true
+ },
+ "node_modules/@offgrid/rag": {
+ "resolved": "packages/rag",
+ "link": true
+ },
+ "node_modules/@oxc-parser/binding-android-arm-eabi": {
+ "version": "0.137.0",
+ "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm-eabi/-/binding-android-arm-eabi-0.137.0.tgz",
+ "integrity": "sha512-KDs+0VPdEmasOkpuJHW9V5WCF+cvYdMQv2Jd+aJXt+cxIx12NToRQRbXaRwUEDsZw+/jMk81Ve8ZFbjUkJTOwA==",
"cpu": [
- "arm64",
- "x64"
+ "arm"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
- "linux"
+ "android"
],
"engines": {
- "node": ">=20.0.0"
+ "node": "^20.19.0 || >=22.12.0"
}
},
- "node_modules/@node-llama-cpp/linux-armv7l": {
- "version": "3.15.0",
- "resolved": "https://registry.npmjs.org/@node-llama-cpp/linux-armv7l/-/linux-armv7l-3.15.0.tgz",
- "integrity": "sha512-ZuZ3q6mejQnEP4o22la7zBv7jNR+IZfgItDm3KjAl04HUXTKJ43HpNwjnf9GyYYd+dEgtoX0MESvWz4RnGH8Jw==",
+ "node_modules/@oxc-parser/binding-android-arm64": {
+ "version": "0.137.0",
+ "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.137.0.tgz",
+ "integrity": "sha512-WhALNzfy3x/RfC6bsqX+csavuUY0yHHE7XfgPE5M542uhoBZUUoGTPG+nkMbGoG4+gcfss5s7urMyn5QBHu0sw==",
"cpu": [
- "arm",
- "x64"
+ "arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
- "linux"
+ "android"
],
"engines": {
- "node": ">=20.0.0"
+ "node": "^20.19.0 || >=22.12.0"
}
},
- "node_modules/@node-llama-cpp/linux-x64": {
- "version": "3.15.0",
- "resolved": "https://registry.npmjs.org/@node-llama-cpp/linux-x64/-/linux-x64-3.15.0.tgz",
- "integrity": "sha512-etUuTqSyNefRObqc5+JZviNTkuef2XEtHcQLaamEIWwjI1dj7nTD2YMZPBP7H3M3E55HSIY82vqCQ1bp6ZILiA==",
+ "node_modules/@oxc-parser/binding-darwin-arm64": {
+ "version": "0.137.0",
+ "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.137.0.tgz",
+ "integrity": "sha512-bFPr5hgmNMOMoyPTGtdsK4Ug21RovIPojRMgDDhSp1LtCnc/DkLwGONKjgRjszg677RlGnkYSviQ8hHaUPOVYA==",
"cpu": [
- "x64"
+ "arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
- "linux"
+ "darwin"
],
"engines": {
- "node": ">=20.0.0"
+ "node": "^20.19.0 || >=22.12.0"
}
},
- "node_modules/@node-llama-cpp/linux-x64-cuda": {
- "version": "3.15.0",
- "resolved": "https://registry.npmjs.org/@node-llama-cpp/linux-x64-cuda/-/linux-x64-cuda-3.15.0.tgz",
- "integrity": "sha512-mDjyVulCTRYilm9Emm3lDMx7dbI1vzGqk28Pj28shartjERTUu8aUNDYOmVKNMLpUKS1akw7vy0lMF8t4qswxQ==",
+ "node_modules/@oxc-parser/binding-darwin-x64": {
+ "version": "0.137.0",
+ "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.137.0.tgz",
+ "integrity": "sha512-CL5dMm1asqXIDZHg14FLxj3Mc36w8PI7xCWh1uA4is6z8g2XrIILoTcQYOxDbwzuk34RDPX5IAGUxZr6LA9KAg==",
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
- "linux"
+ "darwin"
],
"engines": {
- "node": ">=20.0.0"
+ "node": "^20.19.0 || >=22.12.0"
}
},
- "node_modules/@node-llama-cpp/linux-x64-cuda-ext": {
- "version": "3.15.0",
- "resolved": "https://registry.npmjs.org/@node-llama-cpp/linux-x64-cuda-ext/-/linux-x64-cuda-ext-3.15.0.tgz",
- "integrity": "sha512-wQwgSl7Qm8vH56oBt7IuWWDNNsDECkVMS000C92wl3PkbzjwZFiWzehwa+kF8Lr2BBMiCJNkI5nEabhYH3RN2Q==",
+ "node_modules/@oxc-parser/binding-freebsd-x64": {
+ "version": "0.137.0",
+ "resolved": "https://registry.npmjs.org/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.137.0.tgz",
+ "integrity": "sha512-79h8rYGnSlKPGWo7mHr2ixO6ea7aW8B0CT965SZ8SLbNnCOH5aOYBTeVXUY6eMvEaiLyWr8Skuiugr5pDYgLGw==",
"cpu": [
"x64"
],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxc-parser/binding-linux-arm-gnueabihf": {
+ "version": "0.137.0",
+ "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.137.0.tgz",
+ "integrity": "sha512-ASgmlSimhGyr0lksgVIo6hibz1obnDq4qJbiMX/AzltfgPnanRrzG1Q+23g8ljOHOjv6dsznkUuCYL3gg0sY1Q==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
- "node": ">=20.0.0"
+ "node": "^20.19.0 || >=22.12.0"
}
},
- "node_modules/@node-llama-cpp/linux-x64-vulkan": {
- "version": "3.15.0",
- "resolved": "https://registry.npmjs.org/@node-llama-cpp/linux-x64-vulkan/-/linux-x64-vulkan-3.15.0.tgz",
- "integrity": "sha512-htVIthQKq/rr8v5e7NiVtcHsstqTBAAC50kUymmDMbrzAu6d/EHacCJpNbU57b1UUa1nKN5cBqr6Jr+QqEalMA==",
+ "node_modules/@oxc-parser/binding-linux-arm-musleabihf": {
+ "version": "0.137.0",
+ "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.137.0.tgz",
+ "integrity": "sha512-AU2J9aa22Sx32wRGnDjybOU9TQXXQUud5sdUi+ZB0XxwM8aToWLweV+yA0wlQm0yIUVqljquqoHCYEq9II8gJQ==",
"cpu": [
- "x64"
+ "arm"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
- "node": ">=20.0.0"
+ "node": "^20.19.0 || >=22.12.0"
}
},
- "node_modules/@node-llama-cpp/mac-arm64-metal": {
- "version": "3.15.0",
- "resolved": "https://registry.npmjs.org/@node-llama-cpp/mac-arm64-metal/-/mac-arm64-metal-3.15.0.tgz",
- "integrity": "sha512-3Vkq6bpyQZaIzoaLLP7H2Tt8ty5BS0zxUY2pX0ox2S9P4fp8Au0CCJuUJF4V+EKi+/PTn70A6R1QCkRMfMQJig==",
+ "node_modules/@oxc-parser/binding-linux-arm64-gnu": {
+ "version": "0.137.0",
+ "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.137.0.tgz",
+ "integrity": "sha512-GdEtiG89yMr7XkUGxifgodXEEm2f+xW2f9CpDjlgAnBOwhTmrpQMvhOGobLVKUyzf/qHBXW16smk5zbF3nZU6w==",
"cpu": [
- "arm64",
- "x64"
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
],
"license": "MIT",
"optional": true,
"os": [
- "darwin"
+ "linux"
],
"engines": {
- "node": ">=20.0.0"
+ "node": "^20.19.0 || >=22.12.0"
}
},
- "node_modules/@node-llama-cpp/mac-x64": {
- "version": "3.15.0",
- "resolved": "https://registry.npmjs.org/@node-llama-cpp/mac-x64/-/mac-x64-3.15.0.tgz",
- "integrity": "sha512-BUrmLu0ySveEYv2YzFIjqnWWAqjTZfRHuzoFLaZwqIJ86Jzycm9tzxJub4wfJCj6ixeuWyI1sUdNGIw4/2E03Q==",
+ "node_modules/@oxc-parser/binding-linux-arm64-musl": {
+ "version": "0.137.0",
+ "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.137.0.tgz",
+ "integrity": "sha512-EGJ+Bs8iXx8KBH8DQ5BLoEm5lnHaYjlh4/8j8vFhrr/6z4tqONy5BZDzLpKmmNWlN6Hlc5r8YOuBVHqZ9vRFEQ==",
"cpu": [
- "x64"
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
],
"license": "MIT",
"optional": true,
"os": [
- "darwin"
+ "linux"
],
"engines": {
- "node": ">=20.0.0"
+ "node": "^20.19.0 || >=22.12.0"
}
},
- "node_modules/@node-llama-cpp/win-arm64": {
- "version": "3.15.0",
- "resolved": "https://registry.npmjs.org/@node-llama-cpp/win-arm64/-/win-arm64-3.15.0.tgz",
- "integrity": "sha512-GwhqaPNpbtGDmw0Ex13hwq4jqzSZr7hw5QpRWhSKB1dHiYj6C1NLM1Vz5xiDZX+69WI/ndb+FEqGiIYFQpfmiQ==",
+ "node_modules/@oxc-parser/binding-linux-ppc64-gnu": {
+ "version": "0.137.0",
+ "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.137.0.tgz",
+ "integrity": "sha512-vzFUQENy/fnbSe5DZWovq6tIBc1uhuMztanSW6rz1e9WdQE4gHwYuD7ZII6JnrJifd1R3RSoqiZbgRFlVL2tYQ==",
"cpu": [
- "arm64",
- "x64"
+ "ppc64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
],
"license": "MIT",
"optional": true,
"os": [
- "win32"
+ "linux"
],
"engines": {
- "node": ">=20.0.0"
+ "node": "^20.19.0 || >=22.12.0"
}
},
- "node_modules/@node-llama-cpp/win-x64": {
- "version": "3.15.0",
- "resolved": "https://registry.npmjs.org/@node-llama-cpp/win-x64/-/win-x64-3.15.0.tgz",
- "integrity": "sha512-gWhtc8l3HOry5guO46YfFohLQnF0NfL4On0GAO8E27JiYYxHO9nHSCfFif4+U03+FfHquZXL0znJ1qPVOiwOPw==",
+ "node_modules/@oxc-parser/binding-linux-riscv64-gnu": {
+ "version": "0.137.0",
+ "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.137.0.tgz",
+ "integrity": "sha512-SfVI14HBQs9gtLcUD5hTt5hsNbdrqSUNg9S8muN+LhVQ5nf1WwH3hAoK6B9NKgdYgWAQSXFXGiiBedQ4r/BKuw==",
"cpu": [
- "x64"
+ "riscv64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
],
"license": "MIT",
"optional": true,
"os": [
- "win32"
+ "linux"
],
"engines": {
- "node": ">=20.0.0"
+ "node": "^20.19.0 || >=22.12.0"
}
},
- "node_modules/@node-llama-cpp/win-x64-cuda": {
- "version": "3.15.0",
- "resolved": "https://registry.npmjs.org/@node-llama-cpp/win-x64-cuda/-/win-x64-cuda-3.15.0.tgz",
- "integrity": "sha512-2Kyu1roDwXwFLaJgGZQISIXP9lCDZtJCx/DRcmrYRHcSUFCzo5ikOuAECyliSSQmRUAvvlRCuD+GrTcegbhojA==",
+ "node_modules/@oxc-parser/binding-linux-riscv64-musl": {
+ "version": "0.137.0",
+ "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.137.0.tgz",
+ "integrity": "sha512-e7Ppy4FCIFNQxT/ikSeIWFoQ0l+N9vgtRBtLcyZXeolTzApyVoPqEXsYPrcdM/9i0Bwk8knvYd37vaEMxHyi6g==",
"cpu": [
- "x64"
+ "riscv64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
],
"license": "MIT",
"optional": true,
"os": [
- "win32"
+ "linux"
],
"engines": {
- "node": ">=20.0.0"
+ "node": "^20.19.0 || >=22.12.0"
}
},
- "node_modules/@node-llama-cpp/win-x64-cuda-ext": {
- "version": "3.15.0",
- "resolved": "https://registry.npmjs.org/@node-llama-cpp/win-x64-cuda-ext/-/win-x64-cuda-ext-3.15.0.tgz",
- "integrity": "sha512-KQoNH9KsVtqGVXaRdPrnHPrg5w3KOM7CfynPmG1m16gmjmDSIspdPg/Dbg6DgHBfkdAzt+duRZEBk8Bg8KbDHw==",
+ "node_modules/@oxc-parser/binding-linux-s390x-gnu": {
+ "version": "0.137.0",
+ "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.137.0.tgz",
+ "integrity": "sha512-Bho5qFwdhqsIFR7gipYEUlqvi3SRrY8sugxXig380MIaakBB1PyU9+7dBiBVScfImTNWhijUxdBwqrprGdq5WA==",
"cpu": [
- "x64"
+ "s390x"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
],
"license": "MIT",
"optional": true,
"os": [
- "win32"
+ "linux"
],
"engines": {
- "node": ">=20.0.0"
+ "node": "^20.19.0 || >=22.12.0"
}
},
- "node_modules/@node-llama-cpp/win-x64-vulkan": {
- "version": "3.15.0",
- "resolved": "https://registry.npmjs.org/@node-llama-cpp/win-x64-vulkan/-/win-x64-vulkan-3.15.0.tgz",
- "integrity": "sha512-sH+K7lO49WrUvCCC3RPddCBrn2ZQwKCXKL90P/NZicMRgxTPFZEVSU2jXR/bu1K8B+4lNN+z5OEbjSYs7cKEcA==",
+ "node_modules/@oxc-parser/binding-linux-x64-gnu": {
+ "version": "0.137.0",
+ "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.137.0.tgz",
+ "integrity": "sha512-36mGWtg7PyFzjJwGDkH6/F4o2nIDEoKXLPr/X/lwqklkomQwJJt1I5GJVmGhovUEmgPK5WAeAZMqlFCehwiy9Q==",
"cpu": [
"x64"
],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
"license": "MIT",
"optional": true,
"os": [
- "win32"
+ "linux"
],
"engines": {
- "node": ">=20.0.0"
+ "node": "^20.19.0 || >=22.12.0"
}
},
- "node_modules/@npmcli/agent": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-3.0.0.tgz",
- "integrity": "sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q==",
+ "node_modules/@oxc-parser/binding-linux-x64-musl": {
+ "version": "0.137.0",
+ "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.137.0.tgz",
+ "integrity": "sha512-/Jqx6+N7A44n2BdvUr7pXhVr2vFjs6WGH3unZRczwrfiH0H1zY0QwKQMG/dtRiTlKGDKGukznPT8lx84/oEsZg==",
+ "cpu": [
+ "x64"
+ ],
"dev": true,
- "license": "ISC",
- "dependencies": {
- "agent-base": "^7.1.0",
- "http-proxy-agent": "^7.0.0",
- "https-proxy-agent": "^7.0.1",
- "lru-cache": "^10.0.1",
- "socks-proxy-agent": "^8.0.3"
- },
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
"engines": {
- "node": "^18.17.0 || >=20.5.0"
+ "node": "^20.19.0 || >=22.12.0"
}
},
- "node_modules/@npmcli/agent/node_modules/lru-cache": {
- "version": "10.4.3",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
- "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "node_modules/@oxc-parser/binding-openharmony-arm64": {
+ "version": "0.137.0",
+ "resolved": "https://registry.npmjs.org/@oxc-parser/binding-openharmony-arm64/-/binding-openharmony-arm64-0.137.0.tgz",
+ "integrity": "sha512-9Uj0qHNNl+OgT1UTGwF7ixIXU6T1u2SbMidmgPy/h1h/fl2gRS6YpAxxY1gwHofcWjoTwkoMFd8xs5Vuj6GOFA==",
+ "cpu": [
+ "arm64"
+ ],
"dev": true,
- "license": "ISC"
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
},
- "node_modules/@npmcli/fs": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-4.0.0.tgz",
- "integrity": "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==",
+ "node_modules/@oxc-parser/binding-wasm32-wasi": {
+ "version": "0.137.0",
+ "resolved": "https://registry.npmjs.org/@oxc-parser/binding-wasm32-wasi/-/binding-wasm32-wasi-0.137.0.tgz",
+ "integrity": "sha512-gW2vfkytNGgMVADiuzdvOfw0mWG9za20F/1fCJsif5aBMAvWJTSbpIXbIe0XkOe0VENk+PadpQ7cZgUy2sUJcA==",
+ "cpu": [
+ "wasm32"
+ ],
"dev": true,
- "license": "ISC",
+ "license": "MIT",
+ "optional": true,
"dependencies": {
- "semver": "^7.3.5"
+ "@emnapi/core": "1.11.1",
+ "@emnapi/runtime": "1.11.1",
+ "@napi-rs/wasm-runtime": "^1.1.5"
},
"engines": {
- "node": "^18.17.0 || >=20.5.0"
+ "node": "^20.19.0 || >=22.12.0"
}
},
- "node_modules/@npmcli/fs/node_modules/semver": {
- "version": "7.7.3",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
- "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
+ "node_modules/@oxc-parser/binding-win32-arm64-msvc": {
+ "version": "0.137.0",
+ "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.137.0.tgz",
+ "integrity": "sha512-x+pFANF0yL5uK/6T7lu6SlR5qid6sp//eZXKLq5iNsIE+EQg6EaS8/wsW7E91nXXjpnPhSoMOHXShSVhGRdn8w==",
+ "cpu": [
+ "arm64"
+ ],
"dev": true,
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/@octokit/app": {
- "version": "16.1.2",
- "resolved": "https://registry.npmjs.org/@octokit/app/-/app-16.1.2.tgz",
- "integrity": "sha512-8j7sEpUYVj18dxvh0KWj6W/l6uAiVRBl1JBDVRqH1VHKAO/G5eRVl4yEoYACjakWers1DjUkcCHyJNQK47JqyQ==",
"license": "MIT",
- "dependencies": {
- "@octokit/auth-app": "^8.1.2",
- "@octokit/auth-unauthenticated": "^7.0.3",
- "@octokit/core": "^7.0.6",
- "@octokit/oauth-app": "^8.0.3",
- "@octokit/plugin-paginate-rest": "^14.0.0",
- "@octokit/types": "^16.0.0",
- "@octokit/webhooks": "^14.0.0"
- },
+ "optional": true,
+ "os": [
+ "win32"
+ ],
"engines": {
- "node": ">= 20"
+ "node": "^20.19.0 || >=22.12.0"
}
},
- "node_modules/@octokit/auth-app": {
- "version": "8.1.2",
- "resolved": "https://registry.npmjs.org/@octokit/auth-app/-/auth-app-8.1.2.tgz",
- "integrity": "sha512-db8VO0PqXxfzI6GdjtgEFHY9tzqUql5xMFXYA12juq8TeTgPAuiiP3zid4h50lwlIP457p5+56PnJOgd2GGBuw==",
+ "node_modules/@oxc-parser/binding-win32-ia32-msvc": {
+ "version": "0.137.0",
+ "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.137.0.tgz",
+ "integrity": "sha512-sQUqym80PFi6McRsIqfJrSu2JrSClEZIXXD+/FjAFoULEKzOPsldIdFBG96xdX8aVMzCNQ9792FPx3MfkEIrFA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
"license": "MIT",
- "dependencies": {
- "@octokit/auth-oauth-app": "^9.0.3",
- "@octokit/auth-oauth-user": "^6.0.2",
- "@octokit/request": "^10.0.6",
- "@octokit/request-error": "^7.0.2",
- "@octokit/types": "^16.0.0",
- "toad-cache": "^3.7.0",
- "universal-github-app-jwt": "^2.2.0",
- "universal-user-agent": "^7.0.0"
- },
+ "optional": true,
+ "os": [
+ "win32"
+ ],
"engines": {
- "node": ">= 20"
+ "node": "^20.19.0 || >=22.12.0"
}
},
- "node_modules/@octokit/auth-oauth-app": {
- "version": "9.0.3",
- "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-app/-/auth-oauth-app-9.0.3.tgz",
- "integrity": "sha512-+yoFQquaF8OxJSxTb7rnytBIC2ZLbLqA/yb71I4ZXT9+Slw4TziV9j/kyGhUFRRTF2+7WlnIWsePZCWHs+OGjg==",
+ "node_modules/@oxc-parser/binding-win32-x64-msvc": {
+ "version": "0.137.0",
+ "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.137.0.tgz",
+ "integrity": "sha512-2AsevxlvNN4WKxpEn3RtqD5zbqMaXF+T7JXblsP4gVuY+vC9dXS4ED/PwfRCliFqoeisYS3Iro4DHzxr0TEvVA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
"license": "MIT",
- "dependencies": {
- "@octokit/auth-oauth-device": "^8.0.3",
- "@octokit/auth-oauth-user": "^6.0.2",
- "@octokit/request": "^10.0.6",
- "@octokit/types": "^16.0.0",
- "universal-user-agent": "^7.0.0"
- },
+ "optional": true,
+ "os": [
+ "win32"
+ ],
"engines": {
- "node": ">= 20"
+ "node": "^20.19.0 || >=22.12.0"
}
},
- "node_modules/@octokit/auth-oauth-device": {
- "version": "8.0.3",
- "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-device/-/auth-oauth-device-8.0.3.tgz",
- "integrity": "sha512-zh2W0mKKMh/VWZhSqlaCzY7qFyrgd9oTWmTmHaXnHNeQRCZr/CXy2jCgHo4e4dJVTiuxP5dLa0YM5p5QVhJHbw==",
+ "node_modules/@oxc-project/types": {
+ "version": "0.137.0",
+ "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.137.0.tgz",
+ "integrity": "sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA==",
+ "dev": true,
"license": "MIT",
- "dependencies": {
- "@octokit/oauth-methods": "^6.0.2",
- "@octokit/request": "^10.0.6",
- "@octokit/types": "^16.0.0",
- "universal-user-agent": "^7.0.0"
- },
- "engines": {
- "node": ">= 20"
+ "funding": {
+ "url": "https://github.com/sponsors/Boshen"
}
},
- "node_modules/@octokit/auth-oauth-user": {
- "version": "6.0.2",
- "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-user/-/auth-oauth-user-6.0.2.tgz",
- "integrity": "sha512-qLoPPc6E6GJoz3XeDG/pnDhJpTkODTGG4kY0/Py154i/I003O9NazkrwJwRuzgCalhzyIeWQ+6MDvkUmKXjg/A==",
+ "node_modules/@oxc-resolver/binding-android-arm-eabi": {
+ "version": "11.21.3",
+ "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm-eabi/-/binding-android-arm-eabi-11.21.3.tgz",
+ "integrity": "sha512-eNU11A2WNizh04v3uyaJCootrHIaS0B9aHYXvAvVnPNk4xYSjMUjHnhQ6dewPN2MRYDskV85d1N0Aw0WNWhcyg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
"license": "MIT",
- "dependencies": {
- "@octokit/auth-oauth-device": "^8.0.3",
- "@octokit/oauth-methods": "^6.0.2",
- "@octokit/request": "^10.0.6",
- "@octokit/types": "^16.0.0",
- "universal-user-agent": "^7.0.0"
- },
- "engines": {
- "node": ">= 20"
- }
+ "optional": true,
+ "os": [
+ "android"
+ ]
},
- "node_modules/@octokit/auth-token": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz",
- "integrity": "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==",
+ "node_modules/@oxc-resolver/binding-android-arm64": {
+ "version": "11.21.3",
+ "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm64/-/binding-android-arm64-11.21.3.tgz",
+ "integrity": "sha512-8Q+ZjTLvn2dIcWsrmhdrEihm7q+ag/k+mkry7Z+t0QbbHaVxXQfvH9AewyVMh/WrpEKhQ3DDgx9fYbqeCpeOEw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
"license": "MIT",
- "engines": {
- "node": ">= 20"
- }
+ "optional": true,
+ "os": [
+ "android"
+ ]
},
- "node_modules/@octokit/auth-unauthenticated": {
- "version": "7.0.3",
- "resolved": "https://registry.npmjs.org/@octokit/auth-unauthenticated/-/auth-unauthenticated-7.0.3.tgz",
- "integrity": "sha512-8Jb1mtUdmBHL7lGmop9mU9ArMRUTRhg8vp0T1VtZ4yd9vEm3zcLwmjQkhNEduKawOOORie61xhtYIhTDN+ZQ3g==",
+ "node_modules/@oxc-resolver/binding-darwin-arm64": {
+ "version": "11.21.3",
+ "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-arm64/-/binding-darwin-arm64-11.21.3.tgz",
+ "integrity": "sha512-wkh0qKZGHXVUDxFw3oA1TXnU2BDYY/r775oJflGeIr8uDPPoN2pk8gijQIzYRT6hoql/lg3+Tx/SaTn9e2/aGg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
"license": "MIT",
- "dependencies": {
- "@octokit/request-error": "^7.0.2",
- "@octokit/types": "^16.0.0"
- },
- "engines": {
- "node": ">= 20"
- }
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
},
- "node_modules/@octokit/core": {
- "version": "7.0.6",
- "resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.6.tgz",
- "integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==",
+ "node_modules/@oxc-resolver/binding-darwin-x64": {
+ "version": "11.21.3",
+ "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-x64/-/binding-darwin-x64-11.21.3.tgz",
+ "integrity": "sha512-HbNc23FAQYbuyDV2vBWMez4u4mrsm5RAkniGZAWqr6lYZ3N4beeqIb776jzwRl8qL2zRhHVXpUj97X0QgogVzg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
"license": "MIT",
- "dependencies": {
- "@octokit/auth-token": "^6.0.0",
- "@octokit/graphql": "^9.0.3",
- "@octokit/request": "^10.0.6",
- "@octokit/request-error": "^7.0.2",
- "@octokit/types": "^16.0.0",
- "before-after-hook": "^4.0.0",
- "universal-user-agent": "^7.0.0"
- },
- "engines": {
- "node": ">= 20"
- }
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
},
- "node_modules/@octokit/endpoint": {
- "version": "11.0.2",
- "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.2.tgz",
- "integrity": "sha512-4zCpzP1fWc7QlqunZ5bSEjxc6yLAlRTnDwKtgXfcI/FxxGoqedDG8V2+xJ60bV2kODqcGB+nATdtap/XYq2NZQ==",
+ "node_modules/@oxc-resolver/binding-freebsd-x64": {
+ "version": "11.21.3",
+ "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-freebsd-x64/-/binding-freebsd-x64-11.21.3.tgz",
+ "integrity": "sha512-K6xNsTUPEUdfrn0+kbMq5nOUB5w1C5pavPQngt4TM2FpN91lP0PBe2srSpamb4d69O7h86oAi/qWX/kZNRSjkw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
"license": "MIT",
- "dependencies": {
- "@octokit/types": "^16.0.0",
- "universal-user-agent": "^7.0.2"
- },
- "engines": {
- "node": ">= 20"
- }
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
},
- "node_modules/@octokit/graphql": {
- "version": "9.0.3",
- "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.3.tgz",
- "integrity": "sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA==",
+ "node_modules/@oxc-resolver/binding-linux-arm-gnueabihf": {
+ "version": "11.21.3",
+ "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-11.21.3.tgz",
+ "integrity": "sha512-VcFmOpcpWX1zoEy8M58tR2M9YxM+Z9RuQhqAx5q0CTmrruaP7Gveejg75hzd/5sg5nk9G3aLALEa3hE2FsmmTQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
"license": "MIT",
- "dependencies": {
- "@octokit/request": "^10.0.6",
- "@octokit/types": "^16.0.0",
- "universal-user-agent": "^7.0.0"
- },
- "engines": {
- "node": ">= 20"
- }
+ "optional": true,
+ "os": [
+ "linux"
+ ]
},
- "node_modules/@octokit/oauth-app": {
- "version": "8.0.3",
- "resolved": "https://registry.npmjs.org/@octokit/oauth-app/-/oauth-app-8.0.3.tgz",
- "integrity": "sha512-jnAjvTsPepyUaMu9e69hYBuozEPgYqP4Z3UnpmvoIzHDpf8EXDGvTY1l1jK0RsZ194oRd+k6Hm13oRU8EoDFwg==",
+ "node_modules/@oxc-resolver/binding-linux-arm-musleabihf": {
+ "version": "11.21.3",
+ "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-11.21.3.tgz",
+ "integrity": "sha512-quVoxFLBy43hWaQbbDtQNRwAX5vX76mv7n64icAtQcJ3eNgVeblqmkupF/hAneNthdqSlnd1sTjb3aQSaDPaCQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
"license": "MIT",
- "dependencies": {
- "@octokit/auth-oauth-app": "^9.0.2",
- "@octokit/auth-oauth-user": "^6.0.1",
- "@octokit/auth-unauthenticated": "^7.0.2",
- "@octokit/core": "^7.0.5",
- "@octokit/oauth-authorization-url": "^8.0.0",
- "@octokit/oauth-methods": "^6.0.1",
- "@types/aws-lambda": "^8.10.83",
- "universal-user-agent": "^7.0.0"
- },
- "engines": {
- "node": ">= 20"
- }
+ "optional": true,
+ "os": [
+ "linux"
+ ]
},
- "node_modules/@octokit/oauth-authorization-url": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@octokit/oauth-authorization-url/-/oauth-authorization-url-8.0.0.tgz",
- "integrity": "sha512-7QoLPRh/ssEA/HuHBHdVdSgF8xNLz/Bc5m9fZkArJE5bb6NmVkDm3anKxXPmN1zh6b5WKZPRr3697xKT/yM3qQ==",
+ "node_modules/@oxc-resolver/binding-linux-arm64-gnu": {
+ "version": "11.21.3",
+ "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-11.21.3.tgz",
+ "integrity": "sha512-X0AqNZgcD07Q4V3RDK18/vYOj/HQT/FnmEFGYS2jTWqY7JO13ryE3TEs3eAIgUJhBnNkpEaiXqz3VK8M7qQhWQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
"license": "MIT",
- "engines": {
- "node": ">= 20"
- }
+ "optional": true,
+ "os": [
+ "linux"
+ ]
},
- "node_modules/@octokit/oauth-methods": {
- "version": "6.0.2",
- "resolved": "https://registry.npmjs.org/@octokit/oauth-methods/-/oauth-methods-6.0.2.tgz",
- "integrity": "sha512-HiNOO3MqLxlt5Da5bZbLV8Zarnphi4y9XehrbaFMkcoJ+FL7sMxH/UlUsCVxpddVu4qvNDrBdaTVE2o4ITK8ng==",
+ "node_modules/@oxc-resolver/binding-linux-arm64-musl": {
+ "version": "11.21.3",
+ "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-musl/-/binding-linux-arm64-musl-11.21.3.tgz",
+ "integrity": "sha512-YkaQnaKYdbuaXvRt5Qd0GpbihzVnyfR6z1SpYfIUC6RTu4NF7lDKPjVkYb+jRI2gedVO2rVpN35Y6akG6ud4Lw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
"license": "MIT",
- "dependencies": {
- "@octokit/oauth-authorization-url": "^8.0.0",
- "@octokit/request": "^10.0.6",
- "@octokit/request-error": "^7.0.2",
- "@octokit/types": "^16.0.0"
- },
- "engines": {
- "node": ">= 20"
- }
- },
- "node_modules/@octokit/openapi-types": {
- "version": "27.0.0",
- "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-27.0.0.tgz",
- "integrity": "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA==",
- "license": "MIT"
- },
- "node_modules/@octokit/openapi-webhooks-types": {
- "version": "12.1.0",
- "resolved": "https://registry.npmjs.org/@octokit/openapi-webhooks-types/-/openapi-webhooks-types-12.1.0.tgz",
- "integrity": "sha512-WiuzhOsiOvb7W3Pvmhf8d2C6qaLHXrWiLBP4nJ/4kydu+wpagV5Fkz9RfQwV2afYzv3PB+3xYgp4mAdNGjDprA==",
- "license": "MIT"
+ "optional": true,
+ "os": [
+ "linux"
+ ]
},
- "node_modules/@octokit/plugin-paginate-graphql": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-graphql/-/plugin-paginate-graphql-6.0.0.tgz",
- "integrity": "sha512-crfpnIoFiBtRkvPqOyLOsw12XsveYuY2ieP6uYDosoUegBJpSVxGwut9sxUgFFcll3VTOTqpUf8yGd8x1OmAkQ==",
+ "node_modules/@oxc-resolver/binding-linux-ppc64-gnu": {
+ "version": "11.21.3",
+ "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-11.21.3.tgz",
+ "integrity": "sha512-gB9HwhrPiFqUzDeEq+y/CgAijz1YdI6BnXz5GaH2Pa9cWdutchlkGFAiAuGb/PjVQpiK6NFKzFuztxrweoit7A==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
"license": "MIT",
- "engines": {
- "node": ">= 20"
- },
- "peerDependencies": {
- "@octokit/core": ">=6"
- }
+ "optional": true,
+ "os": [
+ "linux"
+ ]
},
- "node_modules/@octokit/plugin-paginate-rest": {
- "version": "14.0.0",
- "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-14.0.0.tgz",
- "integrity": "sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw==",
+ "node_modules/@oxc-resolver/binding-linux-riscv64-gnu": {
+ "version": "11.21.3",
+ "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-11.21.3.tgz",
+ "integrity": "sha512-zjDWBlYk8QGv0H8dsPUWqkfjYIIjG2TvspGkzXL0eImbgxtZorA/klKeHyolevoT3Kvbi+1iMr9Lhrh7jf54Og==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
"license": "MIT",
- "dependencies": {
- "@octokit/types": "^16.0.0"
- },
- "engines": {
- "node": ">= 20"
- },
- "peerDependencies": {
- "@octokit/core": ">=6"
- }
+ "optional": true,
+ "os": [
+ "linux"
+ ]
},
- "node_modules/@octokit/plugin-rest-endpoint-methods": {
- "version": "17.0.0",
- "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-17.0.0.tgz",
- "integrity": "sha512-B5yCyIlOJFPqUUeiD0cnBJwWJO8lkJs5d8+ze9QDP6SvfiXSz1BF+91+0MeI1d2yxgOhU/O+CvtiZ9jSkHhFAw==",
+ "node_modules/@oxc-resolver/binding-linux-riscv64-musl": {
+ "version": "11.21.3",
+ "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-11.21.3.tgz",
+ "integrity": "sha512-4UfsQvacV388y1zpXL7C1x1FNYaV52JtuNRiuzrfQA2z1z6ElVrsidkGsrvQ5EgeSq1Pj7kaKqrgGkvFuxJ/tw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
"license": "MIT",
- "dependencies": {
- "@octokit/types": "^16.0.0"
- },
- "engines": {
- "node": ">= 20"
- },
- "peerDependencies": {
- "@octokit/core": ">=6"
- }
+ "optional": true,
+ "os": [
+ "linux"
+ ]
},
- "node_modules/@octokit/plugin-retry": {
- "version": "8.0.3",
- "resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-8.0.3.tgz",
- "integrity": "sha512-vKGx1i3MC0za53IzYBSBXcrhmd+daQDzuZfYDd52X5S0M2otf3kVZTVP8bLA3EkU0lTvd1WEC2OlNNa4G+dohA==",
+ "node_modules/@oxc-resolver/binding-linux-s390x-gnu": {
+ "version": "11.21.3",
+ "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-11.21.3.tgz",
+ "integrity": "sha512-b5uH+HKH0MP5mNBYaK75SKsJbw52URqrx2LavYdq6wb0l3ExAG5niYRP9DWUNHdKilpaBVM2bXk9HNWrH3ew7Q==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
"license": "MIT",
- "dependencies": {
- "@octokit/request-error": "^7.0.2",
- "@octokit/types": "^16.0.0",
- "bottleneck": "^2.15.3"
- },
- "engines": {
- "node": ">= 20"
- },
- "peerDependencies": {
- "@octokit/core": ">=7"
- }
+ "optional": true,
+ "os": [
+ "linux"
+ ]
},
- "node_modules/@octokit/plugin-throttling": {
- "version": "11.0.3",
- "resolved": "https://registry.npmjs.org/@octokit/plugin-throttling/-/plugin-throttling-11.0.3.tgz",
- "integrity": "sha512-34eE0RkFCKycLl2D2kq7W+LovheM/ex3AwZCYN8udpi6bxsyjZidb2McXs69hZhLmJlDqTSP8cH+jSRpiaijBg==",
+ "node_modules/@oxc-resolver/binding-linux-x64-gnu": {
+ "version": "11.21.3",
+ "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-gnu/-/binding-linux-x64-gnu-11.21.3.tgz",
+ "integrity": "sha512-PjYlmilBpNRh2ntXNYAK3Am5w/nPfEpnU/96iNx7CI8EzAn12J4JRiec63wHJTH31nLoCNxBg/829pN+3CfG3Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
"license": "MIT",
- "dependencies": {
- "@octokit/types": "^16.0.0",
- "bottleneck": "^2.15.3"
- },
- "engines": {
- "node": ">= 20"
- },
- "peerDependencies": {
- "@octokit/core": "^7.0.0"
- }
+ "optional": true,
+ "os": [
+ "linux"
+ ]
},
- "node_modules/@octokit/request": {
- "version": "10.0.7",
- "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.7.tgz",
- "integrity": "sha512-v93h0i1yu4idj8qFPZwjehoJx4j3Ntn+JhXsdJrG9pYaX6j/XRz2RmasMUHtNgQD39nrv/VwTWSqK0RNXR8upA==",
+ "node_modules/@oxc-resolver/binding-linux-x64-musl": {
+ "version": "11.21.3",
+ "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-musl/-/binding-linux-x64-musl-11.21.3.tgz",
+ "integrity": "sha512-QTBAb7JuHlZ7JUEyM8UiQi2f7m/L4swBhP2TNpYIDc9Wp/wRw1G/8sl6i13aIzQAXH7LKIm294LeOHd0lQR8zA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
"license": "MIT",
- "dependencies": {
- "@octokit/endpoint": "^11.0.2",
- "@octokit/request-error": "^7.0.2",
- "@octokit/types": "^16.0.0",
- "fast-content-type-parse": "^3.0.0",
- "universal-user-agent": "^7.0.2"
- },
- "engines": {
- "node": ">= 20"
- }
- },
- "node_modules/@octokit/request-error": {
- "version": "7.1.0",
- "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.1.0.tgz",
- "integrity": "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw==",
- "license": "MIT",
- "dependencies": {
- "@octokit/types": "^16.0.0"
- },
- "engines": {
- "node": ">= 20"
- }
+ "optional": true,
+ "os": [
+ "linux"
+ ]
},
- "node_modules/@octokit/types": {
- "version": "16.0.0",
- "resolved": "https://registry.npmjs.org/@octokit/types/-/types-16.0.0.tgz",
- "integrity": "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==",
+ "node_modules/@oxc-resolver/binding-openharmony-arm64": {
+ "version": "11.21.3",
+ "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-openharmony-arm64/-/binding-openharmony-arm64-11.21.3.tgz",
+ "integrity": "sha512-4j1DFwjwv36ec9kds0jU/ucQ5Ha4ERO/H95BxR5JFf0kqUUAJ1kwII7XhTc1vZrkdJkvLGC9Q2MbpObpum8RBg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
"license": "MIT",
- "dependencies": {
- "@octokit/openapi-types": "^27.0.0"
- }
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
},
- "node_modules/@octokit/webhooks": {
- "version": "14.2.0",
- "resolved": "https://registry.npmjs.org/@octokit/webhooks/-/webhooks-14.2.0.tgz",
- "integrity": "sha512-da6KbdNCV5sr1/txD896V+6W0iamFWrvVl8cHkBSPT+YlvmT3DwXa4jxZnQc+gnuTEqSWbBeoSZYTayXH9wXcw==",
+ "node_modules/@oxc-resolver/binding-wasm32-wasi": {
+ "version": "11.21.3",
+ "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-wasm32-wasi/-/binding-wasm32-wasi-11.21.3.tgz",
+ "integrity": "sha512-i8oluoel5kru/j1WNrjmQSiA3GQ7wvIYVR1IwIoZtKogAhya2iub+ZKIeSIkcJOrnzQ18Tzl/F+kL3fYOxZLvA==",
+ "cpu": [
+ "wasm32"
+ ],
+ "dev": true,
"license": "MIT",
+ "optional": true,
"dependencies": {
- "@octokit/openapi-webhooks-types": "12.1.0",
- "@octokit/request-error": "^7.0.0",
- "@octokit/webhooks-methods": "^6.0.0"
+ "@emnapi/core": "1.11.0",
+ "@emnapi/runtime": "1.11.0",
+ "@napi-rs/wasm-runtime": "^1.1.5"
},
"engines": {
- "node": ">= 20"
+ "node": ">=14.0.0"
}
},
- "node_modules/@octokit/webhooks-methods": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/@octokit/webhooks-methods/-/webhooks-methods-6.0.0.tgz",
- "integrity": "sha512-MFlzzoDJVw/GcbfzVC1RLR36QqkTLUf79vLVO3D+xn7r0QgxnFoLZgtrzxiQErAjFUOdH6fas2KeQJ1yr/qaXQ==",
+ "node_modules/@oxc-resolver/binding-wasm32-wasi/node_modules/@emnapi/core": {
+ "version": "1.11.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.0.tgz",
+ "integrity": "sha512-l9Oo58x0HOP5znGzVhYW9U3e5wVuA4LAZU2AGezTmkhO1CgQRFDhDg4nneHsu/t3WniXg9QrG2nIXL/ZS8ln8Q==",
+ "dev": true,
"license": "MIT",
- "engines": {
- "node": ">= 20"
- }
- },
- "node_modules/@offgrid/clipboard": {
- "resolved": "packages/clipboard",
- "link": true
- },
- "node_modules/@offgrid/design": {
- "resolved": "packages/design",
- "link": true
- },
- "node_modules/@offgrid/models": {
- "resolved": "packages/models",
- "link": true
- },
- "node_modules/@offgrid/rag": {
- "resolved": "packages/rag",
- "link": true
- },
- "node_modules/@opentelemetry/api": {
- "version": "1.9.0",
- "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
- "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
- "license": "Apache-2.0",
- "engines": {
- "node": ">=8.0.0"
- }
- },
- "node_modules/@opentelemetry/api-logs": {
- "version": "0.208.0",
- "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.208.0.tgz",
- "integrity": "sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg==",
- "license": "Apache-2.0",
- "dependencies": {
- "@opentelemetry/api": "^1.3.0"
- },
- "engines": {
- "node": ">=8.0.0"
- }
- },
- "node_modules/@opentelemetry/core": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.2.0.tgz",
- "integrity": "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==",
- "license": "Apache-2.0",
- "dependencies": {
- "@opentelemetry/semantic-conventions": "^1.29.0"
- },
- "engines": {
- "node": "^18.19.0 || >=20.6.0"
- },
- "peerDependencies": {
- "@opentelemetry/api": ">=1.0.0 <1.10.0"
- }
- },
- "node_modules/@opentelemetry/exporter-logs-otlp-http": {
- "version": "0.208.0",
- "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.208.0.tgz",
- "integrity": "sha512-jOv40Bs9jy9bZVLo/i8FwUiuCvbjWDI+ZW13wimJm4LjnlwJxGgB+N/VWOZUTpM+ah/awXeQqKdNlpLf2EjvYg==",
- "license": "Apache-2.0",
- "dependencies": {
- "@opentelemetry/api-logs": "0.208.0",
- "@opentelemetry/core": "2.2.0",
- "@opentelemetry/otlp-exporter-base": "0.208.0",
- "@opentelemetry/otlp-transformer": "0.208.0",
- "@opentelemetry/sdk-logs": "0.208.0"
- },
- "engines": {
- "node": "^18.19.0 || >=20.6.0"
- },
- "peerDependencies": {
- "@opentelemetry/api": "^1.3.0"
- }
- },
- "node_modules/@opentelemetry/otlp-exporter-base": {
- "version": "0.208.0",
- "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.208.0.tgz",
- "integrity": "sha512-gMd39gIfVb2OgxldxUtOwGJYSH8P1kVFFlJLuut32L6KgUC4gl1dMhn+YC2mGn0bDOiQYSk/uHOdSjuKp58vvA==",
- "license": "Apache-2.0",
- "dependencies": {
- "@opentelemetry/core": "2.2.0",
- "@opentelemetry/otlp-transformer": "0.208.0"
- },
- "engines": {
- "node": "^18.19.0 || >=20.6.0"
- },
- "peerDependencies": {
- "@opentelemetry/api": "^1.3.0"
- }
- },
- "node_modules/@opentelemetry/otlp-transformer": {
- "version": "0.208.0",
- "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.208.0.tgz",
- "integrity": "sha512-DCFPY8C6lAQHUNkzcNT9R+qYExvsk6C5Bto2pbNxgicpcSWbe2WHShLxkOxIdNcBiYPdVHv/e7vH7K6TI+C+fQ==",
- "license": "Apache-2.0",
- "dependencies": {
- "@opentelemetry/api-logs": "0.208.0",
- "@opentelemetry/core": "2.2.0",
- "@opentelemetry/resources": "2.2.0",
- "@opentelemetry/sdk-logs": "0.208.0",
- "@opentelemetry/sdk-metrics": "2.2.0",
- "@opentelemetry/sdk-trace-base": "2.2.0",
- "protobufjs": "^7.3.0"
- },
- "engines": {
- "node": "^18.19.0 || >=20.6.0"
- },
- "peerDependencies": {
- "@opentelemetry/api": "^1.3.0"
- }
- },
- "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/resources": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz",
- "integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==",
- "license": "Apache-2.0",
- "dependencies": {
- "@opentelemetry/core": "2.2.0",
- "@opentelemetry/semantic-conventions": "^1.29.0"
- },
- "engines": {
- "node": "^18.19.0 || >=20.6.0"
- },
- "peerDependencies": {
- "@opentelemetry/api": ">=1.3.0 <1.10.0"
- }
- },
- "node_modules/@opentelemetry/otlp-transformer/node_modules/long": {
- "version": "5.3.2",
- "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
- "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
- "license": "Apache-2.0"
- },
- "node_modules/@opentelemetry/otlp-transformer/node_modules/protobufjs": {
- "version": "7.5.4",
- "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz",
- "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==",
- "hasInstallScript": true,
- "license": "BSD-3-Clause",
- "dependencies": {
- "@protobufjs/aspromise": "^1.1.2",
- "@protobufjs/base64": "^1.1.2",
- "@protobufjs/codegen": "^2.0.4",
- "@protobufjs/eventemitter": "^1.1.0",
- "@protobufjs/fetch": "^1.1.0",
- "@protobufjs/float": "^1.0.2",
- "@protobufjs/inquire": "^1.1.0",
- "@protobufjs/path": "^1.1.2",
- "@protobufjs/pool": "^1.1.0",
- "@protobufjs/utf8": "^1.1.0",
- "@types/node": ">=13.7.0",
- "long": "^5.0.0"
- },
- "engines": {
- "node": ">=12.0.0"
- }
- },
- "node_modules/@opentelemetry/resources": {
- "version": "2.4.0",
- "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.4.0.tgz",
- "integrity": "sha512-RWvGLj2lMDZd7M/5tjkI/2VHMpXebLgPKvBUd9LRasEWR2xAynDwEYZuLvY9P2NGG73HF07jbbgWX2C9oavcQg==",
- "license": "Apache-2.0",
+ "optional": true,
"dependencies": {
- "@opentelemetry/core": "2.4.0",
- "@opentelemetry/semantic-conventions": "^1.29.0"
- },
- "engines": {
- "node": "^18.19.0 || >=20.6.0"
- },
- "peerDependencies": {
- "@opentelemetry/api": ">=1.3.0 <1.10.0"
+ "@emnapi/wasi-threads": "1.2.2",
+ "tslib": "^2.4.0"
}
},
- "node_modules/@opentelemetry/resources/node_modules/@opentelemetry/core": {
- "version": "2.4.0",
- "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.4.0.tgz",
- "integrity": "sha512-KtcyFHssTn5ZgDu6SXmUznS80OFs/wN7y6MyFRRcKU6TOw8hNcGxKvt8hsdaLJfhzUszNSjURetq5Qpkad14Gw==",
- "license": "Apache-2.0",
+ "node_modules/@oxc-resolver/binding-wasm32-wasi/node_modules/@emnapi/runtime": {
+ "version": "1.11.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.0.tgz",
+ "integrity": "sha512-55coeOFKHv1ywEcUXJtWU5f+Jr/W5tZDvZig8DLKSwUN1JpROQ4rk/SNOQiFWmaR/VKF4zuFyW1B8JduOSv6Pg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
"dependencies": {
- "@opentelemetry/semantic-conventions": "^1.29.0"
- },
- "engines": {
- "node": "^18.19.0 || >=20.6.0"
- },
- "peerDependencies": {
- "@opentelemetry/api": ">=1.0.0 <1.10.0"
+ "tslib": "^2.4.0"
}
},
- "node_modules/@opentelemetry/sdk-logs": {
- "version": "0.208.0",
- "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.208.0.tgz",
- "integrity": "sha512-QlAyL1jRpOeaqx7/leG1vJMp84g0xKP6gJmfELBpnI4O/9xPX+Hu5m1POk9Kl+veNkyth5t19hRlN6tNY1sjbA==",
- "license": "Apache-2.0",
- "dependencies": {
- "@opentelemetry/api-logs": "0.208.0",
- "@opentelemetry/core": "2.2.0",
- "@opentelemetry/resources": "2.2.0"
- },
- "engines": {
- "node": "^18.19.0 || >=20.6.0"
- },
- "peerDependencies": {
- "@opentelemetry/api": ">=1.4.0 <1.10.0"
- }
+ "node_modules/@oxc-resolver/binding-win32-arm64-msvc": {
+ "version": "11.21.3",
+ "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-11.21.3.tgz",
+ "integrity": "sha512-M/8dw8dD6aOs+NlPJax401CZB9I7Aut84isQLgALGGwke4Afvw+/7yYhZb94yXf6t2sPLhQLmSmtSV+2FhsOWg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
},
- "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/resources": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz",
- "integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==",
- "license": "Apache-2.0",
- "dependencies": {
- "@opentelemetry/core": "2.2.0",
- "@opentelemetry/semantic-conventions": "^1.29.0"
- },
- "engines": {
- "node": "^18.19.0 || >=20.6.0"
- },
- "peerDependencies": {
- "@opentelemetry/api": ">=1.3.0 <1.10.0"
- }
+ "node_modules/@oxc-resolver/binding-win32-x64-msvc": {
+ "version": "11.21.3",
+ "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-x64-msvc/-/binding-win32-x64-msvc-11.21.3.tgz",
+ "integrity": "sha512-H7BCt/VnS9hnmMp42eGhZ99izSCRvlnWwy/N71K1/J8QoExwY4262Z8QiEkMDtduRJrztayDxETTckmUuAVL9Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
},
- "node_modules/@opentelemetry/sdk-metrics": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.2.0.tgz",
- "integrity": "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw==",
- "license": "Apache-2.0",
+ "node_modules/@peculiar/asn1-schema": {
+ "version": "2.8.0",
+ "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.8.0.tgz",
+ "integrity": "sha512-7YT0U/ze0tF2QOBbE15gKZwy5tvgGyLRiRHLzhlbOpf7BT032oBSd0haZqXn5W6l26WLlu3dyxzjM+2638/z2Q==",
+ "dev": true,
+ "license": "MIT",
"dependencies": {
- "@opentelemetry/core": "2.2.0",
- "@opentelemetry/resources": "2.2.0"
- },
- "engines": {
- "node": "^18.19.0 || >=20.6.0"
- },
- "peerDependencies": {
- "@opentelemetry/api": ">=1.9.0 <1.10.0"
+ "@peculiar/utils": "^2.0.2",
+ "asn1js": "^3.0.10",
+ "tslib": "^2.8.1"
}
},
- "node_modules/@opentelemetry/sdk-metrics/node_modules/@opentelemetry/resources": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz",
- "integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==",
- "license": "Apache-2.0",
+ "node_modules/@peculiar/json-schema": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/@peculiar/json-schema/-/json-schema-1.1.12.tgz",
+ "integrity": "sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w==",
+ "dev": true,
+ "license": "MIT",
"dependencies": {
- "@opentelemetry/core": "2.2.0",
- "@opentelemetry/semantic-conventions": "^1.29.0"
+ "tslib": "^2.0.0"
},
"engines": {
- "node": "^18.19.0 || >=20.6.0"
- },
- "peerDependencies": {
- "@opentelemetry/api": ">=1.3.0 <1.10.0"
+ "node": ">=8.0.0"
}
},
- "node_modules/@opentelemetry/sdk-trace-base": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.2.0.tgz",
- "integrity": "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==",
- "license": "Apache-2.0",
+ "node_modules/@peculiar/utils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/@peculiar/utils/-/utils-2.0.3.tgz",
+ "integrity": "sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ==",
+ "dev": true,
+ "license": "MIT",
"dependencies": {
- "@opentelemetry/core": "2.2.0",
- "@opentelemetry/resources": "2.2.0",
- "@opentelemetry/semantic-conventions": "^1.29.0"
- },
- "engines": {
- "node": "^18.19.0 || >=20.6.0"
- },
- "peerDependencies": {
- "@opentelemetry/api": ">=1.3.0 <1.10.0"
+ "tslib": "^2.8.1"
}
},
- "node_modules/@opentelemetry/sdk-trace-base/node_modules/@opentelemetry/resources": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz",
- "integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==",
- "license": "Apache-2.0",
+ "node_modules/@peculiar/webcrypto": {
+ "version": "1.7.1",
+ "resolved": "https://registry.npmjs.org/@peculiar/webcrypto/-/webcrypto-1.7.1.tgz",
+ "integrity": "sha512-ODOov0sGMJMf3jPonOkgGqPknTsu+DdQ7kD++gz8aI+aFMOMHFbWAA2taqXXVTdP+OTOQR/znGvSpmkeI0WTYQ==",
+ "dev": true,
+ "license": "MIT",
"dependencies": {
- "@opentelemetry/core": "2.2.0",
- "@opentelemetry/semantic-conventions": "^1.29.0"
+ "@peculiar/asn1-schema": "^2.7.0",
+ "@peculiar/json-schema": "^1.1.12",
+ "@peculiar/utils": "^2.0.2",
+ "tslib": "^2.8.1",
+ "webcrypto-core": "^1.9.2"
},
"engines": {
- "node": "^18.19.0 || >=20.6.0"
- },
- "peerDependencies": {
- "@opentelemetry/api": ">=1.3.0 <1.10.0"
- }
- },
- "node_modules/@opentelemetry/semantic-conventions": {
- "version": "1.39.0",
- "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.39.0.tgz",
- "integrity": "sha512-R5R9tb2AXs2IRLNKLBJDynhkfmx7mX0vi8NkhZb3gUkPWHn6HXk5J8iQ/dql0U3ApfWym4kXXmBDRGO+oeOfjg==",
- "license": "Apache-2.0",
- "engines": {
- "node": ">=14"
+ "node": ">=14.18.0"
}
},
"node_modules/@phosphor-icons/react": {
@@ -4744,21 +4635,6 @@
"node": ">=18"
}
},
- "node_modules/@posthog/core": {
- "version": "1.11.0",
- "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.11.0.tgz",
- "integrity": "sha512-BnUQ9FP5vqMr2NKntDSLfMCwO/pOI2In7kAjg6vLVzU1JdcPt266kwCZj84PTYbdSfHG5ELDs3hXNv9Rn+coUw==",
- "license": "MIT",
- "dependencies": {
- "cross-spawn": "^7.0.6"
- }
- },
- "node_modules/@posthog/types": {
- "version": "1.331.0",
- "resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.331.0.tgz",
- "integrity": "sha512-C2eT5T2zovjLB+KOOWOb4vGBInH58imeuOrH/DL3hpcH0V5Nd5ttOAZczr1AxsI08lR5VaUPCw2bUsfPxYJGRA==",
- "license": "MIT"
- },
"node_modules/@protobufjs/aspromise": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
@@ -6315,193 +6191,19 @@
"integrity": "sha512-xnXE7wG13PI+cxieVssYXlQJuYVRhH9NBoxt3KNwzghDIA69GMm7d4wXRouHIYjE+KvS6U/MsMO73NdS2MH9ZA==",
"license": "MIT"
},
- "node_modules/@react-three/fiber": {
- "version": "10.0.0-alpha.1",
- "resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-10.0.0-alpha.1.tgz",
- "integrity": "sha512-GsjYIeW7JpJXkO1RWGqY5/GaDJXKzJei+1CrPXj6x1XmCY0QIKELU7+nhX6kdG0/nIndB7TpBFCB5Ob+JfOXnA==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.17.8",
- "dequal": "^2.0.3",
- "its-fine": "^2.0.0",
- "react-use-measure": "^2.1.7",
- "scheduler": "^0.27.0",
- "suspend-react": "^0.1.3",
- "use-sync-external-store": "^1.4.0",
- "zustand": "^5.0.3"
- },
- "peerDependencies": {
- "react": ">=19.0 <19.3",
- "react-dom": ">=19.0 <19.3",
- "three": ">=0.181.2"
- },
- "peerDependenciesMeta": {
- "react-dom": {
- "optional": true
- }
- }
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-beta.53",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz",
+ "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==",
+ "dev": true,
+ "license": "MIT"
},
- "node_modules/@reflink/reflink": {
- "version": "0.1.19",
- "resolved": "https://registry.npmjs.org/@reflink/reflink/-/reflink-0.1.19.tgz",
- "integrity": "sha512-DmCG8GzysnCZ15bres3N5AHCmwBwYgp0As6xjhQ47rAUTUXxJiK+lLUxaGsX3hd/30qUpVElh05PbGuxRPgJwA==",
- "license": "MIT",
- "optional": true,
- "engines": {
- "node": ">= 10"
- },
- "optionalDependencies": {
- "@reflink/reflink-darwin-arm64": "0.1.19",
- "@reflink/reflink-darwin-x64": "0.1.19",
- "@reflink/reflink-linux-arm64-gnu": "0.1.19",
- "@reflink/reflink-linux-arm64-musl": "0.1.19",
- "@reflink/reflink-linux-x64-gnu": "0.1.19",
- "@reflink/reflink-linux-x64-musl": "0.1.19",
- "@reflink/reflink-win32-arm64-msvc": "0.1.19",
- "@reflink/reflink-win32-x64-msvc": "0.1.19"
- }
- },
- "node_modules/@reflink/reflink-darwin-arm64": {
- "version": "0.1.19",
- "resolved": "https://registry.npmjs.org/@reflink/reflink-darwin-arm64/-/reflink-darwin-arm64-0.1.19.tgz",
- "integrity": "sha512-ruy44Lpepdk1FqDz38vExBY/PVUsjxZA+chd9wozjUH9JjuDT/HEaQYA6wYN9mf041l0yLVar6BCZuWABJvHSA==",
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz",
+ "integrity": "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==",
"cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@reflink/reflink-darwin-x64": {
- "version": "0.1.19",
- "resolved": "https://registry.npmjs.org/@reflink/reflink-darwin-x64/-/reflink-darwin-x64-0.1.19.tgz",
- "integrity": "sha512-By85MSWrMZa+c26TcnAy8SDk0sTUkYlNnwknSchkhHpGXOtjNDUOxJE9oByBnGbeuIE1PiQsxDG3Ud+IVV9yuA==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@reflink/reflink-linux-arm64-gnu": {
- "version": "0.1.19",
- "resolved": "https://registry.npmjs.org/@reflink/reflink-linux-arm64-gnu/-/reflink-linux-arm64-gnu-0.1.19.tgz",
- "integrity": "sha512-7P+er8+rP9iNeN+bfmccM4hTAaLP6PQJPKWSA4iSk2bNvo6KU6RyPgYeHxXmzNKzPVRcypZQTpFgstHam6maVg==",
- "cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@reflink/reflink-linux-arm64-musl": {
- "version": "0.1.19",
- "resolved": "https://registry.npmjs.org/@reflink/reflink-linux-arm64-musl/-/reflink-linux-arm64-musl-0.1.19.tgz",
- "integrity": "sha512-37iO/Dp6m5DDaC2sf3zPtx/hl9FV3Xze4xoYidrxxS9bgP3S8ALroxRK6xBG/1TtfXKTvolvp+IjrUU6ujIGmA==",
- "cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@reflink/reflink-linux-x64-gnu": {
- "version": "0.1.19",
- "resolved": "https://registry.npmjs.org/@reflink/reflink-linux-x64-gnu/-/reflink-linux-x64-gnu-0.1.19.tgz",
- "integrity": "sha512-jbI8jvuYCaA3MVUdu8vLoLAFqC+iNMpiSuLbxlAgg7x3K5bsS8nOpTRnkLF7vISJ+rVR8W+7ThXlXlUQ93ulkw==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@reflink/reflink-linux-x64-musl": {
- "version": "0.1.19",
- "resolved": "https://registry.npmjs.org/@reflink/reflink-linux-x64-musl/-/reflink-linux-x64-musl-0.1.19.tgz",
- "integrity": "sha512-e9FBWDe+lv7QKAwtKOt6A2W/fyy/aEEfr0g6j/hWzvQcrzHCsz07BNQYlNOjTfeytrtLU7k449H1PI95jA4OjQ==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@reflink/reflink-win32-arm64-msvc": {
- "version": "0.1.19",
- "resolved": "https://registry.npmjs.org/@reflink/reflink-win32-arm64-msvc/-/reflink-win32-arm64-msvc-0.1.19.tgz",
- "integrity": "sha512-09PxnVIQcd+UOn4WAW73WU6PXL7DwGS6wPlkMhMg2zlHHG65F3vHepOw06HFCq+N42qkaNAc8AKIabWvtk6cIQ==",
- "cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@reflink/reflink-win32-x64-msvc": {
- "version": "0.1.19",
- "resolved": "https://registry.npmjs.org/@reflink/reflink-win32-x64-msvc/-/reflink-win32-x64-msvc-0.1.19.tgz",
- "integrity": "sha512-E//yT4ni2SyhwP8JRjVGWr3cbnhWDiPLgnQ66qqaanjjnMiu3O/2tjCPQXlcGc/DEYofpDc9fvhv6tALQsMV9w==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@rolldown/pluginutils": {
- "version": "1.0.0-beta.53",
- "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz",
- "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@rollup/rollup-android-arm-eabi": {
- "version": "4.55.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz",
- "integrity": "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==",
- "cpu": [
- "arm"
+ "arm"
],
"license": "MIT",
"optional": true,
@@ -6859,6 +6561,7 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
+ "dev": true,
"license": "MIT"
},
"node_modules/@swc/helpers": {
@@ -7165,1267 +6868,1349 @@
"vite": "^5.2.0 || ^6 || ^7"
}
},
- "node_modules/@tinyhttp/content-disposition": {
- "version": "2.2.2",
- "resolved": "https://registry.npmjs.org/@tinyhttp/content-disposition/-/content-disposition-2.2.2.tgz",
- "integrity": "sha512-crXw1txzrS36huQOyQGYFvhTeLeG0Si1xu+/l6kXUVYpE0TjFjEZRqTbuadQLfKGZ0jaI+jJoRyqaWwxOSHW2g==",
+ "node_modules/@testing-library/dom": {
+ "version": "10.4.1",
+ "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
+ "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
+ "dev": true,
"license": "MIT",
- "engines": {
- "node": ">=12.20.0"
+ "peer": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.10.4",
+ "@babel/runtime": "^7.12.5",
+ "@types/aria-query": "^5.0.1",
+ "aria-query": "5.3.0",
+ "dom-accessibility-api": "^0.5.9",
+ "lz-string": "^1.5.0",
+ "picocolors": "1.1.1",
+ "pretty-format": "^27.0.2"
},
- "funding": {
- "type": "individual",
- "url": "https://github.com/tinyhttp/tinyhttp?sponsor=1"
+ "engines": {
+ "node": ">=18"
}
},
- "node_modules/@tsparticles/basic": {
- "version": "3.9.1",
- "resolved": "https://registry.npmjs.org/@tsparticles/basic/-/basic-3.9.1.tgz",
- "integrity": "sha512-ijr2dHMx0IQHqhKW3qA8tfwrR2XYbbWYdaJMQuBo2CkwBVIhZ76U+H20Y492j/NXpd1FUnt2aC0l4CEVGVGdeQ==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/matteobruni"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/tsparticles"
- },
- {
- "type": "buymeacoffee",
- "url": "https://www.buymeacoffee.com/matteobruni"
- }
- ],
+ "node_modules/@testing-library/react": {
+ "version": "16.3.2",
+ "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz",
+ "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@tsparticles/engine": "3.9.1",
- "@tsparticles/move-base": "3.9.1",
- "@tsparticles/plugin-hex-color": "3.9.1",
- "@tsparticles/plugin-hsl-color": "3.9.1",
- "@tsparticles/plugin-rgb-color": "3.9.1",
- "@tsparticles/shape-circle": "3.9.1",
- "@tsparticles/updater-color": "3.9.1",
- "@tsparticles/updater-opacity": "3.9.1",
- "@tsparticles/updater-out-modes": "3.9.1",
- "@tsparticles/updater-size": "3.9.1"
- }
- },
- "node_modules/@tsparticles/engine": {
- "version": "3.9.1",
- "resolved": "https://registry.npmjs.org/@tsparticles/engine/-/engine-3.9.1.tgz",
- "integrity": "sha512-DpdgAhWMZ3Eh2gyxik8FXS6BKZ8vyea+Eu5BC4epsahqTGY9V3JGGJcXC6lRJx6cPMAx1A0FaQAojPF3v6rkmQ==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/matteobruni"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/tsparticles"
+ "@babel/runtime": "^7.12.5"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@testing-library/dom": "^10.0.0",
+ "@types/react": "^18.0.0 || ^19.0.0",
+ "@types/react-dom": "^18.0.0 || ^19.0.0",
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
},
- {
- "type": "buymeacoffee",
- "url": "https://www.buymeacoffee.com/matteobruni"
+ "@types/react-dom": {
+ "optional": true
}
- ],
- "hasInstallScript": true,
- "license": "MIT"
- },
- "node_modules/@tsparticles/interaction-external-attract": {
- "version": "3.9.1",
- "resolved": "https://registry.npmjs.org/@tsparticles/interaction-external-attract/-/interaction-external-attract-3.9.1.tgz",
- "integrity": "sha512-5AJGmhzM9o4AVFV24WH5vSqMBzOXEOzIdGLIr+QJf4fRh9ZK62snsusv/ozKgs2KteRYQx+L7c5V3TqcDy2upg==",
- "license": "MIT",
- "dependencies": {
- "@tsparticles/engine": "3.9.1"
}
},
- "node_modules/@tsparticles/interaction-external-bounce": {
- "version": "3.9.1",
- "resolved": "https://registry.npmjs.org/@tsparticles/interaction-external-bounce/-/interaction-external-bounce-3.9.1.tgz",
- "integrity": "sha512-bv05+h70UIHOTWeTsTI1AeAmX6R3s8nnY74Ea6p6AbQjERzPYIa0XY19nq/hA7+Nrg+EissP5zgoYYeSphr85A==",
+ "node_modules/@testing-library/user-event": {
+ "version": "14.6.1",
+ "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz",
+ "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==",
+ "dev": true,
"license": "MIT",
- "dependencies": {
- "@tsparticles/engine": "3.9.1"
+ "engines": {
+ "node": ">=12",
+ "npm": ">=6"
+ },
+ "peerDependencies": {
+ "@testing-library/dom": ">=7.21.4"
}
},
- "node_modules/@tsparticles/interaction-external-bubble": {
- "version": "3.9.1",
- "resolved": "https://registry.npmjs.org/@tsparticles/interaction-external-bubble/-/interaction-external-bubble-3.9.1.tgz",
- "integrity": "sha512-tbd8ox/1GPl+zr+KyHQVV1bW88GE7OM6i4zql801YIlCDrl9wgTDdDFGIy9X7/cwTvTrCePhrfvdkUamXIribQ==",
- "license": "MIT",
- "dependencies": {
- "@tsparticles/engine": "3.9.1"
- }
+ "node_modules/@tweenjs/tween.js": {
+ "version": "23.1.3",
+ "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
+ "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==",
+ "license": "MIT"
},
- "node_modules/@tsparticles/interaction-external-connect": {
- "version": "3.9.1",
- "resolved": "https://registry.npmjs.org/@tsparticles/interaction-external-connect/-/interaction-external-connect-3.9.1.tgz",
- "integrity": "sha512-sq8YfUNsIORjXHzzW7/AJQtfi/qDqLnYG2qOSE1WOsog39MD30RzmiOloejOkfNeUdcGUcfsDgpUuL3UhzFUOA==",
+ "node_modules/@tybys/wasm-util": {
+ "version": "0.10.3",
+ "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz",
+ "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==",
"license": "MIT",
+ "optional": true,
"dependencies": {
- "@tsparticles/engine": "3.9.1"
+ "tslib": "^2.4.0"
}
},
- "node_modules/@tsparticles/interaction-external-grab": {
- "version": "3.9.1",
- "resolved": "https://registry.npmjs.org/@tsparticles/interaction-external-grab/-/interaction-external-grab-3.9.1.tgz",
- "integrity": "sha512-QwXza+sMMWDaMiFxd8y2tJwUK6c+nNw554+/9+tEZeTTk2fCbB0IJ7p/TH6ZGWDL0vo2muK54Njv2fEey191ow==",
+ "node_modules/@types/aria-query": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
+ "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
+ "dev": true,
"license": "MIT",
- "dependencies": {
- "@tsparticles/engine": "3.9.1"
- }
+ "peer": true
},
- "node_modules/@tsparticles/interaction-external-pause": {
- "version": "3.9.1",
- "resolved": "https://registry.npmjs.org/@tsparticles/interaction-external-pause/-/interaction-external-pause-3.9.1.tgz",
- "integrity": "sha512-Gzv4/FeNir0U/tVM9zQCqV1k+IAgaFjDU3T30M1AeAsNGh/rCITV2wnT7TOGFkbcla27m4Yxa+Fuab8+8pzm+g==",
+ "node_modules/@types/babel__core": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@tsparticles/engine": "3.9.1"
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
}
},
- "node_modules/@tsparticles/interaction-external-push": {
- "version": "3.9.1",
- "resolved": "https://registry.npmjs.org/@tsparticles/interaction-external-push/-/interaction-external-push-3.9.1.tgz",
- "integrity": "sha512-GvnWF9Qy4YkZdx+WJL2iy9IcgLvzOIu3K7aLYJFsQPaxT8d9TF8WlpoMlWKnJID6H5q4JqQuMRKRyWH8aAKyQw==",
+ "node_modules/@types/babel__generator": {
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@tsparticles/engine": "3.9.1"
+ "@babel/types": "^7.0.0"
}
},
- "node_modules/@tsparticles/interaction-external-remove": {
- "version": "3.9.1",
- "resolved": "https://registry.npmjs.org/@tsparticles/interaction-external-remove/-/interaction-external-remove-3.9.1.tgz",
- "integrity": "sha512-yPThm4UDWejDOWW5Qc8KnnS2EfSo5VFcJUQDWc1+Wcj17xe7vdSoiwwOORM0PmNBzdDpSKQrte/gUnoqaUMwOA==",
+ "node_modules/@types/babel__template": {
+ "version": "7.4.4",
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@tsparticles/engine": "3.9.1"
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
}
},
- "node_modules/@tsparticles/interaction-external-repulse": {
- "version": "3.9.1",
- "resolved": "https://registry.npmjs.org/@tsparticles/interaction-external-repulse/-/interaction-external-repulse-3.9.1.tgz",
- "integrity": "sha512-/LBppXkrMdvLHlEKWC7IykFhzrz+9nebT2fwSSFXK4plEBxDlIwnkDxd3FbVOAbnBvx4+L8+fbrEx+RvC8diAw==",
+ "node_modules/@types/babel__traverse": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@tsparticles/engine": "3.9.1"
+ "@babel/types": "^7.28.2"
}
},
- "node_modules/@tsparticles/interaction-external-slow": {
- "version": "3.9.1",
- "resolved": "https://registry.npmjs.org/@tsparticles/interaction-external-slow/-/interaction-external-slow-3.9.1.tgz",
- "integrity": "sha512-1ZYIR/udBwA9MdSCfgADsbDXKSFS0FMWuPWz7bm79g3sUxcYkihn+/hDhc6GXvNNR46V1ocJjrj0u6pAynS1KQ==",
+ "node_modules/@types/better-sqlite3": {
+ "version": "7.6.13",
+ "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz",
+ "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@tsparticles/engine": "3.9.1"
+ "@types/node": "*"
}
},
- "node_modules/@tsparticles/interaction-particles-attract": {
- "version": "3.9.1",
- "resolved": "https://registry.npmjs.org/@tsparticles/interaction-particles-attract/-/interaction-particles-attract-3.9.1.tgz",
- "integrity": "sha512-CYYYowJuGwRLUixQcSU/48PTKM8fCUYThe0hXwQ+yRMLAn053VHzL7NNZzKqEIeEyt5oJoy9KcvubjKWbzMBLQ==",
+ "node_modules/@types/cacheable-request": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz",
+ "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==",
"license": "MIT",
"dependencies": {
- "@tsparticles/engine": "3.9.1"
+ "@types/http-cache-semantics": "*",
+ "@types/keyv": "^3.1.4",
+ "@types/node": "*",
+ "@types/responselike": "^1.0.0"
}
},
- "node_modules/@tsparticles/interaction-particles-collisions": {
- "version": "3.9.1",
- "resolved": "https://registry.npmjs.org/@tsparticles/interaction-particles-collisions/-/interaction-particles-collisions-3.9.1.tgz",
- "integrity": "sha512-ggGyjW/3v1yxvYW1IF1EMT15M6w31y5zfNNUPkqd/IXRNPYvm0Z0ayhp+FKmz70M5p0UxxPIQHTvAv9Jqnuj8w==",
+ "node_modules/@types/chai": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
+ "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@tsparticles/engine": "3.9.1"
+ "@types/deep-eql": "*",
+ "assertion-error": "^2.0.1"
}
},
- "node_modules/@tsparticles/interaction-particles-links": {
- "version": "3.9.1",
- "resolved": "https://registry.npmjs.org/@tsparticles/interaction-particles-links/-/interaction-particles-links-3.9.1.tgz",
- "integrity": "sha512-MsLbMjy1vY5M5/hu/oa5OSRZAUz49H3+9EBMTIOThiX+a+vpl3sxc9AqNd9gMsPbM4WJlub8T6VBZdyvzez1Vg==",
+ "node_modules/@types/command-line-args": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/@types/command-line-args/-/command-line-args-5.2.3.tgz",
+ "integrity": "sha512-uv0aG6R0Y8WHZLTamZwtfsDLVRnOa+n+n5rEvFWL5Na5gZ8V2Teab/duDPFzIIIhs9qizDpcavCusCLJZu62Kw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/command-line-usage": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/@types/command-line-usage/-/command-line-usage-5.0.4.tgz",
+ "integrity": "sha512-BwR5KP3Es/CSht0xqBcUXS3qCAUVXwpRKsV2+arxeb65atasuXG9LykC9Ab10Cw3s2raH92ZqOeILaQbsB2ACg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/debug": {
+ "version": "4.1.12",
+ "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
+ "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==",
"license": "MIT",
"dependencies": {
- "@tsparticles/engine": "3.9.1"
+ "@types/ms": "*"
}
},
- "node_modules/@tsparticles/move-base": {
- "version": "3.9.1",
- "resolved": "https://registry.npmjs.org/@tsparticles/move-base/-/move-base-3.9.1.tgz",
- "integrity": "sha512-X4huBS27d8srpxwOxliWPUt+NtCwY+8q/cx1DvQxyqmTA8VFCGpcHNwtqiN+9JicgzOvSuaORVqUgwlsc7h4pQ==",
- "license": "MIT",
- "dependencies": {
- "@tsparticles/engine": "3.9.1"
- }
+ "node_modules/@types/deep-eql": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
+ "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
+ "dev": true,
+ "license": "MIT"
},
- "node_modules/@tsparticles/move-parallax": {
- "version": "3.9.1",
- "resolved": "https://registry.npmjs.org/@tsparticles/move-parallax/-/move-parallax-3.9.1.tgz",
- "integrity": "sha512-whlOR0bVeyh6J/hvxf/QM3DqvNnITMiAQ0kro6saqSDItAVqg4pYxBfEsSOKq7EhjxNvfhhqR+pFMhp06zoCVA==",
- "license": "MIT",
- "dependencies": {
- "@tsparticles/engine": "3.9.1"
- }
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "license": "MIT"
},
- "node_modules/@tsparticles/plugin-easing-quad": {
- "version": "3.9.1",
- "resolved": "https://registry.npmjs.org/@tsparticles/plugin-easing-quad/-/plugin-easing-quad-3.9.1.tgz",
- "integrity": "sha512-C2UJOca5MTDXKUTBXj30Kiqr5UyID+xrY/LxicVWWZPczQW2bBxbIbfq9ULvzGDwBTxE2rdvIB8YFKmDYO45qw==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/matteobruni"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/tsparticles"
- },
- {
- "type": "buymeacoffee",
- "url": "https://www.buymeacoffee.com/matteobruni"
- }
- ],
+ "node_modules/@types/estree-jsx": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz",
+ "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==",
"license": "MIT",
"dependencies": {
- "@tsparticles/engine": "3.9.1"
+ "@types/estree": "*"
}
},
- "node_modules/@tsparticles/plugin-hex-color": {
- "version": "3.9.1",
- "resolved": "https://registry.npmjs.org/@tsparticles/plugin-hex-color/-/plugin-hex-color-3.9.1.tgz",
- "integrity": "sha512-vZgZ12AjUicJvk7AX4K2eAmKEQX/D1VEjEPFhyjbgI7A65eX72M465vVKIgNA6QArLZ1DLs7Z787LOE6GOBWsg==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/matteobruni"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/tsparticles"
- },
- {
- "type": "buymeacoffee",
- "url": "https://www.buymeacoffee.com/matteobruni"
- }
- ],
+ "node_modules/@types/fs-extra": {
+ "version": "9.0.13",
+ "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz",
+ "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@tsparticles/engine": "3.9.1"
+ "@types/node": "*"
}
},
- "node_modules/@tsparticles/plugin-hsl-color": {
- "version": "3.9.1",
- "resolved": "https://registry.npmjs.org/@tsparticles/plugin-hsl-color/-/plugin-hsl-color-3.9.1.tgz",
- "integrity": "sha512-jJd1iGgRwX6eeNjc1zUXiJivaqC5UE+SC2A3/NtHwwoQrkfxGWmRHOsVyLnOBRcCPgBp/FpdDe6DIDjCMO715w==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/matteobruni"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/tsparticles"
- },
- {
- "type": "buymeacoffee",
- "url": "https://www.buymeacoffee.com/matteobruni"
- }
- ],
+ "node_modules/@types/hast": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
+ "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
"license": "MIT",
"dependencies": {
- "@tsparticles/engine": "3.9.1"
+ "@types/unist": "*"
}
},
- "node_modules/@tsparticles/plugin-rgb-color": {
- "version": "3.9.1",
- "resolved": "https://registry.npmjs.org/@tsparticles/plugin-rgb-color/-/plugin-rgb-color-3.9.1.tgz",
- "integrity": "sha512-SBxk7f1KBfXeTnnklbE2Hx4jBgh6I6HOtxb+Os1gTp0oaghZOkWcCD2dP4QbUu7fVNCMOcApPoMNC8RTFcy9wQ==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/matteobruni"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/tsparticles"
- },
- {
- "type": "buymeacoffee",
- "url": "https://www.buymeacoffee.com/matteobruni"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "@tsparticles/engine": "3.9.1"
- }
+ "node_modules/@types/http-cache-semantics": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz",
+ "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==",
+ "license": "MIT"
},
- "node_modules/@tsparticles/react": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/@tsparticles/react/-/react-3.0.0.tgz",
- "integrity": "sha512-hjGEtTT1cwv6BcjL+GcVgH++KYs52bIuQGW3PWv7z3tMa8g0bd6RI/vWSLj7p//NZ3uTjEIeilYIUPBh7Jfq/Q==",
- "peerDependencies": {
- "@tsparticles/engine": "^3.0.2",
- "react": ">=16.8.0",
- "react-dom": ">=16.8.0"
- }
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true,
+ "license": "MIT"
},
- "node_modules/@tsparticles/shape-circle": {
- "version": "3.9.1",
- "resolved": "https://registry.npmjs.org/@tsparticles/shape-circle/-/shape-circle-3.9.1.tgz",
- "integrity": "sha512-DqZFLjbuhVn99WJ+A9ajz9YON72RtCcvubzq6qfjFmtwAK7frvQeb6iDTp6Ze9FUipluxVZWVRG4vWTxi2B+/g==",
+ "node_modules/@types/keyv": {
+ "version": "3.1.4",
+ "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz",
+ "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==",
"license": "MIT",
"dependencies": {
- "@tsparticles/engine": "3.9.1"
+ "@types/node": "*"
}
},
- "node_modules/@tsparticles/shape-emoji": {
- "version": "3.9.1",
- "resolved": "https://registry.npmjs.org/@tsparticles/shape-emoji/-/shape-emoji-3.9.1.tgz",
- "integrity": "sha512-ifvY63usuT+hipgVHb8gelBHSeF6ryPnMxAAEC1RGHhhXfpSRWMtE6ybr+pSsYU52M3G9+TF84v91pSwNrb9ZQ==",
- "license": "MIT",
- "dependencies": {
- "@tsparticles/engine": "3.9.1"
- }
+ "node_modules/@types/long": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz",
+ "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==",
+ "license": "MIT"
},
- "node_modules/@tsparticles/shape-image": {
- "version": "3.9.1",
- "resolved": "https://registry.npmjs.org/@tsparticles/shape-image/-/shape-image-3.9.1.tgz",
- "integrity": "sha512-fCA5eme8VF3oX8yNVUA0l2SLDKuiZObkijb0z3Ky0qj1HUEVlAuEMhhNDNB9E2iELTrWEix9z7BFMePp2CC7AA==",
+ "node_modules/@types/mdast": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
+ "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==",
"license": "MIT",
"dependencies": {
- "@tsparticles/engine": "3.9.1"
+ "@types/unist": "*"
}
},
- "node_modules/@tsparticles/shape-line": {
- "version": "3.9.1",
- "resolved": "https://registry.npmjs.org/@tsparticles/shape-line/-/shape-line-3.9.1.tgz",
- "integrity": "sha512-wT8NSp0N9HURyV05f371cHKcNTNqr0/cwUu6WhBzbshkYGy1KZUP9CpRIh5FCrBpTev34mEQfOXDycgfG0KiLQ==",
+ "node_modules/@types/ms": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
+ "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "22.19.7",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.7.tgz",
+ "integrity": "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==",
"license": "MIT",
"dependencies": {
- "@tsparticles/engine": "3.9.1"
+ "undici-types": "~6.21.0"
}
},
- "node_modules/@tsparticles/shape-polygon": {
- "version": "3.9.1",
- "resolved": "https://registry.npmjs.org/@tsparticles/shape-polygon/-/shape-polygon-3.9.1.tgz",
- "integrity": "sha512-dA77PgZdoLwxnliH6XQM/zF0r4jhT01pw5y7XTeTqws++hg4rTLV9255k6R6eUqKq0FPSW1/WBsBIl7q/MmrqQ==",
+ "node_modules/@types/react": {
+ "version": "19.2.8",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.8.tgz",
+ "integrity": "sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg==",
"license": "MIT",
"dependencies": {
- "@tsparticles/engine": "3.9.1"
+ "csstype": "^3.2.2"
}
},
- "node_modules/@tsparticles/shape-square": {
- "version": "3.9.1",
- "resolved": "https://registry.npmjs.org/@tsparticles/shape-square/-/shape-square-3.9.1.tgz",
- "integrity": "sha512-DKGkDnRyZrAm7T2ipqNezJahSWs6xd9O5LQLe5vjrYm1qGwrFxJiQaAdlb00UNrexz1/SA7bEoIg4XKaFa7qhQ==",
+ "node_modules/@types/react-dom": {
+ "version": "19.2.3",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
+ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
+ "devOptional": true,
"license": "MIT",
- "dependencies": {
- "@tsparticles/engine": "3.9.1"
+ "peerDependencies": {
+ "@types/react": "^19.2.0"
}
},
- "node_modules/@tsparticles/shape-star": {
- "version": "3.9.1",
- "resolved": "https://registry.npmjs.org/@tsparticles/shape-star/-/shape-star-3.9.1.tgz",
- "integrity": "sha512-kdMJpi8cdeb6vGrZVSxTG0JIjCwIenggqk0EYeKAwtOGZFBgL7eHhF2F6uu1oq8cJAbXPujEoabnLsz6mW8XaA==",
+ "node_modules/@types/responselike": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz",
+ "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==",
"license": "MIT",
"dependencies": {
- "@tsparticles/engine": "3.9.1"
+ "@types/node": "*"
}
},
- "node_modules/@tsparticles/slim": {
- "version": "3.9.1",
- "resolved": "https://registry.npmjs.org/@tsparticles/slim/-/slim-3.9.1.tgz",
- "integrity": "sha512-CL5cDmADU7sDjRli0So+hY61VMbdroqbArmR9Av+c1Fisa5ytr6QD7Jv62iwU2S6rvgicEe9OyRmSy5GIefwZw==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/matteobruni"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/tsparticles"
- },
- {
- "type": "buymeacoffee",
- "url": "https://www.buymeacoffee.com/matteobruni"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "@tsparticles/basic": "3.9.1",
- "@tsparticles/engine": "3.9.1",
- "@tsparticles/interaction-external-attract": "3.9.1",
- "@tsparticles/interaction-external-bounce": "3.9.1",
- "@tsparticles/interaction-external-bubble": "3.9.1",
- "@tsparticles/interaction-external-connect": "3.9.1",
- "@tsparticles/interaction-external-grab": "3.9.1",
- "@tsparticles/interaction-external-pause": "3.9.1",
- "@tsparticles/interaction-external-push": "3.9.1",
- "@tsparticles/interaction-external-remove": "3.9.1",
- "@tsparticles/interaction-external-repulse": "3.9.1",
- "@tsparticles/interaction-external-slow": "3.9.1",
- "@tsparticles/interaction-particles-attract": "3.9.1",
- "@tsparticles/interaction-particles-collisions": "3.9.1",
- "@tsparticles/interaction-particles-links": "3.9.1",
- "@tsparticles/move-parallax": "3.9.1",
- "@tsparticles/plugin-easing-quad": "3.9.1",
- "@tsparticles/shape-emoji": "3.9.1",
- "@tsparticles/shape-image": "3.9.1",
- "@tsparticles/shape-line": "3.9.1",
- "@tsparticles/shape-polygon": "3.9.1",
- "@tsparticles/shape-square": "3.9.1",
- "@tsparticles/shape-star": "3.9.1",
- "@tsparticles/updater-life": "3.9.1",
- "@tsparticles/updater-rotate": "3.9.1",
- "@tsparticles/updater-stroke-color": "3.9.1"
- }
- },
- "node_modules/@tsparticles/updater-color": {
- "version": "3.9.1",
- "resolved": "https://registry.npmjs.org/@tsparticles/updater-color/-/updater-color-3.9.1.tgz",
- "integrity": "sha512-XGWdscrgEMA8L5E7exsE0f8/2zHKIqnTrZymcyuFBw2DCB6BIV+5z6qaNStpxrhq3DbIxxhqqcybqeOo7+Alpg==",
+ "node_modules/@types/stats.js": {
+ "version": "0.17.4",
+ "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz",
+ "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/three": {
+ "version": "0.182.0",
+ "resolved": "https://registry.npmjs.org/@types/three/-/three-0.182.0.tgz",
+ "integrity": "sha512-WByN9V3Sbwbe2OkWuSGyoqQO8Du6yhYaXtXLoA5FkKTUJorZ+yOHBZ35zUUPQXlAKABZmbYp5oAqpA4RBjtJ/Q==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@tsparticles/engine": "3.9.1"
+ "@dimforge/rapier3d-compat": "~0.12.0",
+ "@tweenjs/tween.js": "~23.1.3",
+ "@types/stats.js": "*",
+ "@types/webxr": ">=0.5.17",
+ "@webgpu/types": "*",
+ "fflate": "~0.8.2",
+ "meshoptimizer": "~0.22.0"
}
},
- "node_modules/@tsparticles/updater-life": {
- "version": "3.9.1",
- "resolved": "https://registry.npmjs.org/@tsparticles/updater-life/-/updater-life-3.9.1.tgz",
- "integrity": "sha512-Oi8aF2RIwMMsjssUkCB6t3PRpENHjdZf6cX92WNfAuqXtQphr3OMAkYFJFWkvyPFK22AVy3p/cFt6KE5zXxwAA==",
+ "node_modules/@types/unist": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
+ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
+ "license": "MIT"
+ },
+ "node_modules/@types/webxr": {
+ "version": "0.5.24",
+ "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz",
+ "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/yauzl": {
+ "version": "2.10.3",
+ "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
+ "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==",
"license": "MIT",
+ "optional": true,
"dependencies": {
- "@tsparticles/engine": "3.9.1"
+ "@types/node": "*"
}
},
- "node_modules/@tsparticles/updater-opacity": {
- "version": "3.9.1",
- "resolved": "https://registry.npmjs.org/@tsparticles/updater-opacity/-/updater-opacity-3.9.1.tgz",
- "integrity": "sha512-w778LQuRZJ+IoWzeRdrGykPYSSaTeWfBvLZ2XwYEkh/Ss961InOxZKIpcS6i5Kp/Zfw0fS1ZAuqeHwuj///Osw==",
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "8.63.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.63.0.tgz",
+ "integrity": "sha512-rvwSgqT+DHpWdzfSzPatRLm02a0GlESt++9iy3hLCDY4BgkaLcl8LBi9Yh7XGFBpwcBE/K3024QuXWTpbz4FfQ==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@tsparticles/engine": "3.9.1"
+ "@eslint-community/regexpp": "^4.12.2",
+ "@typescript-eslint/scope-manager": "8.63.0",
+ "@typescript-eslint/type-utils": "8.63.0",
+ "@typescript-eslint/utils": "8.63.0",
+ "@typescript-eslint/visitor-keys": "8.63.0",
+ "ignore": "^7.0.5",
+ "natural-compare": "^1.4.0",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^8.63.0",
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
}
},
- "node_modules/@tsparticles/updater-out-modes": {
- "version": "3.9.1",
- "resolved": "https://registry.npmjs.org/@tsparticles/updater-out-modes/-/updater-out-modes-3.9.1.tgz",
- "integrity": "sha512-cKQEkAwbru+hhKF+GTsfbOvuBbx2DSB25CxOdhtW2wRvDBoCnngNdLw91rs+0Cex4tgEeibkebrIKFDDE6kELg==",
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
+ "version": "7.0.5",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
+ "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
+ "dev": true,
"license": "MIT",
- "dependencies": {
- "@tsparticles/engine": "3.9.1"
+ "engines": {
+ "node": ">= 4"
}
},
- "node_modules/@tsparticles/updater-rotate": {
- "version": "3.9.1",
- "resolved": "https://registry.npmjs.org/@tsparticles/updater-rotate/-/updater-rotate-3.9.1.tgz",
- "integrity": "sha512-9BfKaGfp28JN82MF2qs6Ae/lJr9EColMfMTHqSKljblwbpVDHte4umuwKl3VjbRt87WD9MGtla66NTUYl+WxuQ==",
+ "node_modules/@typescript-eslint/parser": {
+ "version": "8.63.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.63.0.tgz",
+ "integrity": "sha512-gwh4gvvlaVDKKxyfxMG+Gnu1u9X0OQBwyGLkbwB65dIzBKnxeRiJlNFqlI3zwVhNXJIs6qV7mlFCn/BIajlVig==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@tsparticles/engine": "3.9.1"
+ "@typescript-eslint/scope-manager": "8.63.0",
+ "@typescript-eslint/types": "8.63.0",
+ "@typescript-eslint/typescript-estree": "8.63.0",
+ "@typescript-eslint/visitor-keys": "8.63.0",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
}
},
- "node_modules/@tsparticles/updater-size": {
- "version": "3.9.1",
- "resolved": "https://registry.npmjs.org/@tsparticles/updater-size/-/updater-size-3.9.1.tgz",
- "integrity": "sha512-3NSVs0O2ApNKZXfd+y/zNhTXSFeG1Pw4peI8e6z/q5+XLbmue9oiEwoPy/tQLaark3oNj3JU7Q903ZijPyXSzw==",
+ "node_modules/@typescript-eslint/project-service": {
+ "version": "8.63.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.63.0.tgz",
+ "integrity": "sha512-e5dh0/UI0ok53AlZ5wRkXCB32z/f2jUZqPR/ygAw5WYaSw8j9EoJWlS7wQjr/dmOaqWjnPIn2m+HhVPCMWGZVQ==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@tsparticles/engine": "3.9.1"
+ "@typescript-eslint/tsconfig-utils": "^8.63.0",
+ "@typescript-eslint/types": "^8.63.0",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
}
},
- "node_modules/@tsparticles/updater-stroke-color": {
- "version": "3.9.1",
- "resolved": "https://registry.npmjs.org/@tsparticles/updater-stroke-color/-/updater-stroke-color-3.9.1.tgz",
- "integrity": "sha512-3x14+C2is9pZYTg9T2TiA/aM1YMq4wLdYaZDcHm3qO30DZu5oeQq0rm/6w+QOGKYY1Z3Htg9rlSUZkhTHn7eDA==",
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "8.63.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.63.0.tgz",
+ "integrity": "sha512-uUyfMWCnDSN8bCpcrY8nGP2BLkQ9Xn0GsipcONcpIDWhwhO4ZSyHvyS14U3X75mzxWxL3I2UZIrenTzdzcJO8A==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@tsparticles/engine": "3.9.1"
+ "@typescript-eslint/types": "8.63.0",
+ "@typescript-eslint/visitor-keys": "8.63.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
}
},
- "node_modules/@tweenjs/tween.js": {
- "version": "23.1.3",
- "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
- "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==",
- "license": "MIT"
- },
- "node_modules/@types/aws-lambda": {
- "version": "8.10.159",
- "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.159.tgz",
- "integrity": "sha512-SAP22WSGNN12OQ8PlCzGzRCZ7QDCwI85dQZbmpz7+mAk+L7j+wI7qnvmdKh+o7A5LaOp6QnOZ2NJphAZQTTHQg==",
- "license": "MIT"
- },
- "node_modules/@types/babel__core": {
- "version": "7.20.5",
- "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
- "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+ "node_modules/@typescript-eslint/tsconfig-utils": {
+ "version": "8.63.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.63.0.tgz",
+ "integrity": "sha512-sUAbkulqBAsncKnbRP3+7CtQFRKicexnj7ZwNC6ddCR7EmrXvjvdCYMJbUIqMd6lwoEriZjwLo08aS5tSjVMHg==",
"dev": true,
"license": "MIT",
- "dependencies": {
- "@babel/parser": "^7.20.7",
- "@babel/types": "^7.20.7",
- "@types/babel__generator": "*",
- "@types/babel__template": "*",
- "@types/babel__traverse": "*"
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
}
},
- "node_modules/@types/babel__generator": {
- "version": "7.27.0",
- "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
- "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "8.63.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.63.0.tgz",
+ "integrity": "sha512-Nzzh/OGxVCOjObjaj1CQF2RUasyYy2Jfuh+zZ3PjLzG2fYRriAiZLib9UKtO+CpQAS3YHiAS+ckZDclwqI1TPA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/types": "^7.0.0"
+ "@typescript-eslint/types": "8.63.0",
+ "@typescript-eslint/typescript-estree": "8.63.0",
+ "@typescript-eslint/utils": "8.63.0",
+ "debug": "^4.4.3",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
}
},
- "node_modules/@types/babel__template": {
- "version": "7.4.4",
- "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
- "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+ "node_modules/@typescript-eslint/types": {
+ "version": "8.63.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.63.0.tgz",
+ "integrity": "sha512-xyLtl9DUBBFrcJS4x2pIqGLH68/tC2uOa4Z7pUteW09D3bXnnXUom4dyPikzWgB7llmIc1zoeI3aoUdC4rPK/Q==",
"dev": true,
"license": "MIT",
- "dependencies": {
- "@babel/parser": "^7.1.0",
- "@babel/types": "^7.0.0"
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
}
},
- "node_modules/@types/babel__traverse": {
- "version": "7.28.0",
- "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
- "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "8.63.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.63.0.tgz",
+ "integrity": "sha512-ygBkU+B7ex5UI/gKhaqexWev79uISfIv7XQCRNYO/jmD8rGLPyWLAb3KMRT6nd8Gt9bmUBi9+iX6tBdYfOY81Q==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/types": "^7.28.2"
+ "@typescript-eslint/project-service": "8.63.0",
+ "@typescript-eslint/tsconfig-utils": "8.63.0",
+ "@typescript-eslint/types": "8.63.0",
+ "@typescript-eslint/visitor-keys": "8.63.0",
+ "debug": "^4.4.3",
+ "minimatch": "^10.2.2",
+ "semver": "^7.7.3",
+ "tinyglobby": "^0.2.15",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
}
},
- "node_modules/@types/better-sqlite3": {
- "version": "7.6.13",
- "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz",
- "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==",
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
+ "version": "7.8.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
+ "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
"dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/node": "*"
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
}
},
- "node_modules/@types/cacheable-request": {
- "version": "6.0.3",
- "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz",
- "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==",
+ "node_modules/@typescript-eslint/utils": {
+ "version": "8.63.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.63.0.tgz",
+ "integrity": "sha512-fUKaeAvrTuQg/Tgt3nliAUSZHJM6DlCcfyEmxCvlX8kieWSStBX+5O5Fnidtc3i2JrH+9c/GL4RY2iasd/GPTA==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@types/http-cache-semantics": "*",
- "@types/keyv": "^3.1.4",
- "@types/node": "*",
- "@types/responselike": "^1.0.0"
+ "@eslint-community/eslint-utils": "^4.9.1",
+ "@typescript-eslint/scope-manager": "8.63.0",
+ "@typescript-eslint/types": "8.63.0",
+ "@typescript-eslint/typescript-estree": "8.63.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
}
},
- "node_modules/@types/chai": {
- "version": "5.2.3",
- "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
- "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "8.63.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.63.0.tgz",
+ "integrity": "sha512-UexrHGnGTpbuQHct2ExOc2ZcFbGUS9FOesCxxqdBGcpI1BxYu/LZ6U8Aq6/72XtF/qRBk9nhuGHFJIXXMhPMdw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@types/deep-eql": "*",
- "assertion-error": "^2.0.1"
+ "@typescript-eslint/types": "8.63.0",
+ "eslint-visitor-keys": "^5.0.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
}
},
- "node_modules/@types/command-line-args": {
- "version": "5.2.3",
- "resolved": "https://registry.npmjs.org/@types/command-line-args/-/command-line-args-5.2.3.tgz",
- "integrity": "sha512-uv0aG6R0Y8WHZLTamZwtfsDLVRnOa+n+n5rEvFWL5Na5gZ8V2Teab/duDPFzIIIhs9qizDpcavCusCLJZu62Kw==",
- "license": "MIT"
+ "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
+ "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
},
- "node_modules/@types/command-line-usage": {
- "version": "5.0.4",
- "resolved": "https://registry.npmjs.org/@types/command-line-usage/-/command-line-usage-5.0.4.tgz",
- "integrity": "sha512-BwR5KP3Es/CSht0xqBcUXS3qCAUVXwpRKsV2+arxeb65atasuXG9LykC9Ab10Cw3s2raH92ZqOeILaQbsB2ACg==",
- "license": "MIT"
+ "node_modules/@ungap/structured-clone": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
+ "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
+ "license": "ISC"
},
- "node_modules/@types/debug": {
- "version": "4.1.12",
- "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
- "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==",
+ "node_modules/@vitejs/plugin-react": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.2.tgz",
+ "integrity": "sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@types/ms": "*"
+ "@babel/core": "^7.28.5",
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
+ "@rolldown/pluginutils": "1.0.0-beta.53",
+ "@types/babel__core": "^7.20.5",
+ "react-refresh": "^0.18.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "peerDependencies": {
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
}
},
- "node_modules/@types/deep-eql": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
- "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
+ "node_modules/@vitest/coverage-v8": {
+ "version": "4.1.10",
+ "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.10.tgz",
+ "integrity": "sha512-IM49HmthevbgAO4anp1hwtoT9wYe59w0LR00gr+eagHE+ZJ5lK4sLPeO0ubgoJcwLk6dehU3R24N+FbEEKDc8g==",
"dev": true,
- "license": "MIT"
- },
- "node_modules/@types/estree": {
- "version": "1.0.8",
- "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
- "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
- "license": "MIT"
- },
- "node_modules/@types/estree-jsx": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz",
- "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==",
"license": "MIT",
"dependencies": {
- "@types/estree": "*"
+ "@bcoe/v8-coverage": "^1.0.2",
+ "@vitest/utils": "4.1.10",
+ "ast-v8-to-istanbul": "^1.0.0",
+ "istanbul-lib-coverage": "^3.2.2",
+ "istanbul-lib-report": "^3.0.1",
+ "istanbul-reports": "^3.2.0",
+ "magicast": "^0.5.2",
+ "obug": "^2.1.1",
+ "std-env": "^4.0.0-rc.1",
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@vitest/browser": "4.1.10",
+ "vitest": "4.1.10"
+ },
+ "peerDependenciesMeta": {
+ "@vitest/browser": {
+ "optional": true
+ }
}
},
- "node_modules/@types/fs-extra": {
- "version": "9.0.13",
- "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz",
- "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==",
+ "node_modules/@vitest/expect": {
+ "version": "4.1.10",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz",
+ "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@types/node": "*"
+ "@standard-schema/spec": "^1.1.0",
+ "@types/chai": "^5.2.2",
+ "@vitest/spy": "4.1.10",
+ "@vitest/utils": "4.1.10",
+ "chai": "^6.2.2",
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
}
},
- "node_modules/@types/hast": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
- "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
+ "node_modules/@vitest/mocker": {
+ "version": "4.1.10",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz",
+ "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@types/unist": "*"
- }
- },
- "node_modules/@types/http-cache-semantics": {
- "version": "4.0.4",
- "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz",
- "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==",
- "license": "MIT"
- },
- "node_modules/@types/json-schema": {
- "version": "7.0.15",
- "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
- "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
- "license": "MIT"
- },
- "node_modules/@types/keyv": {
- "version": "3.1.4",
- "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz",
- "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==",
- "license": "MIT",
- "dependencies": {
- "@types/node": "*"
- }
- },
- "node_modules/@types/long": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz",
- "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==",
- "license": "MIT"
- },
- "node_modules/@types/mdast": {
- "version": "4.0.4",
- "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
- "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==",
- "license": "MIT",
- "dependencies": {
- "@types/unist": "*"
+ "@vitest/spy": "4.1.10",
+ "estree-walker": "^3.0.3",
+ "magic-string": "^0.30.21"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "msw": "^2.4.9",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "msw": {
+ "optional": true
+ },
+ "vite": {
+ "optional": true
+ }
}
},
- "node_modules/@types/ms": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
- "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
- "license": "MIT"
- },
- "node_modules/@types/node": {
- "version": "22.19.7",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.7.tgz",
- "integrity": "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==",
+ "node_modules/@vitest/pretty-format": {
+ "version": "4.1.10",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz",
+ "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "undici-types": "~6.21.0"
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
}
},
- "node_modules/@types/plist": {
- "version": "3.0.5",
- "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz",
- "integrity": "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==",
+ "node_modules/@vitest/runner": {
+ "version": "4.1.10",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz",
+ "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==",
"dev": true,
"license": "MIT",
- "optional": true,
"dependencies": {
- "@types/node": "*",
- "xmlbuilder": ">=11.0.1"
+ "@vitest/utils": "4.1.10",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
}
},
- "node_modules/@types/react": {
- "version": "19.2.8",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.8.tgz",
- "integrity": "sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg==",
+ "node_modules/@vitest/snapshot": {
+ "version": "4.1.10",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz",
+ "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "csstype": "^3.2.2"
- }
- },
- "node_modules/@types/react-dom": {
- "version": "19.2.3",
- "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
- "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
- "devOptional": true,
- "license": "MIT",
- "peerDependencies": {
- "@types/react": "^19.2.0"
+ "@vitest/pretty-format": "4.1.10",
+ "@vitest/utils": "4.1.10",
+ "magic-string": "^0.30.21",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
}
},
- "node_modules/@types/react-reconciler": {
- "version": "0.28.9",
- "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.9.tgz",
- "integrity": "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==",
+ "node_modules/@vitest/spy": {
+ "version": "4.1.10",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz",
+ "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==",
+ "dev": true,
"license": "MIT",
- "peerDependencies": {
- "@types/react": "*"
+ "funding": {
+ "url": "https://opencollective.com/vitest"
}
},
- "node_modules/@types/responselike": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz",
- "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==",
+ "node_modules/@vitest/utils": {
+ "version": "4.1.10",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz",
+ "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@types/node": "*"
+ "@vitest/pretty-format": "4.1.10",
+ "convert-source-map": "^2.0.0",
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
}
},
- "node_modules/@types/stats.js": {
- "version": "0.17.4",
- "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz",
- "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==",
+ "node_modules/@webgpu/types": {
+ "version": "0.1.69",
+ "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.69.tgz",
+ "integrity": "sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ==",
"dev": true,
- "license": "MIT"
+ "license": "BSD-3-Clause"
},
- "node_modules/@types/three": {
- "version": "0.182.0",
- "resolved": "https://registry.npmjs.org/@types/three/-/three-0.182.0.tgz",
- "integrity": "sha512-WByN9V3Sbwbe2OkWuSGyoqQO8Du6yhYaXtXLoA5FkKTUJorZ+yOHBZ35zUUPQXlAKABZmbYp5oAqpA4RBjtJ/Q==",
- "dev": true,
- "license": "MIT",
+ "node_modules/@xenova/transformers": {
+ "version": "2.17.2",
+ "resolved": "https://registry.npmjs.org/@xenova/transformers/-/transformers-2.17.2.tgz",
+ "integrity": "sha512-lZmHqzrVIkSvZdKZEx7IYY51TK0WDrC8eR0c5IMnBsO8di8are1zzw8BlLhyO2TklZKLN5UffNGs1IJwT6oOqQ==",
+ "license": "Apache-2.0",
"dependencies": {
- "@dimforge/rapier3d-compat": "~0.12.0",
- "@tweenjs/tween.js": "~23.1.3",
- "@types/stats.js": "*",
- "@types/webxr": ">=0.5.17",
- "@webgpu/types": "*",
- "fflate": "~0.8.2",
- "meshoptimizer": "~0.22.0"
+ "@huggingface/jinja": "^0.2.2",
+ "onnxruntime-web": "1.14.0",
+ "sharp": "^0.32.0"
+ },
+ "optionalDependencies": {
+ "onnxruntime-node": "1.14.0"
}
},
- "node_modules/@types/trusted-types": {
- "version": "2.0.7",
- "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
- "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
- "license": "MIT",
- "optional": true
- },
- "node_modules/@types/unist": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
- "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
- "license": "MIT"
- },
- "node_modules/@types/verror": {
- "version": "1.10.11",
- "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz",
- "integrity": "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==",
- "dev": true,
- "license": "MIT",
- "optional": true
- },
- "node_modules/@types/webxr": {
- "version": "0.5.24",
- "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz",
- "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==",
- "dev": true,
+ "node_modules/@xenova/transformers/node_modules/node-addon-api": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz",
+ "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==",
"license": "MIT"
},
- "node_modules/@types/yauzl": {
- "version": "2.10.3",
- "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
- "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==",
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "@types/node": "*"
+ "node_modules/@xenova/transformers/node_modules/semver": {
+ "version": "7.8.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
+ "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
}
},
- "node_modules/@typescript-eslint/eslint-plugin": {
- "version": "8.53.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.53.0.tgz",
- "integrity": "sha512-eEXsVvLPu8Z4PkFibtuFJLJOTAV/nPdgtSjkGoPpddpFk3/ym2oy97jynY6ic2m6+nc5M8SE1e9v/mHKsulcJg==",
- "dev": true,
- "license": "MIT",
+ "node_modules/@xenova/transformers/node_modules/sharp": {
+ "version": "0.32.6",
+ "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.32.6.tgz",
+ "integrity": "sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==",
+ "hasInstallScript": true,
+ "license": "Apache-2.0",
"dependencies": {
- "@eslint-community/regexpp": "^4.12.2",
- "@typescript-eslint/scope-manager": "8.53.0",
- "@typescript-eslint/type-utils": "8.53.0",
- "@typescript-eslint/utils": "8.53.0",
- "@typescript-eslint/visitor-keys": "8.53.0",
- "ignore": "^7.0.5",
- "natural-compare": "^1.4.0",
- "ts-api-utils": "^2.4.0"
+ "color": "^4.2.3",
+ "detect-libc": "^2.0.2",
+ "node-addon-api": "^6.1.0",
+ "prebuild-install": "^7.1.1",
+ "semver": "^7.5.4",
+ "simple-get": "^4.0.1",
+ "tar-fs": "^3.0.4",
+ "tunnel-agent": "^0.6.0"
},
"engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ "node": ">=14.15.0"
},
"funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- },
- "peerDependencies": {
- "@typescript-eslint/parser": "^8.53.0",
- "eslint": "^8.57.0 || ^9.0.0",
- "typescript": ">=4.8.4 <6.0.0"
+ "url": "https://opencollective.com/libvips"
}
},
- "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
- "version": "7.0.5",
- "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
- "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
- "dev": true,
+ "node_modules/@xenova/transformers/node_modules/tar-fs": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.2.tgz",
+ "integrity": "sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw==",
"license": "MIT",
- "engines": {
- "node": ">= 4"
+ "dependencies": {
+ "pump": "^3.0.0",
+ "tar-stream": "^3.1.5"
+ },
+ "optionalDependencies": {
+ "bare-fs": "^4.0.1",
+ "bare-path": "^3.0.0"
}
},
- "node_modules/@typescript-eslint/parser": {
- "version": "8.53.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.53.0.tgz",
- "integrity": "sha512-npiaib8XzbjtzS2N4HlqPvlpxpmZ14FjSJrteZpPxGUaYPlvhzlzUZ4mZyABo0EFrOWnvyd0Xxroq//hKhtAWg==",
- "dev": true,
+ "node_modules/@xenova/transformers/node_modules/tar-stream": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.0.tgz",
+ "integrity": "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==",
"license": "MIT",
"dependencies": {
- "@typescript-eslint/scope-manager": "8.53.0",
- "@typescript-eslint/types": "8.53.0",
- "@typescript-eslint/typescript-estree": "8.53.0",
- "@typescript-eslint/visitor-keys": "8.53.0",
- "debug": "^4.4.3"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- },
- "peerDependencies": {
- "eslint": "^8.57.0 || ^9.0.0",
- "typescript": ">=4.8.4 <6.0.0"
+ "b4a": "^1.6.4",
+ "bare-fs": "^4.5.5",
+ "fast-fifo": "^1.2.0",
+ "streamx": "^2.15.0"
}
},
- "node_modules/@typescript-eslint/project-service": {
- "version": "8.53.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.53.0.tgz",
- "integrity": "sha512-Bl6Gdr7NqkqIP5yP9z1JU///Nmes4Eose6L1HwpuVHwScgDPPuEWbUVhvlZmb8hy0vX9syLk5EGNL700WcBlbg==",
- "dev": true,
+ "node_modules/@xmldom/xmldom": {
+ "version": "0.8.11",
+ "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz",
+ "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==",
"license": "MIT",
- "dependencies": {
- "@typescript-eslint/tsconfig-utils": "^8.53.0",
- "@typescript-eslint/types": "^8.53.0",
- "debug": "^4.4.3"
- },
"engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- },
- "peerDependencies": {
- "typescript": ">=4.8.4 <6.0.0"
+ "node": ">=10.0.0"
}
},
- "node_modules/@typescript-eslint/scope-manager": {
- "version": "8.53.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.53.0.tgz",
- "integrity": "sha512-kWNj3l01eOGSdVBnfAF2K1BTh06WS0Yet6JUgb9Cmkqaz3Jlu0fdVUjj9UI8gPidBWSMqDIglmEXifSgDT/D0g==",
- "dev": true,
+ "node_modules/3d-force-graph": {
+ "version": "1.79.0",
+ "resolved": "https://registry.npmjs.org/3d-force-graph/-/3d-force-graph-1.79.0.tgz",
+ "integrity": "sha512-0RUNcfiH12f93loY/iS4wShzhXzdLLN4futvFnintF7eP30DjX+nAdLDAGOZwSflhijQyVwnGtpczNjFrDLUzQ==",
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.53.0",
- "@typescript-eslint/visitor-keys": "8.53.0"
+ "accessor-fn": "1",
+ "kapsule": "^1.16",
+ "three": ">=0.118 <1",
+ "three-forcegraph": "1",
+ "three-render-objects": "^1.35"
},
"engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
+ "node": ">=12"
}
},
- "node_modules/@typescript-eslint/tsconfig-utils": {
- "version": "8.53.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.53.0.tgz",
- "integrity": "sha512-K6Sc0R5GIG6dNoPdOooQ+KtvT5KCKAvTcY8h2rIuul19vxH5OTQk7ArKkd4yTzkw66WnNY0kPPzzcmWA+XRmiA==",
+ "node_modules/abbrev": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz",
+ "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==",
"dev": true,
- "license": "MIT",
+ "license": "ISC",
"engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- },
- "peerDependencies": {
- "typescript": ">=4.8.4 <6.0.0"
+ "node": "^20.17.0 || >=22.9.0"
}
},
- "node_modules/@typescript-eslint/type-utils": {
- "version": "8.53.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.53.0.tgz",
- "integrity": "sha512-BBAUhlx7g4SmcLhn8cnbxoxtmS7hcq39xKCgiutL3oNx1TaIp+cny51s8ewnKMpVUKQUGb41RAUWZ9kxYdovuw==",
- "dev": true,
+ "node_modules/accepts": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
+ "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.53.0",
- "@typescript-eslint/typescript-estree": "8.53.0",
- "@typescript-eslint/utils": "8.53.0",
- "debug": "^4.4.3",
- "ts-api-utils": "^2.4.0"
+ "mime-types": "^3.0.0",
+ "negotiator": "^1.0.0"
},
"engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- },
- "peerDependencies": {
- "eslint": "^8.57.0 || ^9.0.0",
- "typescript": ">=4.8.4 <6.0.0"
+ "node": ">= 0.6"
}
},
- "node_modules/@typescript-eslint/types": {
- "version": "8.53.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.53.0.tgz",
- "integrity": "sha512-Bmh9KX31Vlxa13+PqPvt4RzKRN1XORYSLlAE+sO1i28NkisGbTtSLFVB3l7PWdHtR3E0mVMuC7JilWJ99m2HxQ==",
- "dev": true,
+ "node_modules/accepts/node_modules/mime-db": {
+ "version": "1.54.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
"license": "MIT",
"engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
+ "node": ">= 0.6"
}
},
- "node_modules/@typescript-eslint/typescript-estree": {
- "version": "8.53.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.53.0.tgz",
- "integrity": "sha512-pw0c0Gdo7Z4xOG987u3nJ8akL9093yEEKv8QTJ+Bhkghj1xyj8cgPaavlr9rq8h7+s6plUJ4QJYw2gCZodqmGw==",
- "dev": true,
+ "node_modules/accepts/node_modules/mime-types": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
+ "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
"license": "MIT",
"dependencies": {
- "@typescript-eslint/project-service": "8.53.0",
- "@typescript-eslint/tsconfig-utils": "8.53.0",
- "@typescript-eslint/types": "8.53.0",
- "@typescript-eslint/visitor-keys": "8.53.0",
- "debug": "^4.4.3",
- "minimatch": "^9.0.5",
- "semver": "^7.7.3",
- "tinyglobby": "^0.2.15",
- "ts-api-utils": "^2.4.0"
+ "mime-db": "^1.54.0"
},
"engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ "node": ">=18"
},
"funding": {
"type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- },
- "peerDependencies": {
- "typescript": ">=4.8.4 <6.0.0"
+ "url": "https://opencollective.com/express"
}
},
- "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
- "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
- "dev": true,
+ "node_modules/accessor-fn": {
+ "version": "1.5.3",
+ "resolved": "https://registry.npmjs.org/accessor-fn/-/accessor-fn-1.5.3.tgz",
+ "integrity": "sha512-rkAofCwe/FvYFUlMB0v0gWmhqtfAtV1IUkdPbfhTUyYniu5LrC0A0UJkTH0Jv3S8SvwkmfuAlY+mQIJATdocMA==",
"license": "MIT",
- "dependencies": {
- "balanced-match": "^1.0.0"
+ "engines": {
+ "node": ">=12"
}
},
- "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
- "version": "9.0.5",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
- "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "node_modules/acorn": {
+ "version": "8.17.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz",
+ "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==",
"dev": true,
- "license": "ISC",
- "dependencies": {
- "brace-expansion": "^2.0.1"
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
},
"engines": {
- "node": ">=16 || 14 >=14.17"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
+ "node": ">=0.4.0"
}
},
- "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
- "version": "7.7.3",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
- "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
"dev": true,
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
- },
- "engines": {
- "node": ">=10"
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
- "node_modules/@typescript-eslint/utils": {
- "version": "8.53.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.53.0.tgz",
- "integrity": "sha512-XDY4mXTez3Z1iRDI5mbRhH4DFSt46oaIFsLg+Zn97+sYrXACziXSQcSelMybnVZ5pa1P6xYkPr5cMJyunM1ZDA==",
+ "node_modules/acorn-jsx-walk": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/acorn-jsx-walk/-/acorn-jsx-walk-2.0.0.tgz",
+ "integrity": "sha512-uuo6iJj4D4ygkdzd6jPtcxs8vZgDX9YFIkqczGImoypX2fQ4dVImmu3UzA4ynixCIMTrEOWW+95M2HuBaCEOVA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/acorn-loose": {
+ "version": "8.5.2",
+ "resolved": "https://registry.npmjs.org/acorn-loose/-/acorn-loose-8.5.2.tgz",
+ "integrity": "sha512-PPvV6g8UGMGgjrMu+n/f9E/tCSkNQ2Y97eFvuVdJfG11+xdIeDcLyNdC8SHcrHbRqkfwLASdplyR6B6sKM1U4A==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@eslint-community/eslint-utils": "^4.9.1",
- "@typescript-eslint/scope-manager": "8.53.0",
- "@typescript-eslint/types": "8.53.0",
- "@typescript-eslint/typescript-estree": "8.53.0"
+ "acorn": "^8.15.0"
},
"engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- },
- "peerDependencies": {
- "eslint": "^8.57.0 || ^9.0.0",
- "typescript": ">=4.8.4 <6.0.0"
+ "node": ">=0.4.0"
}
},
- "node_modules/@typescript-eslint/visitor-keys": {
- "version": "8.53.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.53.0.tgz",
- "integrity": "sha512-LZ2NqIHFhvFwxG0qZeLL9DvdNAHPGCY5dIRwBhyYeU+LfLhcStE1ImjsuTG/WaVh3XysGaeLW8Rqq7cGkPCFvw==",
+ "node_modules/acorn-walk": {
+ "version": "8.3.5",
+ "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz",
+ "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.53.0",
- "eslint-visitor-keys": "^4.2.1"
+ "acorn": "^8.11.0"
},
"engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
+ "node": ">=0.4.0"
}
},
- "node_modules/@ungap/structured-clone": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
- "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
- "license": "ISC"
+ "node_modules/agent-base": {
+ "version": "7.1.4",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
+ "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
+ "devOptional": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14"
+ }
},
- "node_modules/@vitejs/plugin-react": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.2.tgz",
- "integrity": "sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==",
- "dev": true,
+ "node_modules/aggregate-error": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
+ "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==",
"license": "MIT",
+ "optional": true,
"dependencies": {
- "@babel/core": "^7.28.5",
- "@babel/plugin-transform-react-jsx-self": "^7.27.1",
- "@babel/plugin-transform-react-jsx-source": "^7.27.1",
- "@rolldown/pluginutils": "1.0.0-beta.53",
- "@types/babel__core": "^7.20.5",
- "react-refresh": "^0.18.0"
+ "clean-stack": "^2.0.0",
+ "indent-string": "^4.0.0"
},
"engines": {
- "node": "^20.19.0 || >=22.12.0"
- },
- "peerDependencies": {
- "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
+ "node": ">=8"
}
},
- "node_modules/@vitest/coverage-v8": {
- "version": "4.1.10",
- "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.10.tgz",
- "integrity": "sha512-IM49HmthevbgAO4anp1hwtoT9wYe59w0LR00gr+eagHE+ZJ5lK4sLPeO0ubgoJcwLk6dehU3R24N+FbEEKDc8g==",
+ "node_modules/ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@bcoe/v8-coverage": "^1.0.2",
- "@vitest/utils": "4.1.10",
- "ast-v8-to-istanbul": "^1.0.0",
- "istanbul-lib-coverage": "^3.2.2",
- "istanbul-lib-report": "^3.0.1",
- "istanbul-reports": "^3.2.0",
- "magicast": "^0.5.2",
- "obug": "^2.1.1",
- "std-env": "^4.0.0-rc.1",
- "tinyrainbow": "^3.1.0"
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
},
"funding": {
- "url": "https://opencollective.com/vitest"
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ajv-formats": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz",
+ "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^8.0.0"
},
"peerDependencies": {
- "@vitest/browser": "4.1.10",
- "vitest": "4.1.10"
+ "ajv": "^8.0.0"
},
"peerDependenciesMeta": {
- "@vitest/browser": {
+ "ajv": {
"optional": true
}
}
},
- "node_modules/@vitest/expect": {
- "version": "4.1.10",
- "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz",
- "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==",
- "dev": true,
+ "node_modules/ajv-formats/node_modules/ajv": {
+ "version": "8.20.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
+ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
"license": "MIT",
"dependencies": {
- "@standard-schema/spec": "^1.1.0",
- "@types/chai": "^5.2.2",
- "@vitest/spy": "4.1.10",
- "@vitest/utils": "4.1.10",
- "chai": "^6.2.2",
- "tinyrainbow": "^3.1.0"
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2"
},
"funding": {
- "url": "https://opencollective.com/vitest"
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
}
},
- "node_modules/@vitest/mocker": {
- "version": "4.1.10",
- "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz",
- "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==",
- "dev": true,
+ "node_modules/ajv-formats/node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "license": "MIT"
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "devOptional": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"license": "MIT",
"dependencies": {
- "@vitest/spy": "4.1.10",
- "estree-walker": "^3.0.3",
- "magic-string": "^0.30.21"
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
},
"funding": {
- "url": "https://opencollective.com/vitest"
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/apache-arrow": {
+ "version": "18.1.0",
+ "resolved": "https://registry.npmjs.org/apache-arrow/-/apache-arrow-18.1.0.tgz",
+ "integrity": "sha512-v/ShMp57iBnBp4lDgV8Jx3d3Q5/Hac25FWmQ98eMahUiHPXcvwIMKJD0hBIgclm/FCG+LwPkAKtkRO1O/W0YGg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@swc/helpers": "^0.5.11",
+ "@types/command-line-args": "^5.2.3",
+ "@types/command-line-usage": "^5.0.4",
+ "@types/node": "^20.13.0",
+ "command-line-args": "^5.2.1",
+ "command-line-usage": "^7.0.1",
+ "flatbuffers": "^24.3.25",
+ "json-bignum": "^0.0.3",
+ "tslib": "^2.6.2"
},
- "peerDependencies": {
- "msw": "^2.4.9",
- "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ "bin": {
+ "arrow2csv": "bin/arrow2csv.js"
+ }
+ },
+ "node_modules/apache-arrow/node_modules/@types/node": {
+ "version": "20.19.43",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz",
+ "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==",
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
+ "node_modules/apache-arrow/node_modules/flatbuffers": {
+ "version": "24.12.23",
+ "resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-24.12.23.tgz",
+ "integrity": "sha512-dLVCAISd5mhls514keQzmEG6QHmUUsNuWsb4tFafIUwvvgDjXhtfAYSKOzt5SWOy+qByV5pbsDZ+Vb7HUOBEdA==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/app-builder-lib": {
+ "version": "26.15.3",
+ "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-26.15.3.tgz",
+ "integrity": "sha512-2VnyWkqsP5v5XbBhL3tD5Syx8iNPBYsoU7kY4S2fz7wg8Rj/nztWKCUzGKaFRTv0Xwf3/H058CR1Kvtd/3lRow==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@electron/asar": "3.4.1",
+ "@electron/fuses": "^1.8.0",
+ "@electron/get": "^3.0.0",
+ "@electron/notarize": "2.5.0",
+ "@electron/osx-sign": "1.3.3",
+ "@electron/rebuild": "^4.0.4",
+ "@electron/universal": "2.0.3",
+ "@malept/flatpak-bundler": "^0.4.0",
+ "@noble/hashes": "^2.2.0",
+ "@peculiar/webcrypto": "^1.7.1",
+ "@types/fs-extra": "9.0.13",
+ "ajv": "^8.18.0",
+ "asn1js": "^3.0.10",
+ "async-exit-hook": "^2.0.1",
+ "builder-util": "26.15.3",
+ "builder-util-runtime": "9.7.0",
+ "chromium-pickle-js": "^0.2.0",
+ "ci-info": "4.3.1",
+ "debug": "^4.3.4",
+ "dotenv": "^16.4.5",
+ "dotenv-expand": "^11.0.6",
+ "ejs": "^3.1.8",
+ "electron-publish": "26.15.3",
+ "fs-extra": "^10.1.0",
+ "hosted-git-info": "^4.1.0",
+ "isbinaryfile": "^5.0.0",
+ "jiti": "^2.4.2",
+ "js-yaml": "^4.1.0",
+ "json5": "^2.2.3",
+ "lazy-val": "^1.0.5",
+ "minimatch": "^10.2.5",
+ "pkijs": "^3.4.0",
+ "plist": "3.1.0",
+ "proper-lockfile": "^4.1.2",
+ "resedit": "^1.7.0",
+ "semver": "~7.7.3",
+ "tar": "^7.5.7",
+ "temp-file": "^3.4.0",
+ "tiny-async-pool": "1.3.0",
+ "unzipper": "^0.12.3",
+ "which": "^5.0.0"
},
- "peerDependenciesMeta": {
- "msw": {
- "optional": true
- },
- "vite": {
- "optional": true
- }
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "dmg-builder": "26.15.3",
+ "electron-builder-squirrel-windows": "26.15.3"
}
},
- "node_modules/@vitest/pretty-format": {
- "version": "4.1.10",
- "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz",
- "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==",
+ "node_modules/app-builder-lib/node_modules/@electron/get": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@electron/get/-/get-3.1.0.tgz",
+ "integrity": "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "tinyrainbow": "^3.1.0"
+ "debug": "^4.1.1",
+ "env-paths": "^2.2.0",
+ "fs-extra": "^8.1.0",
+ "got": "^11.8.5",
+ "progress": "^2.0.3",
+ "semver": "^6.2.0",
+ "sumchecker": "^3.0.1"
},
- "funding": {
- "url": "https://opencollective.com/vitest"
+ "engines": {
+ "node": ">=14"
+ },
+ "optionalDependencies": {
+ "global-agent": "^3.0.0"
}
},
- "node_modules/@vitest/runner": {
- "version": "4.1.10",
- "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz",
- "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==",
+ "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/fs-extra": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
+ "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/utils": "4.1.10",
- "pathe": "^2.0.3"
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^4.0.0",
+ "universalify": "^0.1.0"
},
- "funding": {
- "url": "https://opencollective.com/vitest"
+ "engines": {
+ "node": ">=6 <7 || >=8"
}
},
- "node_modules/@vitest/snapshot": {
- "version": "4.1.10",
- "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz",
- "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==",
+ "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/app-builder-lib/node_modules/ajv": {
+ "version": "8.20.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
+ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/pretty-format": "4.1.10",
- "@vitest/utils": "4.1.10",
- "magic-string": "^0.30.21",
- "pathe": "^2.0.3"
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2"
},
"funding": {
- "url": "https://opencollective.com/vitest"
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
}
},
- "node_modules/@vitest/spy": {
- "version": "4.1.10",
- "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz",
- "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==",
+ "node_modules/app-builder-lib/node_modules/chownr": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
+ "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/app-builder-lib/node_modules/ci-info": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz",
+ "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==",
"dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/sibiraj-s"
+ }
+ ],
"license": "MIT",
- "funding": {
- "url": "https://opencollective.com/vitest"
+ "engines": {
+ "node": ">=8"
}
},
- "node_modules/@vitest/utils": {
- "version": "4.1.10",
- "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz",
- "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==",
+ "node_modules/app-builder-lib/node_modules/fs-extra": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
+ "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/pretty-format": "4.1.10",
- "convert-source-map": "^2.0.0",
- "tinyrainbow": "^3.1.0"
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
},
- "funding": {
- "url": "https://opencollective.com/vitest"
+ "engines": {
+ "node": ">=12"
}
},
- "node_modules/@webgpu/types": {
- "version": "0.1.69",
- "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.69.tgz",
- "integrity": "sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ==",
+ "node_modules/app-builder-lib/node_modules/fs-extra/node_modules/jsonfile": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz",
+ "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==",
"dev": true,
- "license": "BSD-3-Clause"
- },
- "node_modules/@xenova/transformers": {
- "version": "2.17.2",
- "resolved": "https://registry.npmjs.org/@xenova/transformers/-/transformers-2.17.2.tgz",
- "integrity": "sha512-lZmHqzrVIkSvZdKZEx7IYY51TK0WDrC8eR0c5IMnBsO8di8are1zzw8BlLhyO2TklZKLN5UffNGs1IJwT6oOqQ==",
- "license": "Apache-2.0",
+ "license": "MIT",
"dependencies": {
- "@huggingface/jinja": "^0.2.2",
- "onnxruntime-web": "1.14.0",
- "sharp": "^0.32.0"
+ "universalify": "^2.0.0"
},
"optionalDependencies": {
- "onnxruntime-node": "1.14.0"
+ "graceful-fs": "^4.1.6"
}
},
- "node_modules/@xenova/transformers/node_modules/node-addon-api": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz",
- "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==",
+ "node_modules/app-builder-lib/node_modules/fs-extra/node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/app-builder-lib/node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "dev": true,
"license": "MIT"
},
- "node_modules/@xenova/transformers/node_modules/semver": {
- "version": "7.8.5",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
- "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
+ "node_modules/app-builder-lib/node_modules/semver": {
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+ "dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -8434,755 +8219,674 @@
"node": ">=10"
}
},
- "node_modules/@xenova/transformers/node_modules/sharp": {
- "version": "0.32.6",
- "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.32.6.tgz",
- "integrity": "sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==",
- "hasInstallScript": true,
- "license": "Apache-2.0",
+ "node_modules/app-builder-lib/node_modules/tar": {
+ "version": "7.5.20",
+ "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.20.tgz",
+ "integrity": "sha512-9FcyK4PA6+WbzlTM9WhQm6vB5W7cP7dUiPsv1g7YDwEQnQ1CGpK3MGlKk/ITVWMk05kHZuBhmVhiv8LZoy/PFQ==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
"dependencies": {
- "color": "^4.2.3",
- "detect-libc": "^2.0.2",
- "node-addon-api": "^6.1.0",
- "prebuild-install": "^7.1.1",
- "semver": "^7.5.4",
- "simple-get": "^4.0.1",
- "tar-fs": "^3.0.4",
- "tunnel-agent": "^0.6.0"
+ "@isaacs/fs-minipass": "^4.0.0",
+ "chownr": "^3.0.0",
+ "minipass": "^7.1.2",
+ "minizlib": "^3.1.0",
+ "yallist": "^5.0.0"
},
"engines": {
- "node": ">=14.15.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
+ "node": ">=18"
}
},
- "node_modules/@xenova/transformers/node_modules/tar-fs": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.2.tgz",
- "integrity": "sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw==",
+ "node_modules/app-builder-lib/node_modules/yallist": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
+ "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/aproba": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz",
+ "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==",
+ "license": "ISC",
+ "optional": true
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "license": "Python-2.0"
+ },
+ "node_modules/aria-hidden": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz",
+ "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==",
"license": "MIT",
"dependencies": {
- "pump": "^3.0.0",
- "tar-stream": "^3.1.5"
+ "tslib": "^2.0.0"
},
- "optionalDependencies": {
- "bare-fs": "^4.0.1",
- "bare-path": "^3.0.0"
+ "engines": {
+ "node": ">=10"
}
},
- "node_modules/@xenova/transformers/node_modules/tar-stream": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.0.tgz",
- "integrity": "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==",
- "license": "MIT",
+ "node_modules/aria-query": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
+ "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "peer": true,
"dependencies": {
- "b4a": "^1.6.4",
- "bare-fs": "^4.5.5",
- "fast-fifo": "^1.2.0",
- "streamx": "^2.15.0"
+ "dequal": "^2.0.3"
}
},
- "node_modules/@xmldom/xmldom": {
- "version": "0.8.11",
- "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz",
- "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==",
+ "node_modules/array-back": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz",
+ "integrity": "sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==",
"license": "MIT",
"engines": {
- "node": ">=10.0.0"
+ "node": ">=6"
}
},
- "node_modules/3d-force-graph": {
- "version": "1.79.0",
- "resolved": "https://registry.npmjs.org/3d-force-graph/-/3d-force-graph-1.79.0.tgz",
- "integrity": "sha512-0RUNcfiH12f93loY/iS4wShzhXzdLLN4futvFnintF7eP30DjX+nAdLDAGOZwSflhijQyVwnGtpczNjFrDLUzQ==",
+ "node_modules/array-buffer-byte-length": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz",
+ "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "accessor-fn": "1",
- "kapsule": "^1.16",
- "three": ">=0.118 <1",
- "three-forcegraph": "1",
- "three-render-objects": "^1.35"
+ "call-bound": "^1.0.3",
+ "is-array-buffer": "^3.0.5"
},
"engines": {
- "node": ">=12"
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/7zip-bin": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz",
- "integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/abbrev": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz",
- "integrity": "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==",
+ "node_modules/array-includes": {
+ "version": "3.1.9",
+ "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz",
+ "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==",
"dev": true,
- "license": "ISC",
- "engines": {
- "node": "^18.17.0 || >=20.5.0"
- }
- },
- "node_modules/accepts": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
- "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
"license": "MIT",
"dependencies": {
- "mime-types": "^3.0.0",
- "negotiator": "^1.0.0"
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.4",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.24.0",
+ "es-object-atoms": "^1.1.1",
+ "get-intrinsic": "^1.3.0",
+ "is-string": "^1.1.1",
+ "math-intrinsics": "^1.1.0"
},
"engines": {
- "node": ">= 0.6"
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/accepts/node_modules/mime-db": {
- "version": "1.54.0",
- "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
- "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+ "node_modules/array.prototype.findlast": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz",
+ "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==",
+ "dev": true,
"license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.2",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.0.0",
+ "es-shim-unscopables": "^1.0.2"
+ },
"engines": {
- "node": ">= 0.6"
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/accepts/node_modules/mime-types": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
- "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
+ "node_modules/array.prototype.flat": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz",
+ "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "mime-db": "^1.54.0"
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.5",
+ "es-shim-unscopables": "^1.0.2"
},
"engines": {
- "node": ">=18"
+ "node": ">= 0.4"
},
"funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/accessor-fn": {
- "version": "1.5.3",
- "resolved": "https://registry.npmjs.org/accessor-fn/-/accessor-fn-1.5.3.tgz",
- "integrity": "sha512-rkAofCwe/FvYFUlMB0v0gWmhqtfAtV1IUkdPbfhTUyYniu5LrC0A0UJkTH0Jv3S8SvwkmfuAlY+mQIJATdocMA==",
+ "node_modules/array.prototype.flatmap": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz",
+ "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==",
+ "dev": true,
"license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.5",
+ "es-shim-unscopables": "^1.0.2"
+ },
"engines": {
- "node": ">=12"
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/acorn": {
- "version": "8.15.0",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
- "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
+ "node_modules/array.prototype.tosorted": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz",
+ "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==",
"dev": true,
"license": "MIT",
- "bin": {
- "acorn": "bin/acorn"
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.3",
+ "es-errors": "^1.3.0",
+ "es-shim-unscopables": "^1.0.2"
},
"engines": {
- "node": ">=0.4.0"
+ "node": ">= 0.4"
}
},
- "node_modules/acorn-jsx": {
- "version": "5.3.2",
- "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
- "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "node_modules/arraybuffer.prototype.slice": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz",
+ "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==",
"dev": true,
"license": "MIT",
- "peerDependencies": {
- "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
- }
- },
- "node_modules/agent-base": {
- "version": "7.1.4",
- "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
- "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
- "devOptional": true,
- "license": "MIT",
+ "dependencies": {
+ "array-buffer-byte-length": "^1.0.1",
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.5",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "is-array-buffer": "^3.0.4"
+ },
"engines": {
- "node": ">= 14"
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/aggregate-error": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
- "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==",
- "license": "MIT",
- "optional": true,
+ "node_modules/asn1js": {
+ "version": "3.0.10",
+ "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.10.tgz",
+ "integrity": "sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==",
+ "dev": true,
+ "license": "BSD-3-Clause",
"dependencies": {
- "clean-stack": "^2.0.0",
- "indent-string": "^4.0.0"
+ "pvtsutils": "^1.3.6",
+ "pvutils": "^1.1.5",
+ "tslib": "^2.8.1"
},
"engines": {
- "node": ">=8"
+ "node": ">=12.0.0"
}
},
- "node_modules/ajv": {
- "version": "6.12.6",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
- "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "node_modules/assertion-error": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
+ "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
"dev": true,
"license": "MIT",
- "dependencies": {
- "fast-deep-equal": "^3.1.1",
- "fast-json-stable-stringify": "^2.0.0",
- "json-schema-traverse": "^0.4.1",
- "uri-js": "^4.2.2"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/epoberezkin"
+ "engines": {
+ "node": ">=12"
}
},
- "node_modules/ajv-formats": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz",
- "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==",
+ "node_modules/ast-v8-to-istanbul": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.4.tgz",
+ "integrity": "sha512-0bC0/4bTSrnwdhU3IsZDwEdojvuPrSg59OYZfKsLRtJZ0u8VBx9DebfqqG8bRdCC0I7vjgxmPi41P0lpkhJHtA==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "ajv": "^8.0.0"
- },
- "peerDependencies": {
- "ajv": "^8.0.0"
- },
- "peerDependenciesMeta": {
- "ajv": {
- "optional": true
- }
+ "@jridgewell/trace-mapping": "^0.3.31",
+ "estree-walker": "^3.0.3",
+ "js-tokens": "^10.0.0"
}
},
- "node_modules/ajv-formats/node_modules/ajv": {
- "version": "8.20.0",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
- "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
+ "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": {
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz",
+ "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/async": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
+ "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/async-exit-hook": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz",
+ "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==",
+ "dev": true,
"license": "MIT",
- "dependencies": {
- "fast-deep-equal": "^3.1.3",
- "fast-uri": "^3.0.1",
- "json-schema-traverse": "^1.0.0",
- "require-from-string": "^2.0.2"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/epoberezkin"
+ "engines": {
+ "node": ">=0.12.0"
}
},
- "node_modules/ajv-formats/node_modules/json-schema-traverse": {
+ "node_modules/async-function": {
"version": "1.0.0",
- "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
- "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
- "license": "MIT"
- },
- "node_modules/ajv-keywords": {
- "version": "3.5.2",
- "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
- "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
+ "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz",
+ "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==",
"dev": true,
"license": "MIT",
- "peerDependencies": {
- "ajv": "^6.9.1"
+ "engines": {
+ "node": ">= 0.4"
}
},
- "node_modules/ansi-escapes": {
- "version": "6.2.1",
- "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-6.2.1.tgz",
- "integrity": "sha512-4nJ3yixlEthEJ9Rk4vPcdBRkZvQZlYyu8j4/Mqz5sgIkddmEnH2Yj2ZrnP9S3tQOvSNRUIgVNF/1yPpRAGNRig==",
+ "node_modules/async-mutex": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz",
+ "integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==",
"license": "MIT",
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/at-least-node": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
+ "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==",
+ "dev": true,
+ "license": "ISC",
"engines": {
- "node": ">=14.16"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "node": ">= 4.0.0"
}
},
- "node_modules/ansi-regex": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "node_modules/autoprefixer": {
+ "version": "10.4.23",
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.23.tgz",
+ "integrity": "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/autoprefixer"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
"license": "MIT",
+ "dependencies": {
+ "browserslist": "^4.28.1",
+ "caniuse-lite": "^1.0.30001760",
+ "fraction.js": "^5.3.4",
+ "picocolors": "^1.1.1",
+ "postcss-value-parser": "^4.2.0"
+ },
+ "bin": {
+ "autoprefixer": "bin/autoprefixer"
+ },
"engines": {
- "node": ">=8"
+ "node": "^10 || ^12 || >=14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
}
},
- "node_modules/ansi-styles": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
- "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "node_modules/available-typed-arrays": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
+ "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "color-convert": "^2.0.1"
+ "possible-typed-array-names": "^1.0.0"
},
"engines": {
- "node": ">=8"
+ "node": ">= 0.4"
},
"funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/apache-arrow": {
- "version": "18.1.0",
- "resolved": "https://registry.npmjs.org/apache-arrow/-/apache-arrow-18.1.0.tgz",
- "integrity": "sha512-v/ShMp57iBnBp4lDgV8Jx3d3Q5/Hac25FWmQ98eMahUiHPXcvwIMKJD0hBIgclm/FCG+LwPkAKtkRO1O/W0YGg==",
+ "node_modules/aws4": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz",
+ "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/b4a": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz",
+ "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==",
"license": "Apache-2.0",
- "dependencies": {
- "@swc/helpers": "^0.5.11",
- "@types/command-line-args": "^5.2.3",
- "@types/command-line-usage": "^5.0.4",
- "@types/node": "^20.13.0",
- "command-line-args": "^5.2.1",
- "command-line-usage": "^7.0.1",
- "flatbuffers": "^24.3.25",
- "json-bignum": "^0.0.3",
- "tslib": "^2.6.2"
+ "peerDependencies": {
+ "react-native-b4a": "*"
},
- "bin": {
- "arrow2csv": "bin/arrow2csv.js"
+ "peerDependenciesMeta": {
+ "react-native-b4a": {
+ "optional": true
+ }
}
},
- "node_modules/apache-arrow/node_modules/@types/node": {
- "version": "20.19.43",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz",
- "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==",
+ "node_modules/bail": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
+ "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==",
"license": "MIT",
- "dependencies": {
- "undici-types": "~6.21.0"
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
}
},
- "node_modules/apache-arrow/node_modules/flatbuffers": {
- "version": "24.12.23",
- "resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-24.12.23.tgz",
- "integrity": "sha512-dLVCAISd5mhls514keQzmEG6QHmUUsNuWsb4tFafIUwvvgDjXhtfAYSKOzt5SWOy+qByV5pbsDZ+Vb7HUOBEdA==",
- "license": "Apache-2.0"
- },
- "node_modules/app-builder-bin": {
- "version": "5.0.0-alpha.12",
- "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-5.0.0-alpha.12.tgz",
- "integrity": "sha512-j87o0j6LqPL3QRr8yid6c+Tt5gC7xNfYo6uQIQkorAC6MpeayVMZrEDzKmJJ/Hlv7EnOQpaRm53k6ktDYZyB6w==",
- "dev": true,
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "devOptional": true,
"license": "MIT"
},
- "node_modules/app-builder-lib": {
- "version": "26.4.0",
- "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-26.4.0.tgz",
- "integrity": "sha512-Uas6hNe99KzP3xPWxh5LGlH8kWIVjZixzmMJHNB9+6hPyDpjc7NQMkVgi16rQDdpCFy22ZU5sp8ow7tvjeMgYQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@develar/schema-utils": "~2.6.5",
- "@electron/asar": "3.4.1",
- "@electron/fuses": "^1.8.0",
- "@electron/notarize": "2.5.0",
- "@electron/osx-sign": "1.3.3",
- "@electron/rebuild": "4.0.1",
- "@electron/universal": "2.0.3",
- "@malept/flatpak-bundler": "^0.4.0",
- "@types/fs-extra": "9.0.13",
- "async-exit-hook": "^2.0.1",
- "builder-util": "26.3.4",
- "builder-util-runtime": "9.5.1",
- "chromium-pickle-js": "^0.2.0",
- "ci-info": "4.3.1",
- "debug": "^4.3.4",
- "dotenv": "^16.4.5",
- "dotenv-expand": "^11.0.6",
- "ejs": "^3.1.8",
- "electron-publish": "26.3.4",
- "fs-extra": "^10.1.0",
- "hosted-git-info": "^4.1.0",
- "isbinaryfile": "^5.0.0",
- "jiti": "^2.4.2",
- "js-yaml": "^4.1.0",
- "json5": "^2.2.3",
- "lazy-val": "^1.0.5",
- "minimatch": "^10.0.3",
- "plist": "3.1.0",
- "resedit": "^1.7.0",
- "semver": "~7.7.3",
- "tar": "^6.1.12",
- "temp-file": "^3.4.0",
- "tiny-async-pool": "1.3.0",
- "which": "^5.0.0"
- },
- "engines": {
- "node": ">=14.0.0"
- },
+ "node_modules/bare-events": {
+ "version": "2.9.1",
+ "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.1.tgz",
+ "integrity": "sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==",
+ "license": "Apache-2.0",
"peerDependencies": {
- "dmg-builder": "26.4.0",
- "electron-builder-squirrel-windows": "26.4.0"
+ "bare-abort-controller": "*"
+ },
+ "peerDependenciesMeta": {
+ "bare-abort-controller": {
+ "optional": true
+ }
}
},
- "node_modules/app-builder-lib/node_modules/fs-extra": {
- "version": "10.1.0",
- "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
- "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
- "dev": true,
- "license": "MIT",
+ "node_modules/bare-fs": {
+ "version": "4.7.2",
+ "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.2.tgz",
+ "integrity": "sha512-aTvMFUWkBmjzKtEQMDGGDNF8bkfpD5N1b/FCwt7A3wrU4t1o/e/85Wzkluh6JlODCjqVESYCkQCdTXqZ9G7VFg==",
+ "license": "Apache-2.0",
"dependencies": {
- "graceful-fs": "^4.2.0",
- "jsonfile": "^6.0.1",
- "universalify": "^2.0.0"
+ "bare-events": "^2.5.4",
+ "bare-path": "^3.0.0",
+ "bare-stream": "^2.6.4",
+ "bare-url": "^2.2.2",
+ "fast-fifo": "^1.3.2"
},
"engines": {
- "node": ">=12"
- }
- },
- "node_modules/app-builder-lib/node_modules/jsonfile": {
- "version": "6.2.0",
- "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
- "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "universalify": "^2.0.0"
+ "bare": ">=1.16.0"
},
- "optionalDependencies": {
- "graceful-fs": "^4.1.6"
- }
- },
- "node_modules/app-builder-lib/node_modules/semver": {
- "version": "7.7.3",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
- "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
- "dev": true,
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
+ "peerDependencies": {
+ "bare-buffer": "*"
},
- "engines": {
- "node": ">=10"
+ "peerDependenciesMeta": {
+ "bare-buffer": {
+ "optional": true
+ }
}
},
- "node_modules/app-builder-lib/node_modules/universalify": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
- "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
- "dev": true,
- "license": "MIT",
+ "node_modules/bare-os": {
+ "version": "3.9.1",
+ "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.9.1.tgz",
+ "integrity": "sha512-6M5XjcnsygQNPMCMPXSK379xrJFiZ/AEMNBmFEmQW8d/789VQATvriyi5r0HYTL9TkQ26rn3kgdTG3aisbrXkQ==",
+ "license": "Apache-2.0",
"engines": {
- "node": ">= 10.0.0"
+ "bare": ">=1.14.0"
}
},
- "node_modules/aproba": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz",
- "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==",
- "license": "ISC"
- },
- "node_modules/are-we-there-yet": {
+ "node_modules/bare-path": {
"version": "3.0.1",
- "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz",
- "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==",
- "deprecated": "This package is no longer supported.",
- "license": "ISC",
+ "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.1.tgz",
+ "integrity": "sha512-ghj2DSK/2e99a1anTVPCV4m4YIYtrbXhfM7V3D7XZLOTsybnYyaJloymGqssQc8l/or0UoDyRtNQkmkEF/ysgQ==",
+ "license": "Apache-2.0",
"dependencies": {
- "delegates": "^1.0.0",
- "readable-stream": "^3.6.0"
- },
- "engines": {
- "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+ "bare-os": "^3.0.1"
}
},
- "node_modules/argparse": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
- "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
- "license": "Python-2.0"
- },
- "node_modules/aria-hidden": {
- "version": "1.2.6",
- "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz",
- "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==",
- "license": "MIT",
+ "node_modules/bare-stream": {
+ "version": "2.13.3",
+ "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.3.tgz",
+ "integrity": "sha512-Kc+brLqvEqGkjyfiwJmImAOqLZL7OsoLKuavx+hJjgVV3nLTOjloJyPMFxjUPerGGHrNH0fLU06jjykMLWrERQ==",
+ "license": "Apache-2.0",
"dependencies": {
- "tslib": "^2.0.0"
+ "b4a": "^1.8.1",
+ "streamx": "^2.25.0",
+ "teex": "^1.0.1"
},
- "engines": {
- "node": ">=10"
+ "peerDependencies": {
+ "bare-abort-controller": "*",
+ "bare-buffer": "*",
+ "bare-events": "*"
+ },
+ "peerDependenciesMeta": {
+ "bare-abort-controller": {
+ "optional": true
+ },
+ "bare-buffer": {
+ "optional": true
+ },
+ "bare-events": {
+ "optional": true
+ }
}
},
- "node_modules/array-back": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz",
- "integrity": "sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==",
- "license": "MIT",
- "engines": {
- "node": ">=6"
+ "node_modules/bare-url": {
+ "version": "2.4.5",
+ "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.5.tgz",
+ "integrity": "sha512-K+y9xF1tN+CdPu4qWwr0QiK1Al07eFPGYK5M2pDXcmHdMdgC/tT/bpmMe1hrmRHaidKLkXrC+cRNYf3XVDUhSQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "bare-path": "^3.0.0"
}
},
- "node_modules/array-buffer-byte-length": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz",
- "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==",
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.9.15",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.15.tgz",
+ "integrity": "sha512-kX8h7K2srmDyYnXRIppo4AH/wYgzWVCs+eKr3RusRSQ5PvRYoEFmR/I0PbdTjKFAoKqp5+kbxnNTFO9jOfSVJg==",
"dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.js"
+ }
+ },
+ "node_modules/better-sqlite3": {
+ "version": "12.6.2",
+ "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.6.2.tgz",
+ "integrity": "sha512-8VYKM3MjCa9WcaSAI3hzwhmyHVlH8tiGFwf0RlTsZPWJ1I5MkzjiudCo4KC4DxOaL/53A5B1sI/IbldNFDbsKA==",
+ "hasInstallScript": true,
"license": "MIT",
"dependencies": {
- "call-bound": "^1.0.3",
- "is-array-buffer": "^3.0.5"
+ "bindings": "^1.5.0",
+ "prebuild-install": "^7.1.1"
},
"engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "node": "20.x || 22.x || 23.x || 24.x || 25.x"
}
},
- "node_modules/array-includes": {
- "version": "3.1.9",
- "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz",
- "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==",
- "dev": true,
+ "node_modules/better-sqlite3-multiple-ciphers": {
+ "version": "12.11.1",
+ "resolved": "https://registry.npmjs.org/better-sqlite3-multiple-ciphers/-/better-sqlite3-multiple-ciphers-12.11.1.tgz",
+ "integrity": "sha512-pG72+3VkUipnmYx4LwQqoOXNG2CJ1HlmDrlvqgr7xQHgsE+cz5rAZEiaJHnUKTHaGfGifggA/C0Sv9qZlUrfow==",
+ "hasInstallScript": true,
"license": "MIT",
"dependencies": {
- "call-bind": "^1.0.8",
- "call-bound": "^1.0.4",
- "define-properties": "^1.2.1",
- "es-abstract": "^1.24.0",
- "es-object-atoms": "^1.1.1",
- "get-intrinsic": "^1.3.0",
- "is-string": "^1.1.1",
- "math-intrinsics": "^1.1.0"
+ "bindings": "^1.5.0",
+ "prebuild-install": "^7.1.1"
},
"engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "node": "20.x || 22.x || 23.x || 24.x || 25.x || 26.x"
}
},
- "node_modules/array.prototype.findlast": {
- "version": "1.2.5",
- "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz",
- "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==",
+ "node_modules/bidi-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
+ "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "call-bind": "^1.0.7",
- "define-properties": "^1.2.1",
- "es-abstract": "^1.23.2",
- "es-errors": "^1.3.0",
- "es-object-atoms": "^1.0.0",
- "es-shim-unscopables": "^1.0.2"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "require-from-string": "^2.0.2"
}
},
- "node_modules/array.prototype.flat": {
- "version": "1.3.3",
- "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz",
- "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==",
- "dev": true,
+ "node_modules/bindings": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
+ "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
"license": "MIT",
"dependencies": {
- "call-bind": "^1.0.8",
- "define-properties": "^1.2.1",
- "es-abstract": "^1.23.5",
- "es-shim-unscopables": "^1.0.2"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "file-uri-to-path": "1.0.0"
}
},
- "node_modules/array.prototype.flatmap": {
- "version": "1.3.3",
- "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz",
- "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==",
- "dev": true,
+ "node_modules/bl": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
+ "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
"license": "MIT",
"dependencies": {
- "call-bind": "^1.0.8",
- "define-properties": "^1.2.1",
- "es-abstract": "^1.23.5",
- "es-shim-unscopables": "^1.0.2"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "buffer": "^5.5.0",
+ "inherits": "^2.0.4",
+ "readable-stream": "^3.4.0"
}
},
- "node_modules/array.prototype.tosorted": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz",
- "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bind": "^1.0.7",
- "define-properties": "^1.2.1",
- "es-abstract": "^1.23.3",
- "es-errors": "^1.3.0",
- "es-shim-unscopables": "^1.0.2"
- },
- "engines": {
- "node": ">= 0.4"
- }
+ "node_modules/bluebird": {
+ "version": "3.4.7",
+ "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz",
+ "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==",
+ "license": "MIT"
},
- "node_modules/arraybuffer.prototype.slice": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz",
- "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==",
- "dev": true,
+ "node_modules/body-parser": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz",
+ "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==",
"license": "MIT",
"dependencies": {
- "array-buffer-byte-length": "^1.0.1",
- "call-bind": "^1.0.8",
- "define-properties": "^1.2.1",
- "es-abstract": "^1.23.5",
- "es-errors": "^1.3.0",
- "get-intrinsic": "^1.2.6",
- "is-array-buffer": "^3.0.4"
+ "bytes": "^3.1.2",
+ "content-type": "^2.0.0",
+ "debug": "^4.4.3",
+ "http-errors": "^2.0.1",
+ "iconv-lite": "^0.7.2",
+ "on-finished": "^2.4.1",
+ "qs": "^6.15.2",
+ "raw-body": "^3.0.2",
+ "type-is": "^2.1.0"
},
"engines": {
- "node": ">= 0.4"
+ "node": ">=18"
},
"funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/assert-plus": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
- "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "engines": {
- "node": ">=0.8"
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
}
},
- "node_modules/assertion-error": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
- "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
- "dev": true,
+ "node_modules/body-parser/node_modules/content-type": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz",
+ "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==",
"license": "MIT",
"engines": {
- "node": ">=12"
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
}
},
- "node_modules/ast-v8-to-istanbul": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.4.tgz",
- "integrity": "sha512-0bC0/4bTSrnwdhU3IsZDwEdojvuPrSg59OYZfKsLRtJZ0u8VBx9DebfqqG8bRdCC0I7vjgxmPi41P0lpkhJHtA==",
- "dev": true,
+ "node_modules/body-parser/node_modules/iconv-lite": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
+ "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
"license": "MIT",
"dependencies": {
- "@jridgewell/trace-mapping": "^0.3.31",
- "estree-walker": "^3.0.3",
- "js-tokens": "^10.0.0"
- }
- },
- "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": {
- "version": "10.0.0",
- "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz",
- "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/astral-regex": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
- "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==",
- "dev": true,
- "license": "MIT",
- "optional": true,
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
"engines": {
- "node": ">=8"
+ "node": ">=0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
}
},
- "node_modules/async": {
- "version": "3.2.6",
- "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
- "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
- "dev": true,
+ "node_modules/boolean": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz",
+ "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==",
+ "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
"license": "MIT"
},
- "node_modules/async-exit-hook": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz",
- "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.12.0"
- }
- },
- "node_modules/async-function": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz",
- "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/async-mutex": {
- "version": "0.5.0",
- "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz",
- "integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==",
- "license": "MIT",
- "dependencies": {
- "tslib": "^2.4.0"
- }
- },
- "node_modules/async-retry": {
- "version": "1.3.3",
- "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz",
- "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==",
+ "node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "devOptional": true,
"license": "MIT",
"dependencies": {
- "retry": "0.13.1"
- }
- },
- "node_modules/async-retry/node_modules/retry": {
- "version": "0.13.1",
- "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
- "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==",
- "license": "MIT",
- "engines": {
- "node": ">= 4"
- }
- },
- "node_modules/asynckit": {
- "version": "0.4.0",
- "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
- "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
- "license": "MIT"
- },
- "node_modules/at-least-node": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
- "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==",
- "dev": true,
- "license": "ISC",
- "engines": {
- "node": ">= 4.0.0"
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
}
},
- "node_modules/autoprefixer": {
- "version": "10.4.23",
- "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.23.tgz",
- "integrity": "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==",
+ "node_modules/browserslist": {
+ "version": "4.28.1",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
+ "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
"dev": true,
"funding": [
{
"type": "opencollective",
- "url": "https://opencollective.com/postcss/"
+ "url": "https://opencollective.com/browserslist"
},
{
"type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/autoprefixer"
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
},
{
"type": "github",
@@ -9191,176 +8895,23 @@
],
"license": "MIT",
"dependencies": {
- "browserslist": "^4.28.1",
- "caniuse-lite": "^1.0.30001760",
- "fraction.js": "^5.3.4",
- "picocolors": "^1.1.1",
- "postcss-value-parser": "^4.2.0"
+ "baseline-browser-mapping": "^2.9.0",
+ "caniuse-lite": "^1.0.30001759",
+ "electron-to-chromium": "^1.5.263",
+ "node-releases": "^2.0.27",
+ "update-browserslist-db": "^1.2.0"
},
"bin": {
- "autoprefixer": "bin/autoprefixer"
+ "browserslist": "cli.js"
},
"engines": {
- "node": "^10 || ^12 || >=14"
- },
- "peerDependencies": {
- "postcss": "^8.1.0"
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
- "node_modules/available-typed-arrays": {
- "version": "1.0.7",
- "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
- "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "possible-typed-array-names": "^1.0.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/axios": {
- "version": "1.13.2",
- "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
- "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
- "license": "MIT",
- "dependencies": {
- "follow-redirects": "^1.15.6",
- "form-data": "^4.0.4",
- "proxy-from-env": "^1.1.0"
- }
- },
- "node_modules/b4a": {
- "version": "1.8.1",
- "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz",
- "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==",
- "license": "Apache-2.0",
- "peerDependencies": {
- "react-native-b4a": "*"
- },
- "peerDependenciesMeta": {
- "react-native-b4a": {
- "optional": true
- }
- }
- },
- "node_modules/bail": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
- "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==",
- "license": "MIT",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/wooorm"
- }
- },
- "node_modules/balanced-match": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
- "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
- "devOptional": true,
- "license": "MIT"
- },
- "node_modules/bare-events": {
- "version": "2.9.1",
- "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.1.tgz",
- "integrity": "sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==",
- "license": "Apache-2.0",
- "peerDependencies": {
- "bare-abort-controller": "*"
- },
- "peerDependenciesMeta": {
- "bare-abort-controller": {
- "optional": true
- }
- }
- },
- "node_modules/bare-fs": {
- "version": "4.7.2",
- "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.2.tgz",
- "integrity": "sha512-aTvMFUWkBmjzKtEQMDGGDNF8bkfpD5N1b/FCwt7A3wrU4t1o/e/85Wzkluh6JlODCjqVESYCkQCdTXqZ9G7VFg==",
- "license": "Apache-2.0",
- "dependencies": {
- "bare-events": "^2.5.4",
- "bare-path": "^3.0.0",
- "bare-stream": "^2.6.4",
- "bare-url": "^2.2.2",
- "fast-fifo": "^1.3.2"
- },
- "engines": {
- "bare": ">=1.16.0"
- },
- "peerDependencies": {
- "bare-buffer": "*"
- },
- "peerDependenciesMeta": {
- "bare-buffer": {
- "optional": true
- }
- }
- },
- "node_modules/bare-os": {
- "version": "3.9.1",
- "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.9.1.tgz",
- "integrity": "sha512-6M5XjcnsygQNPMCMPXSK379xrJFiZ/AEMNBmFEmQW8d/789VQATvriyi5r0HYTL9TkQ26rn3kgdTG3aisbrXkQ==",
- "license": "Apache-2.0",
- "engines": {
- "bare": ">=1.14.0"
- }
- },
- "node_modules/bare-path": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.1.tgz",
- "integrity": "sha512-ghj2DSK/2e99a1anTVPCV4m4YIYtrbXhfM7V3D7XZLOTsybnYyaJloymGqssQc8l/or0UoDyRtNQkmkEF/ysgQ==",
- "license": "Apache-2.0",
- "dependencies": {
- "bare-os": "^3.0.1"
- }
- },
- "node_modules/bare-stream": {
- "version": "2.13.3",
- "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.3.tgz",
- "integrity": "sha512-Kc+brLqvEqGkjyfiwJmImAOqLZL7OsoLKuavx+hJjgVV3nLTOjloJyPMFxjUPerGGHrNH0fLU06jjykMLWrERQ==",
- "license": "Apache-2.0",
- "dependencies": {
- "b4a": "^1.8.1",
- "streamx": "^2.25.0",
- "teex": "^1.0.1"
- },
- "peerDependencies": {
- "bare-abort-controller": "*",
- "bare-buffer": "*",
- "bare-events": "*"
- },
- "peerDependenciesMeta": {
- "bare-abort-controller": {
- "optional": true
- },
- "bare-buffer": {
- "optional": true
- },
- "bare-events": {
- "optional": true
- }
- }
- },
- "node_modules/bare-url": {
- "version": "2.4.5",
- "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.5.tgz",
- "integrity": "sha512-K+y9xF1tN+CdPu4qWwr0QiK1Al07eFPGYK5M2pDXcmHdMdgC/tT/bpmMe1hrmRHaidKLkXrC+cRNYf3XVDUhSQ==",
- "license": "Apache-2.0",
- "dependencies": {
- "bare-path": "^3.0.0"
- }
- },
- "node_modules/base64-js": {
- "version": "1.5.1",
- "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
- "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "node_modules/buffer": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+ "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
"funding": [
{
"type": "github",
@@ -9375,523 +8926,230 @@
"url": "https://feross.org/support"
}
],
- "license": "MIT"
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.1.13"
+ }
},
- "node_modules/baseline-browser-mapping": {
- "version": "2.9.15",
- "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.15.tgz",
- "integrity": "sha512-kX8h7K2srmDyYnXRIppo4AH/wYgzWVCs+eKr3RusRSQ5PvRYoEFmR/I0PbdTjKFAoKqp5+kbxnNTFO9jOfSVJg==",
- "dev": true,
- "license": "Apache-2.0",
- "bin": {
- "baseline-browser-mapping": "dist/cli.js"
+ "node_modules/buffer-crc32": {
+ "version": "0.2.13",
+ "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
+ "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
+ "license": "MIT",
+ "engines": {
+ "node": "*"
}
},
- "node_modules/before-after-hook": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz",
- "integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==",
- "license": "Apache-2.0"
+ "node_modules/buffer-from": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
+ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
+ "dev": true,
+ "license": "MIT"
},
- "node_modules/better-sqlite3": {
- "version": "12.6.2",
- "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.6.2.tgz",
- "integrity": "sha512-8VYKM3MjCa9WcaSAI3hzwhmyHVlH8tiGFwf0RlTsZPWJ1I5MkzjiudCo4KC4DxOaL/53A5B1sI/IbldNFDbsKA==",
- "hasInstallScript": true,
+ "node_modules/builder-util": {
+ "version": "26.15.3",
+ "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-26.15.3.tgz",
+ "integrity": "sha512-q2hn7Mbo2nFNkVekPiHFx6Nfo3hURmES3tfBn+k5Pqxl2RkmP3QGqZUhH/q9Pch/4G05NRhPjDlVj1O8q4Txvw==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "bindings": "^1.5.0",
- "prebuild-install": "^7.1.1"
+ "@types/debug": "^4.1.6",
+ "builder-util-runtime": "9.7.0",
+ "chalk": "^4.1.2",
+ "cross-spawn": "^7.0.6",
+ "debug": "^4.3.4",
+ "fs-extra": "^10.1.0",
+ "http-proxy-agent": "^7.0.0",
+ "https-proxy-agent": "^7.0.0",
+ "js-yaml": "^4.1.0",
+ "sanitize-filename": "^1.6.3",
+ "source-map-support": "^0.5.19",
+ "stat-mode": "^1.0.0",
+ "temp-file": "^3.4.0",
+ "tiny-async-pool": "1.3.0"
},
"engines": {
- "node": "20.x || 22.x || 23.x || 24.x || 25.x"
+ "node": ">=14.0.0"
}
},
- "node_modules/better-sqlite3-multiple-ciphers": {
- "version": "12.11.1",
- "resolved": "https://registry.npmjs.org/better-sqlite3-multiple-ciphers/-/better-sqlite3-multiple-ciphers-12.11.1.tgz",
- "integrity": "sha512-pG72+3VkUipnmYx4LwQqoOXNG2CJ1HlmDrlvqgr7xQHgsE+cz5rAZEiaJHnUKTHaGfGifggA/C0Sv9qZlUrfow==",
- "hasInstallScript": true,
+ "node_modules/builder-util-runtime": {
+ "version": "9.7.0",
+ "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.7.0.tgz",
+ "integrity": "sha512-g/kR520giAFYkSXTzcmF3kqQq7wi8F6N6SzeDgZrqTBN+VHdmgWOyTdD1yD7AATDId/yXLvuP34CxW46/BwCdw==",
"license": "MIT",
"dependencies": {
- "bindings": "^1.5.0",
- "prebuild-install": "^7.1.1"
+ "debug": "^4.3.4",
+ "sax": "^1.2.4"
},
"engines": {
- "node": "20.x || 22.x || 23.x || 24.x || 25.x || 26.x"
+ "node": ">=12.0.0"
}
},
- "node_modules/bindings": {
- "version": "1.5.0",
- "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
- "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
+ "node_modules/builder-util/node_modules/fs-extra": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
+ "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "file-uri-to-path": "1.0.0"
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=12"
}
},
- "node_modules/bl": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
- "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
+ "node_modules/builder-util/node_modules/jsonfile": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz",
+ "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "buffer": "^5.5.0",
- "inherits": "^2.0.4",
- "readable-stream": "^3.4.0"
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
}
},
- "node_modules/bluebird": {
- "version": "3.4.7",
- "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz",
- "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==",
- "license": "MIT"
- },
- "node_modules/body-parser": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz",
- "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==",
+ "node_modules/builder-util/node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "dev": true,
"license": "MIT",
- "dependencies": {
- "bytes": "^3.1.2",
- "content-type": "^2.0.0",
- "debug": "^4.4.3",
- "http-errors": "^2.0.1",
- "iconv-lite": "^0.7.2",
- "on-finished": "^2.4.1",
- "qs": "^6.15.2",
- "raw-body": "^3.0.2",
- "type-is": "^2.1.0"
- },
"engines": {
- "node": ">=18"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
+ "node": ">= 10.0.0"
}
},
- "node_modules/body-parser/node_modules/content-type": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz",
- "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==",
+ "node_modules/builtin-modules": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz",
+ "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==",
+ "dev": true,
"license": "MIT",
"engines": {
- "node": ">=18"
+ "node": ">=6"
},
"funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
+ "url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/body-parser/node_modules/iconv-lite": {
- "version": "0.7.2",
- "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
- "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
"license": "MIT",
- "dependencies": {
- "safer-buffer": ">= 2.1.2 < 3.0.0"
- },
"engines": {
- "node": ">=0.10.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/boolean": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz",
- "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==",
- "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
- "license": "MIT"
- },
- "node_modules/bottleneck": {
- "version": "2.19.5",
- "resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz",
- "integrity": "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==",
- "license": "MIT"
- },
- "node_modules/brace-expansion": {
- "version": "1.1.12",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
- "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
- "devOptional": true,
- "license": "MIT",
- "dependencies": {
- "balanced-match": "^1.0.0",
- "concat-map": "0.0.1"
+ "node": ">= 0.8"
}
},
- "node_modules/browserslist": {
- "version": "4.28.1",
- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
- "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
+ "node_modules/bytestreamjs": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/bytestreamjs/-/bytestreamjs-2.0.1.tgz",
+ "integrity": "sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ==",
"dev": true,
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/browserslist"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/browserslist"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "baseline-browser-mapping": "^2.9.0",
- "caniuse-lite": "^1.0.30001759",
- "electron-to-chromium": "^1.5.263",
- "node-releases": "^2.0.27",
- "update-browserslist-db": "^1.2.0"
- },
- "bin": {
- "browserslist": "cli.js"
- },
+ "license": "BSD-3-Clause",
"engines": {
- "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ "node": ">=6.0.0"
}
},
- "node_modules/buffer": {
- "version": "5.7.1",
- "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
- "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ],
+ "node_modules/cac": {
+ "version": "6.7.14",
+ "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
+ "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
+ "dev": true,
"license": "MIT",
- "dependencies": {
- "base64-js": "^1.3.1",
- "ieee754": "^1.1.13"
+ "engines": {
+ "node": ">=8"
}
},
- "node_modules/buffer-crc32": {
- "version": "0.2.13",
- "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
- "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
+ "node_modules/cacheable-lookup": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz",
+ "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==",
"license": "MIT",
"engines": {
- "node": "*"
+ "node": ">=10.6.0"
}
},
- "node_modules/buffer-from": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
- "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/builder-util": {
- "version": "26.3.4",
- "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-26.3.4.tgz",
- "integrity": "sha512-aRn88mYMktHxzdqDMF6Ayj0rKoX+ZogJ75Ck7RrIqbY/ad0HBvnS2xA4uHfzrGr5D2aLL3vU6OBEH4p0KMV2XQ==",
- "dev": true,
+ "node_modules/cacheable-request": {
+ "version": "7.0.4",
+ "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz",
+ "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==",
"license": "MIT",
"dependencies": {
- "@types/debug": "^4.1.6",
- "7zip-bin": "~5.2.0",
- "app-builder-bin": "5.0.0-alpha.12",
- "builder-util-runtime": "9.5.1",
- "chalk": "^4.1.2",
- "cross-spawn": "^7.0.6",
- "debug": "^4.3.4",
- "fs-extra": "^10.1.0",
- "http-proxy-agent": "^7.0.0",
- "https-proxy-agent": "^7.0.0",
- "js-yaml": "^4.1.0",
- "sanitize-filename": "^1.6.3",
- "source-map-support": "^0.5.19",
- "stat-mode": "^1.0.0",
- "temp-file": "^3.4.0",
- "tiny-async-pool": "1.3.0"
+ "clone-response": "^1.0.2",
+ "get-stream": "^5.1.0",
+ "http-cache-semantics": "^4.0.0",
+ "keyv": "^4.0.0",
+ "lowercase-keys": "^2.0.0",
+ "normalize-url": "^6.0.1",
+ "responselike": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
}
},
- "node_modules/builder-util-runtime": {
- "version": "9.5.1",
- "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.5.1.tgz",
- "integrity": "sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ==",
+ "node_modules/call-bind": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
+ "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
"dev": true,
"license": "MIT",
"dependencies": {
- "debug": "^4.3.4",
- "sax": "^1.2.4"
+ "call-bind-apply-helpers": "^1.0.0",
+ "es-define-property": "^1.0.0",
+ "get-intrinsic": "^1.2.4",
+ "set-function-length": "^1.2.2"
},
"engines": {
- "node": ">=12.0.0"
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/builder-util/node_modules/fs-extra": {
- "version": "10.1.0",
- "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
- "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
- "dev": true,
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"dependencies": {
- "graceful-fs": "^4.2.0",
- "jsonfile": "^6.0.1",
- "universalify": "^2.0.0"
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
},
"engines": {
- "node": ">=12"
+ "node": ">= 0.4"
}
},
- "node_modules/builder-util/node_modules/jsonfile": {
- "version": "6.2.0",
- "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
- "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
- "dev": true,
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
"license": "MIT",
"dependencies": {
- "universalify": "^2.0.0"
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
},
- "optionalDependencies": {
- "graceful-fs": "^4.1.6"
- }
- },
- "node_modules/builder-util/node_modules/universalify": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
- "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
- "dev": true,
- "license": "MIT",
"engines": {
- "node": ">= 10.0.0"
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/bytes": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
- "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true,
"license": "MIT",
"engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/cac": {
- "version": "6.7.14",
- "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
- "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/cacache": {
- "version": "19.0.1",
- "resolved": "https://registry.npmjs.org/cacache/-/cacache-19.0.1.tgz",
- "integrity": "sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "@npmcli/fs": "^4.0.0",
- "fs-minipass": "^3.0.0",
- "glob": "^10.2.2",
- "lru-cache": "^10.0.1",
- "minipass": "^7.0.3",
- "minipass-collect": "^2.0.1",
- "minipass-flush": "^1.0.5",
- "minipass-pipeline": "^1.2.4",
- "p-map": "^7.0.2",
- "ssri": "^12.0.0",
- "tar": "^7.4.3",
- "unique-filename": "^4.0.0"
- },
- "engines": {
- "node": "^18.17.0 || >=20.5.0"
- }
- },
- "node_modules/cacache/node_modules/brace-expansion": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
- "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "balanced-match": "^1.0.0"
- }
- },
- "node_modules/cacache/node_modules/chownr": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
- "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==",
- "dev": true,
- "license": "BlueOak-1.0.0",
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/cacache/node_modules/glob": {
- "version": "10.5.0",
- "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
- "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "foreground-child": "^3.1.0",
- "jackspeak": "^3.1.2",
- "minimatch": "^9.0.4",
- "minipass": "^7.1.2",
- "package-json-from-dist": "^1.0.0",
- "path-scurry": "^1.11.1"
- },
- "bin": {
- "glob": "dist/esm/bin.mjs"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/cacache/node_modules/lru-cache": {
- "version": "10.4.3",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
- "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/cacache/node_modules/minimatch": {
- "version": "9.0.5",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
- "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "brace-expansion": "^2.0.1"
- },
- "engines": {
- "node": ">=16 || 14 >=14.17"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/cacache/node_modules/tar": {
- "version": "7.5.3",
- "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.3.tgz",
- "integrity": "sha512-ENg5JUHUm2rDD7IvKNFGzyElLXNjachNLp6RaGf4+JOgxXHkqA+gq81ZAMCUmtMtqBsoU62lcp6S27g1LCYGGQ==",
- "dev": true,
- "license": "BlueOak-1.0.0",
- "dependencies": {
- "@isaacs/fs-minipass": "^4.0.0",
- "chownr": "^3.0.0",
- "minipass": "^7.1.2",
- "minizlib": "^3.1.0",
- "yallist": "^5.0.0"
- },
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/cacache/node_modules/yallist": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
- "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==",
- "dev": true,
- "license": "BlueOak-1.0.0",
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/cacheable-lookup": {
- "version": "5.0.4",
- "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz",
- "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==",
- "license": "MIT",
- "engines": {
- "node": ">=10.6.0"
- }
- },
- "node_modules/cacheable-request": {
- "version": "7.0.4",
- "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz",
- "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==",
- "license": "MIT",
- "dependencies": {
- "clone-response": "^1.0.2",
- "get-stream": "^5.1.0",
- "http-cache-semantics": "^4.0.0",
- "keyv": "^4.0.0",
- "lowercase-keys": "^2.0.0",
- "normalize-url": "^6.0.1",
- "responselike": "^2.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/call-bind": {
- "version": "1.0.8",
- "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
- "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bind-apply-helpers": "^1.0.0",
- "es-define-property": "^1.0.0",
- "get-intrinsic": "^1.2.4",
- "set-function-length": "^1.2.2"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/call-bind-apply-helpers": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
- "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
- "license": "MIT",
- "dependencies": {
- "es-errors": "^1.3.0",
- "function-bind": "^1.1.2"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/call-bound": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
- "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
- "license": "MIT",
- "dependencies": {
- "call-bind-apply-helpers": "^1.0.2",
- "get-intrinsic": "^1.3.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/callsites": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
- "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6"
+ "node": ">=6"
}
},
"node_modules/caniuse-lite": {
@@ -10006,17 +9264,12 @@
"url": "https://github.com/sponsors/wooorm"
}
},
- "node_modules/chmodrp": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/chmodrp/-/chmodrp-1.0.2.tgz",
- "integrity": "sha512-TdngOlFV1FLTzU0o1w8MB6/BFywhtLC0SzRTGJU7T9lmdjlCWeMRt1iVo0Ki+ldwNk0BqNiKoc8xpLZEQ8mY1w==",
- "license": "MIT"
- },
"node_modules/chownr": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
"integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
"license": "ISC",
+ "optional": true,
"engines": {
"node": ">=10"
}
@@ -10029,9 +9282,10 @@
"license": "MIT"
},
"node_modules/ci-info": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz",
- "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==",
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz",
+ "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==",
+ "dev": true,
"funding": [
{
"type": "github",
@@ -10065,53 +9319,11 @@
"node": ">=6"
}
},
- "node_modules/cli-cursor": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
- "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "restore-cursor": "^3.1.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/cli-spinners": {
- "version": "2.9.2",
- "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz",
- "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==",
- "license": "MIT",
- "engines": {
- "node": ">=6"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/cli-truncate": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz",
- "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "slice-ansi": "^3.0.0",
- "string-width": "^4.2.0"
- },
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/cliui": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+ "dev": true,
"license": "ISC",
"dependencies": {
"string-width": "^4.2.0",
@@ -10122,16 +9334,6 @@
"node": ">=12"
}
},
- "node_modules/clone": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
- "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.8"
- }
- },
"node_modules/clone-response": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz",
@@ -10153,127 +9355,33 @@
"node": ">=6"
}
},
- "node_modules/cmake-js": {
- "version": "7.4.0",
- "resolved": "https://registry.npmjs.org/cmake-js/-/cmake-js-7.4.0.tgz",
- "integrity": "sha512-Lw0JxEHrmk+qNj1n9W9d4IvkDdYTBn7l2BW6XmtLj7WPpIo2shvxUy+YokfjMxAAOELNonQwX3stkPhM5xSC2Q==",
+ "node_modules/cmdk": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz",
+ "integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==",
"license": "MIT",
"dependencies": {
- "axios": "^1.6.5",
- "debug": "^4",
- "fs-extra": "^11.2.0",
- "memory-stream": "^1.0.0",
- "node-api-headers": "^1.1.0",
- "npmlog": "^6.0.2",
- "rc": "^1.2.7",
- "semver": "^7.5.4",
- "tar": "^6.2.0",
- "url-join": "^4.0.1",
- "which": "^2.0.2",
- "yargs": "^17.7.2"
- },
- "bin": {
- "cmake-js": "bin/cmake-js"
+ "@radix-ui/react-compose-refs": "^1.1.1",
+ "@radix-ui/react-dialog": "^1.1.6",
+ "@radix-ui/react-id": "^1.1.0",
+ "@radix-ui/react-primitive": "^2.0.2"
},
- "engines": {
- "node": ">= 14.15.0"
+ "peerDependencies": {
+ "react": "^18 || ^19 || ^19.0.0-rc",
+ "react-dom": "^18 || ^19 || ^19.0.0-rc"
}
},
- "node_modules/cmake-js/node_modules/fs-extra": {
- "version": "11.3.3",
- "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz",
- "integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==",
+ "node_modules/color": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
+ "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
"license": "MIT",
"dependencies": {
- "graceful-fs": "^4.2.0",
- "jsonfile": "^6.0.1",
- "universalify": "^2.0.0"
+ "color-convert": "^2.0.1",
+ "color-string": "^1.9.0"
},
"engines": {
- "node": ">=14.14"
- }
- },
- "node_modules/cmake-js/node_modules/isexe": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
- "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
- "license": "ISC"
- },
- "node_modules/cmake-js/node_modules/jsonfile": {
- "version": "6.2.0",
- "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
- "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
- "license": "MIT",
- "dependencies": {
- "universalify": "^2.0.0"
- },
- "optionalDependencies": {
- "graceful-fs": "^4.1.6"
- }
- },
- "node_modules/cmake-js/node_modules/semver": {
- "version": "7.7.3",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
- "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/cmake-js/node_modules/universalify": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
- "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
- "license": "MIT",
- "engines": {
- "node": ">= 10.0.0"
- }
- },
- "node_modules/cmake-js/node_modules/which": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
- "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
- "license": "ISC",
- "dependencies": {
- "isexe": "^2.0.0"
- },
- "bin": {
- "node-which": "bin/node-which"
- },
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/cmdk": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz",
- "integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-compose-refs": "^1.1.1",
- "@radix-ui/react-dialog": "^1.1.6",
- "@radix-ui/react-id": "^1.1.0",
- "@radix-ui/react-primitive": "^2.0.2"
- },
- "peerDependencies": {
- "react": "^18 || ^19 || ^19.0.0-rc",
- "react-dom": "^18 || ^19 || ^19.0.0-rc"
- }
- },
- "node_modules/color": {
- "version": "4.2.3",
- "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
- "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
- "license": "MIT",
- "dependencies": {
- "color-convert": "^2.0.1",
- "color-string": "^1.9.0"
- },
- "engines": {
- "node": ">=12.5.0"
+ "node": ">=12.5.0"
}
},
"node_modules/color-convert": {
@@ -10309,6 +9417,7 @@
"resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
"integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==",
"license": "ISC",
+ "optional": true,
"bin": {
"color-support": "bin.js"
}
@@ -10317,6 +9426,7 @@
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
@@ -10414,7 +9524,8 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
"integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==",
- "license": "ISC"
+ "license": "ISC",
+ "optional": true
},
"node_modules/content-disposition": {
"version": "1.1.0",
@@ -10463,17 +9574,6 @@
"node": ">=6.6.0"
}
},
- "node_modules/core-js": {
- "version": "3.47.0",
- "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.47.0.tgz",
- "integrity": "sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg==",
- "hasInstallScript": true,
- "license": "MIT",
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/core-js"
- }
- },
"node_modules/core-util-is": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
@@ -10497,17 +9597,6 @@
"url": "https://opencollective.com/express"
}
},
- "node_modules/crc": {
- "version": "3.8.0",
- "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz",
- "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "buffer": "^5.1.0"
- }
- },
"node_modules/cross-dirname": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/cross-dirname/-/cross-dirname-0.1.0.tgz",
@@ -10552,6 +9641,20 @@
"node": ">= 8"
}
},
+ "node_modules/css-tree": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz",
+ "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mdn-data": "2.27.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
+ }
+ },
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
@@ -10729,6 +9832,58 @@
"node": ">=12"
}
},
+ "node_modules/data-urls": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz",
+ "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-mimetype": "^5.0.0",
+ "whatwg-url": "^16.0.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
+ "node_modules/data-urls/node_modules/tr46": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz",
+ "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "punycode": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/data-urls/node_modules/webidl-conversions": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz",
+ "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/data-urls/node_modules/whatwg-url": {
+ "version": "16.0.1",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz",
+ "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@exodus/bytes": "^1.11.0",
+ "tr46": "^6.0.0",
+ "webidl-conversions": "^8.0.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
"node_modules/data-view-buffer": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz",
@@ -10800,6 +9955,13 @@
}
}
},
+ "node_modules/decimal.js": {
+ "version": "10.6.0",
+ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
+ "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/decode-named-character-reference": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz",
@@ -10856,19 +10018,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/defaults": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz",
- "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "clone": "^1.0.2"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/defer-to-connect": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz",
@@ -10916,6 +10065,7 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=0.4.0"
@@ -10925,7 +10075,8 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
"integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==",
- "license": "MIT"
+ "license": "MIT",
+ "optional": true
},
"node_modules/depd": {
"version": "2.0.0",
@@ -10936,6 +10087,77 @@
"node": ">= 0.8"
}
},
+ "node_modules/dependency-cruiser": {
+ "version": "18.0.0",
+ "resolved": "https://registry.npmjs.org/dependency-cruiser/-/dependency-cruiser-18.0.0.tgz",
+ "integrity": "sha512-51Q7wbHoP3/GZrENpINnvKE/xfBsj41NVKarqu5Fff/kmqB/bg0GpiD5bOBwj0+CVunxPGzO80uTdYoy/d4Rsw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "acorn": "8.17.0",
+ "acorn-jsx": "5.3.2",
+ "acorn-jsx-walk": "2.0.0",
+ "acorn-loose": "8.5.2",
+ "acorn-walk": "8.3.5",
+ "commander": "15.0.0",
+ "enhanced-resolve": "5.24.1",
+ "ignore": "7.0.5",
+ "interpret": "3.1.1",
+ "is-installed-globally": "1.0.0",
+ "json5": "2.2.3",
+ "picomatch": "4.0.4",
+ "prompts": "2.4.2",
+ "rechoir": "0.8.0",
+ "safe-regex": "2.1.1",
+ "semver": "7.8.5",
+ "tsconfig-paths-webpack-plugin": "4.2.0",
+ "watskeburt": "6.0.0"
+ },
+ "bin": {
+ "depcruise": "bin/dependency-cruise.mjs",
+ "depcruise-baseline": "bin/depcruise-baseline.mjs",
+ "depcruise-fmt": "bin/depcruise-fmt.mjs",
+ "depcruise-wrap-stream-in-html": "bin/wrap-stream-in-html.mjs",
+ "dependency-cruise": "bin/dependency-cruise.mjs",
+ "dependency-cruiser": "bin/dependency-cruise.mjs"
+ },
+ "engines": {
+ "node": "^22||^24||>=26"
+ }
+ },
+ "node_modules/dependency-cruiser/node_modules/commander": {
+ "version": "15.0.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz",
+ "integrity": "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=22.12.0"
+ }
+ },
+ "node_modules/dependency-cruiser/node_modules/ignore": {
+ "version": "7.0.5",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
+ "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/dependency-cruiser/node_modules/semver": {
+ "version": "7.8.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
+ "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/dequal": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
@@ -10997,9 +10219,9 @@
}
},
"node_modules/dir-compare/node_modules/minimatch": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
- "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -11010,20 +10232,16 @@
}
},
"node_modules/dmg-builder": {
- "version": "26.4.0",
- "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-26.4.0.tgz",
- "integrity": "sha512-ce4Ogns4VMeisIuCSK0C62umG0lFy012jd8LMZ6w/veHUeX4fqfDrGe+HTWALAEwK6JwKP+dhPvizhArSOsFbg==",
+ "version": "26.15.3",
+ "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-26.15.3.tgz",
+ "integrity": "sha512-O3zJUFUYHJKgzPqioHxfxzBzlSC1eXCSr79gMSBKBP5AgjjpmrydMsMLotEg9fAJF36vdUncb+4ndRNxoPdlSQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "app-builder-lib": "26.4.0",
- "builder-util": "26.3.4",
+ "app-builder-lib": "26.15.3",
+ "builder-util": "26.15.3",
"fs-extra": "^10.1.0",
- "iconv-lite": "^0.6.2",
"js-yaml": "^4.1.0"
- },
- "optionalDependencies": {
- "dmg-license": "^1.0.11"
}
},
"node_modules/dmg-builder/node_modules/fs-extra": {
@@ -11042,9 +10260,9 @@
}
},
"node_modules/dmg-builder/node_modules/jsonfile": {
- "version": "6.2.0",
- "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
- "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz",
+ "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -11064,33 +10282,6 @@
"node": ">= 10.0.0"
}
},
- "node_modules/dmg-license": {
- "version": "1.0.11",
- "resolved": "https://registry.npmjs.org/dmg-license/-/dmg-license-1.0.11.tgz",
- "integrity": "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "dependencies": {
- "@types/plist": "^3.0.1",
- "@types/verror": "^1.10.3",
- "ajv": "^6.10.0",
- "crc": "^3.8.0",
- "iconv-corefoundation": "^1.1.7",
- "plist": "^3.0.4",
- "smart-buffer": "^4.0.2",
- "verror": "^1.10.0"
- },
- "bin": {
- "dmg-license": "bin/dmg-license.js"
- },
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/doctrine": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
@@ -11104,14 +10295,13 @@
"node": ">=0.10.0"
}
},
- "node_modules/dompurify": {
- "version": "3.3.1",
- "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz",
- "integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==",
- "license": "(MPL-2.0 OR Apache-2.0)",
- "optionalDependencies": {
- "@types/trusted-types": "^2.0.7"
- }
+ "node_modules/dom-accessibility-api": {
+ "version": "0.5.16",
+ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
+ "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
},
"node_modules/dotenv": {
"version": "16.6.1",
@@ -11165,36 +10355,86 @@
"node": ">= 0.4"
}
},
- "node_modules/eastasianwidth": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
- "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
- "devOptional": true,
- "license": "MIT"
+ "node_modules/duplexer2": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz",
+ "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "readable-stream": "^2.0.2"
+ }
},
- "node_modules/ee-first": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
- "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+ "node_modules/duplexer2/node_modules/isarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+ "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
+ "dev": true,
"license": "MIT"
},
- "node_modules/ejs": {
- "version": "3.1.10",
- "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
- "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==",
+ "node_modules/duplexer2/node_modules/readable-stream": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
+ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"dev": true,
- "license": "Apache-2.0",
+ "license": "MIT",
"dependencies": {
- "jake": "^10.8.5"
- },
- "bin": {
- "ejs": "bin/cli.js"
- },
- "engines": {
- "node": ">=0.10.0"
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
}
},
- "node_modules/electron": {
+ "node_modules/duplexer2/node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/duplexer2/node_modules/string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
+ "node_modules/eastasianwidth": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
+ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+ "license": "MIT"
+ },
+ "node_modules/ejs": {
+ "version": "3.1.10",
+ "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
+ "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "jake": "^10.8.5"
+ },
+ "bin": {
+ "ejs": "bin/cli.js"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/electron": {
"version": "39.2.7",
"resolved": "https://registry.npmjs.org/electron/-/electron-39.2.7.tgz",
"integrity": "sha512-KU0uFS6LSTh4aOIC3miolcbizOFP7N1M46VTYVfqIgFiuA2ilfNaOHLDS9tCMvwwHRowAsvqBrh9NgMXcTOHCQ==",
@@ -11213,18 +10453,18 @@
}
},
"node_modules/electron-builder": {
- "version": "26.4.0",
- "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-26.4.0.tgz",
- "integrity": "sha512-FCUqvdq2AULL+Db2SUGgjOYTbrgkPxZtCjqIZGnjH9p29pTWyesQqBIfvQBKa6ewqde87aWl49n/WyI/NyUBog==",
+ "version": "26.15.3",
+ "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-26.15.3.tgz",
+ "integrity": "sha512-a1KM5heqS3gQCZzizXEI8RjJy3QVogULPdeSknt76uLDpBIW/HDGsMg/XgP0riP6PI9COsRvFITKKGDqA8fJxA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "app-builder-lib": "26.4.0",
- "builder-util": "26.3.4",
- "builder-util-runtime": "9.5.1",
+ "app-builder-lib": "26.15.3",
+ "builder-util": "26.15.3",
+ "builder-util-runtime": "9.7.0",
"chalk": "^4.1.2",
"ci-info": "^4.2.0",
- "dmg-builder": "26.4.0",
+ "dmg-builder": "26.15.3",
"fs-extra": "^10.1.0",
"lazy-val": "^1.0.5",
"simple-update-notifier": "2.0.0",
@@ -11239,15 +10479,15 @@
}
},
"node_modules/electron-builder-squirrel-windows": {
- "version": "26.4.0",
- "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-26.4.0.tgz",
- "integrity": "sha512-7dvalY38xBzWNaoOJ4sqy2aGIEpl2S1gLPkkB0MHu1Hu5xKQ82il1mKSFlXs6fLpXUso/NmyjdHGlSHDRoG8/w==",
+ "version": "26.15.3",
+ "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-26.15.3.tgz",
+ "integrity": "sha512-Jc19XPV9y9+2bAdZPkXuVNGNIEFBq9poHC61l8Kv6FdK7DRG3+Ic0rerC0DXOaeHNz8yW0fg/JnF8GQROOF5MA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
- "app-builder-lib": "26.4.0",
- "builder-util": "26.3.4",
+ "app-builder-lib": "26.15.3",
+ "builder-util": "26.15.3",
"electron-winstaller": "5.4.0"
}
},
@@ -11290,17 +10530,18 @@
}
},
"node_modules/electron-publish": {
- "version": "26.3.4",
- "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-26.3.4.tgz",
- "integrity": "sha512-5/ouDPb73SkKuay2EXisPG60LTFTMNHWo2WLrK5GDphnWK9UC+yzYrzVeydj078Yk4WUXi0+TaaZsNd6Zt5k/A==",
+ "version": "26.15.3",
+ "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-26.15.3.tgz",
+ "integrity": "sha512-g/2bn8YTavY4cuS5F+jOS7zmZbXXBV8KZ8yHKfJjFPoKtzBqrpCdNPxBd3tqdBwP7BVd0lGzf7Bk2s0KesWZ4Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/fs-extra": "^9.0.11",
- "builder-util": "26.3.4",
- "builder-util-runtime": "9.5.1",
+ "aws4": "^1.13.2",
+ "builder-util": "26.15.3",
+ "builder-util-runtime": "9.7.0",
"chalk": "^4.1.2",
- "form-data": "^4.0.0",
+ "form-data": "^4.0.5",
"fs-extra": "^10.1.0",
"lazy-val": "^1.0.5",
"mime": "^2.5.2"
@@ -11322,9 +10563,9 @@
}
},
"node_modules/electron-publish/node_modules/jsonfile": {
- "version": "6.2.0",
- "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
- "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz",
+ "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -11367,19 +10608,6 @@
"tiny-typed-emitter": "^2.1.0"
}
},
- "node_modules/electron-updater/node_modules/builder-util-runtime": {
- "version": "9.7.0",
- "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.7.0.tgz",
- "integrity": "sha512-g/kR520giAFYkSXTzcmF3kqQq7wi8F6N6SzeDgZrqTBN+VHdmgWOyTdD1yD7AATDId/yXLvuP34CxW46/BwCdw==",
- "license": "MIT",
- "dependencies": {
- "debug": "^4.3.4",
- "sax": "^1.2.4"
- },
- "engines": {
- "node": ">=12.0.0"
- }
- },
"node_modules/electron-updater/node_modules/fs-extra": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
@@ -11499,6 +10727,7 @@
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "devOptional": true,
"license": "MIT"
},
"node_modules/encodeurl": {
@@ -11530,18 +10759,31 @@
}
},
"node_modules/enhanced-resolve": {
- "version": "5.18.4",
- "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz",
- "integrity": "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==",
+ "version": "5.24.1",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.1.tgz",
+ "integrity": "sha512-7DdUaTjmNwMcH2gLr1qycesKII3BK4RLy/mdAb7x10Lq7bR4aNKHt1BR1ZALSv0rPM/hF5wYF0PhGop/rJm8vw==",
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.4",
- "tapable": "^2.2.0"
+ "tapable": "^2.3.3"
},
"engines": {
"node": ">=10.13.0"
}
},
+ "node_modules/entities": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz",
+ "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
"node_modules/env-paths": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
@@ -11551,15 +10793,6 @@
"node": ">=6"
}
},
- "node_modules/env-var": {
- "version": "7.5.0",
- "resolved": "https://registry.npmjs.org/env-var/-/env-var-7.5.0.tgz",
- "integrity": "sha512-mKZOzLRN0ETzau2W2QXefbFjo5EF4yWq28OyKb9ICdeNhHJlOE/pHHnz4hdYJ9cNZXcJHo5xN4OT4pzuSHSNvA==",
- "license": "MIT",
- "engines": {
- "node": ">=10"
- }
- },
"node_modules/err-code": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz",
@@ -11705,6 +10938,7 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
@@ -11799,6 +11033,7 @@
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@@ -12005,6 +11240,57 @@
"node": "*"
}
},
+ "node_modules/eslint-plugin-sonarjs": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-sonarjs/-/eslint-plugin-sonarjs-4.1.0.tgz",
+ "integrity": "sha512-rh+FlVz0yfd2RNIb6WqSkuGh0addX/Qi5scwQ5FphXDFrM6fZKcxP1+attJ78yUKcyYfiu6MTaISPpAFPzqRJw==",
+ "dev": true,
+ "license": "LGPL-3.0-only",
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.12.2",
+ "builtin-modules": "^3.3.0",
+ "bytes": "^3.1.2",
+ "functional-red-black-tree": "^1.0.1",
+ "globals": "^17.6.0",
+ "jsx-ast-utils-x": "^0.1.0",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^10.2.5",
+ "scslre": "^0.3.0",
+ "semver": "^7.8.4",
+ "ts-api-utils": "^2.5.0",
+ "typescript": ">=5",
+ "yaml": "^2.9.0"
+ },
+ "peerDependencies": {
+ "eslint": "^8.0.0 || ^9.0.0 || ^10.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-sonarjs/node_modules/globals": {
+ "version": "17.7.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-17.7.0.tgz",
+ "integrity": "sha512-Czmyns5dUsq4seFBR/Kdydhmo8y9kC79hiSkPn0YcGtNnYWnrgt0vjrSjx9tspoDGWm2CMarffRuLjM4xUz8xg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint-plugin-sonarjs/node_modules/semver": {
+ "version": "7.8.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
+ "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/eslint-scope": {
"version": "8.4.0",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
@@ -12141,12 +11427,6 @@
"node": ">= 0.6"
}
},
- "node_modules/eventemitter3": {
- "version": "5.0.4",
- "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
- "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
- "license": "MIT"
- },
"node_modules/events-universal": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz",
@@ -12315,33 +11595,6 @@
"@types/yauzl": "^2.9.1"
}
},
- "node_modules/extsprintf": {
- "version": "1.4.1",
- "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz",
- "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==",
- "dev": true,
- "engines": [
- "node >=0.6.0"
- ],
- "license": "MIT",
- "optional": true
- },
- "node_modules/fast-content-type-parse": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz",
- "integrity": "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/fastify"
- },
- {
- "type": "opencollective",
- "url": "https://opencollective.com/fastify"
- }
- ],
- "license": "MIT"
- },
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -12391,6 +11644,16 @@
],
"license": "BSD-3-Clause"
},
+ "node_modules/fd-package-json": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/fd-package-json/-/fd-package-json-2.0.0.tgz",
+ "integrity": "sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "walk-up-path": "^4.0.0"
+ }
+ },
"node_modules/fd-slicer": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
@@ -12444,9 +11707,9 @@
"license": "MIT"
},
"node_modules/filelist": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
- "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==",
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz",
+ "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@@ -12454,9 +11717,9 @@
}
},
"node_modules/filelist/node_modules/brace-expansion": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
- "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz",
+ "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -12464,9 +11727,9 @@
}
},
"node_modules/filelist/node_modules/minimatch": {
- "version": "5.1.6",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
- "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
+ "version": "5.1.9",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz",
+ "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -12476,33 +11739,6 @@
"node": ">=10"
}
},
- "node_modules/filename-reserved-regex": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-3.0.0.tgz",
- "integrity": "sha512-hn4cQfU6GOT/7cFHXBqeBg2TbrMBgdD0kcjLhvSQYYwm3s4B6cjvBfb7nBALJLAXqmU5xajSa7X2NnUud/VCdw==",
- "license": "MIT",
- "engines": {
- "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/filenamify": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-6.0.0.tgz",
- "integrity": "sha512-vqIlNogKeyD3yzrm0yhRMQg8hOVwYcYRfjEoODd49iCprMn4HL85gK3HcykQE53EPIpX3HcAbGA5ELQv216dAQ==",
- "license": "MIT",
- "dependencies": {
- "filename-reserved-regex": "^3.0.0"
- },
- "engines": {
- "node": ">=16"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/finalhandler": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
@@ -12594,26 +11830,6 @@
"node": ">=12"
}
},
- "node_modules/follow-redirects": {
- "version": "1.15.11",
- "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
- "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
- "funding": [
- {
- "type": "individual",
- "url": "https://github.com/sponsors/RubenVerborgh"
- }
- ],
- "license": "MIT",
- "engines": {
- "node": ">=4.0"
- },
- "peerDependenciesMeta": {
- "debug": {
- "optional": true
- }
- }
- },
"node_modules/for-each": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
@@ -12634,8 +11850,8 @@
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
"integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
- "devOptional": true,
"license": "ISC",
+ "optional": true,
"dependencies": {
"cross-spawn": "^7.0.6",
"signal-exit": "^4.0.1"
@@ -12651,8 +11867,8 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
- "devOptional": true,
"license": "ISC",
+ "optional": true,
"engines": {
"node": ">=14"
},
@@ -12661,21 +11877,38 @@
}
},
"node_modules/form-data": {
- "version": "4.0.5",
- "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
- "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz",
+ "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
- "hasown": "^2.0.2",
- "mime-types": "^2.1.12"
+ "hasown": "^2.0.4",
+ "mime-types": "^2.1.35"
},
"engines": {
"node": ">= 6"
}
},
+ "node_modules/formatly": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/formatly/-/formatly-0.3.0.tgz",
+ "integrity": "sha512-9XNj/o4wrRFyhSMJOvsuyMwy8aUfBaZ1VrqHVfohyXf0Sw0e+yfKG+xZaY3arGCOMdwFsqObtzVOc1gU9KiT9w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fd-package-json": "^2.0.0"
+ },
+ "bin": {
+ "formatly": "bin/index.mjs"
+ },
+ "engines": {
+ "node": ">=18.3.0"
+ }
+ },
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -12759,8 +11992,8 @@
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz",
"integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==",
- "devOptional": true,
"license": "ISC",
+ "optional": true,
"dependencies": {
"minipass": "^7.0.3"
},
@@ -12819,6 +12052,13 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/functional-red-black-tree": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz",
+ "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/functions-have-names": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
@@ -12829,26 +12069,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/gauge": {
- "version": "4.0.4",
- "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz",
- "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==",
- "deprecated": "This package is no longer supported.",
- "license": "ISC",
- "dependencies": {
- "aproba": "^1.0.3 || ^2.0.0",
- "color-support": "^1.1.3",
- "console-control-strings": "^1.1.0",
- "has-unicode": "^2.0.1",
- "signal-exit": "^3.0.7",
- "string-width": "^4.2.3",
- "strip-ansi": "^6.0.1",
- "wide-align": "^1.1.5"
- },
- "engines": {
- "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
- }
- },
"node_modules/generator-function": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz",
@@ -12873,23 +12093,12 @@
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "dev": true,
"license": "ISC",
"engines": {
"node": "6.* || 8.* || >= 10.*"
}
},
- "node_modules/get-east-asian-width": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz",
- "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==",
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
@@ -12969,6 +12178,19 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/get-tsconfig": {
+ "version": "4.14.0",
+ "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz",
+ "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "resolve-pkg-maps": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
+ }
+ },
"node_modules/get-windows": {
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/get-windows/-/get-windows-9.3.0.tgz",
@@ -13428,6 +12650,32 @@
"node": ">=10"
}
},
+ "node_modules/global-directory": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz",
+ "integrity": "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ini": "4.1.1"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/global-directory/node_modules/ini": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz",
+ "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
"node_modules/globals": {
"version": "16.5.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz",
@@ -13572,6 +12820,7 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
@@ -13587,7 +12836,8 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
"integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==",
- "license": "ISC"
+ "license": "ISC",
+ "optional": true
},
"node_modules/hash-wasm": {
"version": "4.12.0",
@@ -13596,9 +12846,9 @@
"license": "MIT"
},
"node_modules/hasown": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
- "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
+ "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
@@ -13706,8 +12956,21 @@
"dev": true,
"license": "ISC"
},
- "node_modules/html-escaper": {
- "version": "2.0.2",
+ "node_modules/html-encoding-sniffer": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz",
+ "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@exodus/bytes": "^1.6.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
+ "node_modules/html-escaper": {
+ "version": "2.0.2",
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
"integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
"dev": true,
@@ -13790,30 +13053,12 @@
"node": ">= 14"
}
},
- "node_modules/iconv-corefoundation": {
- "version": "1.1.7",
- "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz",
- "integrity": "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "dependencies": {
- "cli-truncate": "^2.1.0",
- "node-addon-api": "^1.6.3"
- },
- "engines": {
- "node": "^8.11.2 || >=10"
- }
- },
"node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
- "devOptional": true,
"license": "MIT",
+ "optional": true,
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
@@ -13948,6 +13193,16 @@
"node": ">=12"
}
},
+ "node_modules/interpret": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz",
+ "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
"node_modules/ip-address": {
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz",
@@ -13966,217 +13221,6 @@
"node": ">= 0.10"
}
},
- "node_modules/ipull": {
- "version": "3.9.3",
- "resolved": "https://registry.npmjs.org/ipull/-/ipull-3.9.3.tgz",
- "integrity": "sha512-ZMkxaopfwKHwmEuGDYx7giNBdLxbHbRCWcQVA1D2eqE4crUguupfxej6s7UqbidYEwT69dkyumYkY8DPHIxF9g==",
- "license": "MIT",
- "dependencies": {
- "@tinyhttp/content-disposition": "^2.2.0",
- "async-retry": "^1.3.3",
- "chalk": "^5.3.0",
- "ci-info": "^4.0.0",
- "cli-spinners": "^2.9.2",
- "commander": "^10.0.0",
- "eventemitter3": "^5.0.1",
- "filenamify": "^6.0.0",
- "fs-extra": "^11.1.1",
- "is-unicode-supported": "^2.0.0",
- "lifecycle-utils": "^2.0.1",
- "lodash.debounce": "^4.0.8",
- "lowdb": "^7.0.1",
- "pretty-bytes": "^6.1.0",
- "pretty-ms": "^8.0.0",
- "sleep-promise": "^9.1.0",
- "slice-ansi": "^7.1.0",
- "stdout-update": "^4.0.1",
- "strip-ansi": "^7.1.0"
- },
- "bin": {
- "ipull": "dist/cli/cli.js"
- },
- "engines": {
- "node": ">=18.0.0"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/ido-pluto/ipull?sponsor=1"
- },
- "optionalDependencies": {
- "@reflink/reflink": "^0.1.16"
- }
- },
- "node_modules/ipull/node_modules/ansi-regex": {
- "version": "6.2.2",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
- "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-regex?sponsor=1"
- }
- },
- "node_modules/ipull/node_modules/ansi-styles": {
- "version": "6.2.3",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
- "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
- }
- },
- "node_modules/ipull/node_modules/chalk": {
- "version": "5.6.2",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
- "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
- "license": "MIT",
- "engines": {
- "node": "^12.17.0 || ^14.13 || >=16.0.0"
- },
- "funding": {
- "url": "https://github.com/chalk/chalk?sponsor=1"
- }
- },
- "node_modules/ipull/node_modules/commander": {
- "version": "10.0.1",
- "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz",
- "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==",
- "license": "MIT",
- "engines": {
- "node": ">=14"
- }
- },
- "node_modules/ipull/node_modules/fs-extra": {
- "version": "11.3.3",
- "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz",
- "integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==",
- "license": "MIT",
- "dependencies": {
- "graceful-fs": "^4.2.0",
- "jsonfile": "^6.0.1",
- "universalify": "^2.0.0"
- },
- "engines": {
- "node": ">=14.14"
- }
- },
- "node_modules/ipull/node_modules/is-fullwidth-code-point": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz",
- "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==",
- "license": "MIT",
- "dependencies": {
- "get-east-asian-width": "^1.3.1"
- },
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/ipull/node_modules/is-unicode-supported": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz",
- "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==",
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/ipull/node_modules/jsonfile": {
- "version": "6.2.0",
- "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
- "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
- "license": "MIT",
- "dependencies": {
- "universalify": "^2.0.0"
- },
- "optionalDependencies": {
- "graceful-fs": "^4.1.6"
- }
- },
- "node_modules/ipull/node_modules/lifecycle-utils": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/lifecycle-utils/-/lifecycle-utils-2.1.0.tgz",
- "integrity": "sha512-AnrXnE2/OF9PHCyFg0RSqsnQTzV991XaZA/buhFDoc58xU7rhSCDgCz/09Lqpsn4MpoPHt7TRAXV1kWZypFVsA==",
- "license": "MIT"
- },
- "node_modules/ipull/node_modules/parse-ms": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-3.0.0.tgz",
- "integrity": "sha512-Tpb8Z7r7XbbtBTrM9UhpkzzaMrqA2VXMT3YChzYltwV3P3pM6t8wl7TvpMnSTosz1aQAdVib7kdoys7vYOPerw==",
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/ipull/node_modules/pretty-ms": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-8.0.0.tgz",
- "integrity": "sha512-ASJqOugUF1bbzI35STMBUpZqdfYKlJugy6JBziGi2EE+AL5JPJGSzvpeVXojxrr0ViUYoToUjb5kjSEGf7Y83Q==",
- "license": "MIT",
- "dependencies": {
- "parse-ms": "^3.0.0"
- },
- "engines": {
- "node": ">=14.16"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/ipull/node_modules/slice-ansi": {
- "version": "7.1.2",
- "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz",
- "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==",
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^6.2.1",
- "is-fullwidth-code-point": "^5.0.0"
- },
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/chalk/slice-ansi?sponsor=1"
- }
- },
- "node_modules/ipull/node_modules/strip-ansi": {
- "version": "7.1.2",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
- "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
- "license": "MIT",
- "dependencies": {
- "ansi-regex": "^6.0.1"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/strip-ansi?sponsor=1"
- }
- },
- "node_modules/ipull/node_modules/universalify": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
- "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
- "license": "MIT",
- "engines": {
- "node": ">= 10.0.0"
- }
- },
"node_modules/is-alphabetical": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz",
@@ -14382,6 +13426,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "devOptional": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -14430,14 +13475,21 @@
"url": "https://github.com/sponsors/wooorm"
}
},
- "node_modules/is-interactive": {
+ "node_modules/is-installed-globally": {
"version": "1.0.0",
- "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz",
- "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==",
+ "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-1.0.0.tgz",
+ "integrity": "sha512-K55T22lfpQ63N4KEN57jZUAaAYqYHEe8veb/TycJRk9DdSCLLcovXz/mL6mOnhQaZsQGwPhuFopdQIlqGSEjiQ==",
"dev": true,
"license": "MIT",
+ "dependencies": {
+ "global-directory": "^4.0.1",
+ "is-path-inside": "^4.0.0"
+ },
"engines": {
- "node": ">=8"
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-lambda": {
@@ -14473,18 +13525,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/is-network-error": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.2.tgz",
- "integrity": "sha512-PhBY86zaxNZUuWP6h13Vu5oFe0XY6/UlKzQnYFELzGVHygP3MxmvTfYSG7GN3aIab/iWudSMgjSnG9Dq+nHrgA==",
- "license": "MIT",
- "engines": {
- "node": ">=16"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/is-number-object": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz",
@@ -14502,6 +13542,19 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-path-inside": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz",
+ "integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/is-plain-obj": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
@@ -14514,6 +13567,13 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/is-potential-custom-element-name": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
+ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/is-promise": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
@@ -14619,19 +13679,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/is-unicode-supported": {
- "version": "0.1.0",
- "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
- "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/is-weakmap": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz",
@@ -14702,6 +13749,7 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz",
"integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==",
+ "devOptional": true,
"license": "ISC",
"engines": {
"node": ">=16"
@@ -14793,24 +13841,12 @@
"node": ">= 0.4"
}
},
- "node_modules/its-fine": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/its-fine/-/its-fine-2.0.0.tgz",
- "integrity": "sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng==",
- "license": "MIT",
- "dependencies": {
- "@types/react-reconciler": "^0.28.9"
- },
- "peerDependencies": {
- "react": "^19.0.0"
- }
- },
"node_modules/jackspeak": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
"integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
- "devOptional": true,
"license": "BlueOak-1.0.0",
+ "optional": true,
"dependencies": {
"@isaacs/cliui": "^8.0.2"
},
@@ -14849,9 +13885,9 @@
}
},
"node_modules/jiti": {
- "version": "2.6.1",
- "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
- "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz",
+ "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==",
"license": "MIT",
"bin": {
"jiti": "lib/jiti-cli.mjs"
@@ -14866,15 +13902,6 @@
"url": "https://github.com/sponsors/panva"
}
},
- "node_modules/js-tiktoken": {
- "version": "1.0.21",
- "resolved": "https://registry.npmjs.org/js-tiktoken/-/js-tiktoken-1.0.21.tgz",
- "integrity": "sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g==",
- "license": "MIT",
- "dependencies": {
- "base64-js": "^1.5.1"
- }
- },
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -14893,30 +13920,119 @@
"js-yaml": "bin/js-yaml.js"
}
},
- "node_modules/jsesc": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
- "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "node_modules/jsdom": {
+ "version": "29.1.1",
+ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz",
+ "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==",
"dev": true,
"license": "MIT",
- "bin": {
- "jsesc": "bin/jsesc"
+ "dependencies": {
+ "@asamuzakjp/css-color": "^5.1.11",
+ "@asamuzakjp/dom-selector": "^7.1.1",
+ "@bramus/specificity": "^2.4.2",
+ "@csstools/css-syntax-patches-for-csstree": "^1.1.3",
+ "@exodus/bytes": "^1.15.0",
+ "css-tree": "^3.2.1",
+ "data-urls": "^7.0.0",
+ "decimal.js": "^10.6.0",
+ "html-encoding-sniffer": "^6.0.0",
+ "is-potential-custom-element-name": "^1.0.1",
+ "lru-cache": "^11.3.5",
+ "parse5": "^8.0.1",
+ "saxes": "^6.0.0",
+ "symbol-tree": "^3.2.4",
+ "tough-cookie": "^6.0.1",
+ "undici": "^7.25.0",
+ "w3c-xmlserializer": "^5.0.0",
+ "webidl-conversions": "^8.0.1",
+ "whatwg-mimetype": "^5.0.0",
+ "whatwg-url": "^16.0.1",
+ "xml-name-validator": "^5.0.0"
},
"engines": {
- "node": ">=6"
+ "node": "^20.19.0 || ^22.13.0 || >=24.0.0"
+ },
+ "peerDependencies": {
+ "canvas": "^3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "canvas": {
+ "optional": true
+ }
}
},
- "node_modules/json-bignum": {
- "version": "0.0.3",
- "resolved": "https://registry.npmjs.org/json-bignum/-/json-bignum-0.0.3.tgz",
- "integrity": "sha512-2WHyXj3OfHSgNyuzDbSxI1w2jgw5gkWSWhS7Qg4bWXx1nLk3jnbwfUeS0PSba3IzpTUWdHxBieELUzXRjQB2zg==",
+ "node_modules/jsdom/node_modules/lru-cache": {
+ "version": "11.5.2",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz",
+ "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
"engines": {
- "node": ">=0.8"
+ "node": "20 || >=22"
}
},
- "node_modules/json-buffer": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "node_modules/jsdom/node_modules/tr46": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz",
+ "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "punycode": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/jsdom/node_modules/webidl-conversions": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz",
+ "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/jsdom/node_modules/whatwg-url": {
+ "version": "16.0.1",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz",
+ "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@exodus/bytes": "^1.11.0",
+ "tr46": "^6.0.0",
+ "webidl-conversions": "^8.0.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json-bignum": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/json-bignum/-/json-bignum-0.0.3.tgz",
+ "integrity": "sha512-2WHyXj3OfHSgNyuzDbSxI1w2jgw5gkWSWhS7Qg4bWXx1nLk3jnbwfUeS0PSba3IzpTUWdHxBieELUzXRjQB2zg==",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
"license": "MIT"
},
@@ -14984,6 +14100,16 @@
"node": ">=4.0"
}
},
+ "node_modules/jsx-ast-utils-x": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/jsx-ast-utils-x/-/jsx-ast-utils-x-0.1.0.tgz",
+ "integrity": "sha512-eQQBjBnsVtGacsG9uJNB8qOr3yA8rga4wAaGG1qRcBzSIvfhERLrWxMAM1hp5fcS6Abo8M4+bUBTekYR0qTPQw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
"node_modules/jszip": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
@@ -15083,6 +14209,68 @@
"json-buffer": "3.0.1"
}
},
+ "node_modules/kleur": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
+ "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/knip": {
+ "version": "6.25.0",
+ "resolved": "https://registry.npmjs.org/knip/-/knip-6.25.0.tgz",
+ "integrity": "sha512-Q3n41VjOOB/aqsbxb8kallAcFKrUz3b2S5fD5pTODljVpP01t+rvAgy2x3j0Cq8yEpRRHNdar1vHuqFfGuIakQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/webpro"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/knip"
+ }
+ ],
+ "license": "ISC",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "formatly": "^0.3.0",
+ "get-tsconfig": "4.14.0",
+ "jiti": "^2.7.0",
+ "oxc-parser": "^0.137.0",
+ "oxc-resolver": "11.21.3",
+ "picomatch": "^4.0.4",
+ "smol-toml": "^1.6.1",
+ "strip-json-comments": "5.0.3",
+ "tinyglobby": "^0.2.17",
+ "unbash": "^4.0.1",
+ "yaml": "^2.9.0",
+ "zod": "^4.1.11"
+ },
+ "bin": {
+ "knip": "bin/knip.js",
+ "knip-bun": "bin/knip-bun.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/knip/node_modules/strip-json-comments": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz",
+ "integrity": "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/kokoro-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/kokoro-js/-/kokoro-js-1.2.1.tgz",
@@ -15093,39 +14281,6 @@
"phonemizer": "^1.2.1"
}
},
- "node_modules/langsmith": {
- "version": "0.7.11",
- "resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.7.11.tgz",
- "integrity": "sha512-kK7tjvLo3KbyrXonZDJJMvX6jE5Ek9uwqtS2WX8AEGc5IwLoMrJlGR8wWk1UD8H/x0goiJDMsRht4qZd7Ijvcg==",
- "license": "MIT",
- "dependencies": {
- "p-queue": "6.6.2"
- },
- "peerDependencies": {
- "@opentelemetry/api": "*",
- "@opentelemetry/exporter-trace-otlp-proto": "*",
- "@opentelemetry/sdk-trace-base": "*",
- "openai": "*",
- "ws": ">=7"
- },
- "peerDependenciesMeta": {
- "@opentelemetry/api": {
- "optional": true
- },
- "@opentelemetry/exporter-trace-otlp-proto": {
- "optional": true
- },
- "@opentelemetry/sdk-trace-base": {
- "optional": true
- },
- "openai": {
- "optional": true
- },
- "ws": {
- "optional": true
- }
- }
- },
"node_modules/lazy-val": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz",
@@ -15155,12 +14310,6 @@
"immediate": "~3.0.5"
}
},
- "node_modules/lifecycle-utils": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/lifecycle-utils/-/lifecycle-utils-3.0.1.tgz",
- "integrity": "sha512-Qt/Jl5dsNIsyCAZsHB6x3mbwHFn0HJbdmvF49sVX/bHgX2cW7+G+U+I67Zw+TPM1Sr21Gb2nfJMd2g6iUcI1EQ==",
- "license": "MIT"
- },
"node_modules/lightningcss": {
"version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz",
@@ -15427,9 +14576,9 @@
}
},
"node_modules/lodash": {
- "version": "4.17.21",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
- "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
+ "version": "4.18.1",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
+ "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
"dev": true,
"license": "MIT"
},
@@ -15445,12 +14594,6 @@
"integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
"license": "MIT"
},
- "node_modules/lodash.debounce": {
- "version": "4.0.8",
- "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
- "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
- "license": "MIT"
- },
"node_modules/lodash.escaperegexp": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz",
@@ -15471,23 +14614,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/log-symbols": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
- "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "chalk": "^4.1.0",
- "is-unicode-supported": "^0.1.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/long": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
@@ -15527,21 +14653,6 @@
"underscore": "^1.13.1"
}
},
- "node_modules/lowdb": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/lowdb/-/lowdb-7.0.1.tgz",
- "integrity": "sha512-neJAj8GwF0e8EpycYIDFqEPcx9Qz4GUho20jWFR7YiFeXzF1YMLdxB36PypcTSPMA+4+LvgyMacYhlr18Zlymw==",
- "license": "MIT",
- "dependencies": {
- "steno": "^4.0.2"
- },
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/typicode"
- }
- },
"node_modules/lowercase-keys": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
@@ -15561,6 +14672,17 @@
"yallist": "^3.0.2"
}
},
+ "node_modules/lz-string": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
+ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "bin": {
+ "lz-string": "bin/bin.js"
+ }
+ },
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
@@ -15598,29 +14720,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/make-fetch-happen": {
- "version": "14.0.3",
- "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-14.0.3.tgz",
- "integrity": "sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "@npmcli/agent": "^3.0.0",
- "cacache": "^19.0.1",
- "http-cache-semantics": "^4.1.1",
- "minipass": "^7.0.2",
- "minipass-fetch": "^4.0.0",
- "minipass-flush": "^1.0.5",
- "minipass-pipeline": "^1.2.4",
- "negotiator": "^1.0.0",
- "proc-log": "^5.0.0",
- "promise-retry": "^2.0.1",
- "ssri": "^12.0.0"
- },
- "engines": {
- "node": "^18.17.0 || >=20.5.0"
- }
- },
"node_modules/mammoth": {
"version": "1.12.0",
"resolved": "https://registry.npmjs.org/mammoth/-/mammoth-1.12.0.tgz",
@@ -15996,6 +15095,13 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/mdn-data": {
+ "version": "2.27.1",
+ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz",
+ "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==",
+ "dev": true,
+ "license": "CC0-1.0"
+ },
"node_modules/media-typer": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
@@ -16005,15 +15111,6 @@
"node": ">= 0.8"
}
},
- "node_modules/memory-stream": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/memory-stream/-/memory-stream-1.0.0.tgz",
- "integrity": "sha512-Wm13VcsPIMdG96dzILfij09PvuS3APtcKNh7M28FsCA/w6+1mjR7hhPmfFNoilX9xU7wTdhsH5lJAm6XNzdtww==",
- "license": "MIT",
- "dependencies": {
- "readable-stream": "^3.4.0"
- }
- },
"node_modules/merge-descriptors": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
@@ -16590,811 +15687,308 @@
"url": "https://github.com/sponsors/unifiedjs"
},
{
- "type": "OpenCollective",
- "url": "https://opencollective.com/unified"
- }
- ],
- "license": "MIT"
- },
- "node_modules/mime": {
- "version": "2.6.0",
- "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz",
- "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==",
- "dev": true,
- "license": "MIT",
- "bin": {
- "mime": "cli.js"
- },
- "engines": {
- "node": ">=4.0.0"
- }
- },
- "node_modules/mime-db": {
- "version": "1.52.0",
- "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
- "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/mime-types": {
- "version": "2.1.35",
- "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
- "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
- "license": "MIT",
- "dependencies": {
- "mime-db": "1.52.0"
- },
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/mimic-fn": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
- "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/mimic-function": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz",
- "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==",
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/mimic-response": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
- "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==",
- "license": "MIT",
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/mini-svg-data-uri": {
- "version": "1.4.4",
- "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz",
- "integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==",
- "license": "MIT",
- "bin": {
- "mini-svg-data-uri": "cli.js"
- }
- },
- "node_modules/minimatch": {
- "version": "10.1.1",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz",
- "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==",
- "dev": true,
- "license": "BlueOak-1.0.0",
- "dependencies": {
- "@isaacs/brace-expansion": "^5.0.0"
- },
- "engines": {
- "node": "20 || >=22"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/minimist": {
- "version": "1.2.8",
- "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
- "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
- "license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/minipass": {
- "version": "7.1.2",
- "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
- "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
- "license": "ISC",
- "engines": {
- "node": ">=16 || 14 >=14.17"
- }
- },
- "node_modules/minipass-collect": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz",
- "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==",
- "devOptional": true,
- "license": "ISC",
- "dependencies": {
- "minipass": "^7.0.3"
- },
- "engines": {
- "node": ">=16 || 14 >=14.17"
- }
- },
- "node_modules/minipass-fetch": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-4.0.1.tgz",
- "integrity": "sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "minipass": "^7.0.3",
- "minipass-sized": "^1.0.3",
- "minizlib": "^3.0.1"
- },
- "engines": {
- "node": "^18.17.0 || >=20.5.0"
- },
- "optionalDependencies": {
- "encoding": "^0.1.13"
- }
- },
- "node_modules/minipass-flush": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz",
- "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==",
- "devOptional": true,
- "license": "ISC",
- "dependencies": {
- "minipass": "^3.0.0"
- },
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/minipass-flush/node_modules/minipass": {
- "version": "3.3.6",
- "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
- "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
- "devOptional": true,
- "license": "ISC",
- "dependencies": {
- "yallist": "^4.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/minipass-flush/node_modules/yallist": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
- "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
- "devOptional": true,
- "license": "ISC"
- },
- "node_modules/minipass-pipeline": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz",
- "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==",
- "devOptional": true,
- "license": "ISC",
- "dependencies": {
- "minipass": "^3.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/minipass-pipeline/node_modules/minipass": {
- "version": "3.3.6",
- "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
- "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
- "devOptional": true,
- "license": "ISC",
- "dependencies": {
- "yallist": "^4.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/minipass-pipeline/node_modules/yallist": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
- "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
- "devOptional": true,
- "license": "ISC"
- },
- "node_modules/minipass-sized": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz",
- "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==",
- "devOptional": true,
- "license": "ISC",
- "dependencies": {
- "minipass": "^3.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/minipass-sized/node_modules/minipass": {
- "version": "3.3.6",
- "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
- "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
- "devOptional": true,
- "license": "ISC",
- "dependencies": {
- "yallist": "^4.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/minipass-sized/node_modules/yallist": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
- "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
- "devOptional": true,
- "license": "ISC"
- },
- "node_modules/minizlib": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz",
- "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==",
- "license": "MIT",
- "dependencies": {
- "minipass": "^7.1.2"
- },
- "engines": {
- "node": ">= 18"
- }
- },
- "node_modules/mkdirp": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
- "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
- "license": "MIT",
- "bin": {
- "mkdirp": "bin/cmd.js"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/mkdirp-classic": {
- "version": "0.5.3",
- "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
- "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
- "license": "MIT"
- },
- "node_modules/motion": {
- "version": "12.27.1",
- "resolved": "https://registry.npmjs.org/motion/-/motion-12.27.1.tgz",
- "integrity": "sha512-FAZTPDm1LccBdWSL46WLnEdTSHmdVx+fdWK8f61qBQn67MmFefXLXlrwy94rK2DDsd9A50Gj8H+LYCgQ/cQlFg==",
- "license": "MIT",
- "dependencies": {
- "framer-motion": "^12.27.1",
- "tslib": "^2.4.0"
- },
- "peerDependencies": {
- "@emotion/is-prop-valid": "*",
- "react": "^18.0.0 || ^19.0.0",
- "react-dom": "^18.0.0 || ^19.0.0"
- },
- "peerDependenciesMeta": {
- "@emotion/is-prop-valid": {
- "optional": true
- },
- "react": {
- "optional": true
- },
- "react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/motion-dom": {
- "version": "12.27.1",
- "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.27.1.tgz",
- "integrity": "sha512-V/53DA2nBqKl9O2PMJleSUb/G0dsMMeZplZwgIQf5+X0bxIu7Q1cTv6DrjvTTGYRm3+7Y5wMlRZ1wT61boU/bQ==",
- "license": "MIT",
- "dependencies": {
- "motion-utils": "^12.24.10"
- }
- },
- "node_modules/motion-utils": {
- "version": "12.24.10",
- "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.24.10.tgz",
- "integrity": "sha512-x5TFgkCIP4pPsRLpKoI86jv/q8t8FQOiM/0E8QKBzfMozWHfkKap2gA1hOki+B5g3IsBNpxbUnfOum1+dgvYww==",
- "license": "MIT"
- },
- "node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "license": "MIT"
- },
- "node_modules/mustache": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz",
- "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==",
- "license": "MIT",
- "bin": {
- "mustache": "bin/mustache"
- }
- },
- "node_modules/nanoid": {
- "version": "3.3.11",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
- "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "bin": {
- "nanoid": "bin/nanoid.cjs"
- },
- "engines": {
- "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
- }
- },
- "node_modules/napi-build-utils": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
- "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
- "license": "MIT"
- },
- "node_modules/natural-compare": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
- "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/negotiator": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
- "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/ngraph.events": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/ngraph.events/-/ngraph.events-1.4.0.tgz",
- "integrity": "sha512-NeDGI4DSyjBNBRtA86222JoYietsmCXbs8CEB0dZ51Xeh4lhVl1y3wpWLumczvnha8sFQIW4E0vvVWwgmX2mGw==",
- "license": "BSD-3-Clause"
- },
- "node_modules/ngraph.forcelayout": {
- "version": "3.3.1",
- "resolved": "https://registry.npmjs.org/ngraph.forcelayout/-/ngraph.forcelayout-3.3.1.tgz",
- "integrity": "sha512-MKBuEh1wujyQHFTW57y5vd/uuEOK0XfXYxm3lC7kktjJLRdt/KEKEknyOlc6tjXflqBKEuYBBcu7Ax5VY+S6aw==",
- "license": "BSD-3-Clause",
- "dependencies": {
- "ngraph.events": "^1.0.0",
- "ngraph.merge": "^1.0.0",
- "ngraph.random": "^1.0.0"
- }
- },
- "node_modules/ngraph.graph": {
- "version": "20.1.1",
- "resolved": "https://registry.npmjs.org/ngraph.graph/-/ngraph.graph-20.1.1.tgz",
- "integrity": "sha512-KNtZWYzYe7SMOuG3vvROznU+fkPmL5cGYFsWjqt+Ob1uF5xZz5EjomtsNOZEIwVuD37/zokeEqNK1ghY4/fhDg==",
- "license": "BSD-3-Clause",
- "dependencies": {
- "ngraph.events": "^1.4.0"
- }
- },
- "node_modules/ngraph.merge": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/ngraph.merge/-/ngraph.merge-1.0.0.tgz",
- "integrity": "sha512-5J8YjGITUJeapsomtTALYsw7rFveYkM+lBj3QiYZ79EymQcuri65Nw3knQtFxQBU1r5iOaVRXrSwMENUPK62Vg==",
- "license": "MIT"
- },
- "node_modules/ngraph.random": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/ngraph.random/-/ngraph.random-1.2.0.tgz",
- "integrity": "sha512-4EUeAGbB2HWX9njd6bP6tciN6ByJfoaAvmVL9QTaZSeXrW46eNGA9GajiXiPBbvFqxUWFkEbyo6x5qsACUuVfA==",
- "license": "BSD-3-Clause"
- },
- "node_modules/node-abi": {
- "version": "4.25.0",
- "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-4.25.0.tgz",
- "integrity": "sha512-BRrQZc23ljOLms7EXVds3MOpB59/x7gaORodNuIwt96JKlflUmrOgv5hSJZEEM/WkW3uXpjZ4x1wcFu8V9mTpw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "semver": "^7.6.3"
- },
- "engines": {
- "node": ">=22.12.0"
- }
- },
- "node_modules/node-abi/node_modules/semver": {
- "version": "7.7.3",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
- "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
- "dev": true,
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/node-addon-api": {
- "version": "1.7.2",
- "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz",
- "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==",
- "dev": true,
- "license": "MIT",
- "optional": true
- },
- "node_modules/node-api-headers": {
- "version": "1.7.0",
- "resolved": "https://registry.npmjs.org/node-api-headers/-/node-api-headers-1.7.0.tgz",
- "integrity": "sha512-uJMGdkhVwu9+I3UsVvI3KW6ICAy/yDfsu5Br9rSnTtY3WpoaComXvKloiV5wtx0Md2rn0B9n29Ys2WMNwWxj9A==",
- "license": "MIT"
- },
- "node_modules/node-api-version": {
- "version": "0.2.1",
- "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.2.1.tgz",
- "integrity": "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "semver": "^7.3.5"
- }
- },
- "node_modules/node-api-version/node_modules/semver": {
- "version": "7.7.3",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
- "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
- "dev": true,
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/node-ensure": {
- "version": "0.0.0",
- "resolved": "https://registry.npmjs.org/node-ensure/-/node-ensure-0.0.0.tgz",
- "integrity": "sha512-DRI60hzo2oKN1ma0ckc6nQWlHU69RH6xN0sjQTjMpChPfTYvKZdcQFfdYK2RWbJcKyUizSIy/l8OTGxMAM1QDw==",
- "license": "MIT"
- },
- "node_modules/node-fetch": {
- "version": "2.7.0",
- "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
- "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "whatwg-url": "^5.0.0"
- },
- "engines": {
- "node": "4.x || >=6.0.0"
- },
- "peerDependencies": {
- "encoding": "^0.1.0"
- },
- "peerDependenciesMeta": {
- "encoding": {
- "optional": true
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
}
- }
+ ],
+ "license": "MIT"
},
- "node_modules/node-gyp": {
- "version": "11.5.0",
- "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-11.5.0.tgz",
- "integrity": "sha512-ra7Kvlhxn5V9Slyus0ygMa2h+UqExPqUIkfk7Pc8QTLT956JLSy51uWFwHtIYy0vI8cB4BDhc/S03+880My/LQ==",
+ "node_modules/mime": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz",
+ "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==",
"dev": true,
"license": "MIT",
- "dependencies": {
- "env-paths": "^2.2.0",
- "exponential-backoff": "^3.1.1",
- "graceful-fs": "^4.2.6",
- "make-fetch-happen": "^14.0.3",
- "nopt": "^8.0.0",
- "proc-log": "^5.0.0",
- "semver": "^7.3.5",
- "tar": "^7.4.3",
- "tinyglobby": "^0.2.12",
- "which": "^5.0.0"
- },
"bin": {
- "node-gyp": "bin/node-gyp.js"
+ "mime": "cli.js"
},
"engines": {
- "node": "^18.17.0 || >=20.5.0"
+ "node": ">=4.0.0"
}
},
- "node_modules/node-gyp/node_modules/chownr": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
- "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==",
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"dev": true,
- "license": "BlueOak-1.0.0",
+ "license": "MIT",
"engines": {
- "node": ">=18"
+ "node": ">= 0.6"
}
},
- "node_modules/node-gyp/node_modules/semver": {
- "version": "7.7.3",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
- "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dev": true,
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
},
"engines": {
- "node": ">=10"
+ "node": ">= 0.6"
}
},
- "node_modules/node-gyp/node_modules/tar": {
- "version": "7.5.3",
- "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.3.tgz",
- "integrity": "sha512-ENg5JUHUm2rDD7IvKNFGzyElLXNjachNLp6RaGf4+JOgxXHkqA+gq81ZAMCUmtMtqBsoU62lcp6S27g1LCYGGQ==",
- "dev": true,
- "license": "BlueOak-1.0.0",
- "dependencies": {
- "@isaacs/fs-minipass": "^4.0.0",
- "chownr": "^3.0.0",
- "minipass": "^7.1.2",
- "minizlib": "^3.1.0",
- "yallist": "^5.0.0"
- },
+ "node_modules/mimic-response": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
+ "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==",
+ "license": "MIT",
"engines": {
- "node": ">=18"
+ "node": ">=4"
}
},
- "node_modules/node-gyp/node_modules/yallist": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
- "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==",
+ "node_modules/minimatch": {
+ "version": "10.2.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
+ "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
"dev": true,
"license": "BlueOak-1.0.0",
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/node-llama-cpp": {
- "version": "3.15.0",
- "resolved": "https://registry.npmjs.org/node-llama-cpp/-/node-llama-cpp-3.15.0.tgz",
- "integrity": "sha512-xQKl+MvKiA5QNi/CTwqLKMos7hefhRVyzJuNIAEwl7zvOoF+gNMOXEsR4Ojwl7qvgpcjsVeGKWSK3Rb6zoUP1w==",
- "hasInstallScript": true,
- "license": "MIT",
"dependencies": {
- "@huggingface/jinja": "^0.5.3",
- "async-retry": "^1.3.3",
- "bytes": "^3.1.2",
- "chalk": "^5.4.1",
- "chmodrp": "^1.0.2",
- "cmake-js": "^7.4.0",
- "cross-spawn": "^7.0.6",
- "env-var": "^7.5.0",
- "filenamify": "^6.0.0",
- "fs-extra": "^11.3.0",
- "ignore": "^7.0.4",
- "ipull": "^3.9.2",
- "is-unicode-supported": "^2.1.0",
- "lifecycle-utils": "^3.0.1",
- "log-symbols": "^7.0.0",
- "nanoid": "^5.1.5",
- "node-addon-api": "^8.3.1",
- "octokit": "^5.0.3",
- "ora": "^8.2.0",
- "pretty-ms": "^9.2.0",
- "proper-lockfile": "^4.1.2",
- "semver": "^7.7.1",
- "simple-git": "^3.27.0",
- "slice-ansi": "^7.1.0",
- "stdout-update": "^4.0.1",
- "strip-ansi": "^7.1.0",
- "validate-npm-package-name": "^6.0.0",
- "which": "^5.0.0",
- "yargs": "^17.7.2"
- },
- "bin": {
- "nlc": "dist/cli/cli.js",
- "node-llama-cpp": "dist/cli/cli.js"
+ "brace-expansion": "^5.0.5"
},
"engines": {
- "node": ">=20.0.0"
+ "node": "18 || 20 || >=22"
},
"funding": {
- "type": "github",
- "url": "https://github.com/sponsors/giladgd"
- },
- "optionalDependencies": {
- "@node-llama-cpp/linux-arm64": "3.15.0",
- "@node-llama-cpp/linux-armv7l": "3.15.0",
- "@node-llama-cpp/linux-x64": "3.15.0",
- "@node-llama-cpp/linux-x64-cuda": "3.15.0",
- "@node-llama-cpp/linux-x64-cuda-ext": "3.15.0",
- "@node-llama-cpp/linux-x64-vulkan": "3.15.0",
- "@node-llama-cpp/mac-arm64-metal": "3.15.0",
- "@node-llama-cpp/mac-x64": "3.15.0",
- "@node-llama-cpp/win-arm64": "3.15.0",
- "@node-llama-cpp/win-x64": "3.15.0",
- "@node-llama-cpp/win-x64-cuda": "3.15.0",
- "@node-llama-cpp/win-x64-cuda-ext": "3.15.0",
- "@node-llama-cpp/win-x64-vulkan": "3.15.0"
- },
- "peerDependencies": {
- "typescript": ">=5.0.0"
- },
- "peerDependenciesMeta": {
- "typescript": {
- "optional": true
- }
+ "url": "https://github.com/sponsors/isaacs"
}
},
- "node_modules/node-llama-cpp/node_modules/@huggingface/jinja": {
- "version": "0.5.3",
- "resolved": "https://registry.npmjs.org/@huggingface/jinja/-/jinja-0.5.3.tgz",
- "integrity": "sha512-asqfZ4GQS0hD876Uw4qiUb7Tr/V5Q+JZuo2L+BtdrD4U40QU58nIRq3ZSgAzJgT874VLjhGVacaYfrdpXtEvtA==",
+ "node_modules/minimatch/node_modules/balanced-match": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+ "dev": true,
"license": "MIT",
"engines": {
- "node": ">=18"
+ "node": "18 || 20 || >=22"
}
},
- "node_modules/node-llama-cpp/node_modules/ansi-regex": {
- "version": "6.2.2",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
- "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
+ "node_modules/minimatch/node_modules/brace-expansion": {
+ "version": "5.0.7",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz",
+ "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==",
+ "dev": true,
"license": "MIT",
- "engines": {
- "node": ">=12"
+ "dependencies": {
+ "balanced-match": "^4.0.2"
},
- "funding": {
- "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ "engines": {
+ "node": "18 || 20 || >=22"
}
},
- "node_modules/node-llama-cpp/node_modules/ansi-styles": {
- "version": "6.2.3",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
- "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
+ "node_modules/minimist": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"license": "MIT",
- "engines": {
- "node": ">=12"
- },
"funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/node-llama-cpp/node_modules/chalk": {
- "version": "5.6.2",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
- "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
- "license": "MIT",
+ "node_modules/minipass": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
+ "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+ "license": "ISC",
"engines": {
- "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/minipass-collect": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz",
+ "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==",
+ "license": "ISC",
+ "optional": true,
+ "dependencies": {
+ "minipass": "^7.0.3"
},
- "funding": {
- "url": "https://github.com/chalk/chalk?sponsor=1"
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
}
},
- "node_modules/node-llama-cpp/node_modules/cli-cursor": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz",
- "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==",
- "license": "MIT",
+ "node_modules/minipass-flush": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz",
+ "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==",
+ "license": "ISC",
+ "optional": true,
"dependencies": {
- "restore-cursor": "^5.0.0"
+ "minipass": "^3.0.0"
},
"engines": {
- "node": ">=18"
+ "node": ">= 8"
+ }
+ },
+ "node_modules/minipass-flush/node_modules/minipass": {
+ "version": "3.3.6",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+ "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+ "license": "ISC",
+ "optional": true,
+ "dependencies": {
+ "yallist": "^4.0.0"
},
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "engines": {
+ "node": ">=8"
}
},
- "node_modules/node-llama-cpp/node_modules/emoji-regex": {
- "version": "10.6.0",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
- "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
- "license": "MIT"
+ "node_modules/minipass-flush/node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "license": "ISC",
+ "optional": true
},
- "node_modules/node-llama-cpp/node_modules/fs-extra": {
- "version": "11.3.3",
- "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz",
- "integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==",
- "license": "MIT",
+ "node_modules/minipass-pipeline": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz",
+ "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==",
+ "license": "ISC",
+ "optional": true,
"dependencies": {
- "graceful-fs": "^4.2.0",
- "jsonfile": "^6.0.1",
- "universalify": "^2.0.0"
+ "minipass": "^3.0.0"
},
"engines": {
- "node": ">=14.14"
+ "node": ">=8"
}
},
- "node_modules/node-llama-cpp/node_modules/ignore": {
- "version": "7.0.5",
- "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
- "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
- "license": "MIT",
+ "node_modules/minipass-pipeline/node_modules/minipass": {
+ "version": "3.3.6",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+ "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+ "license": "ISC",
+ "optional": true,
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
"engines": {
- "node": ">= 4"
+ "node": ">=8"
}
},
- "node_modules/node-llama-cpp/node_modules/is-fullwidth-code-point": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz",
- "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==",
- "license": "MIT",
+ "node_modules/minipass-pipeline/node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "license": "ISC",
+ "optional": true
+ },
+ "node_modules/minipass-sized": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz",
+ "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==",
+ "license": "ISC",
+ "optional": true,
"dependencies": {
- "get-east-asian-width": "^1.3.1"
+ "minipass": "^3.0.0"
},
"engines": {
- "node": ">=18"
+ "node": ">=8"
+ }
+ },
+ "node_modules/minipass-sized/node_modules/minipass": {
+ "version": "3.3.6",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+ "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+ "license": "ISC",
+ "optional": true,
+ "dependencies": {
+ "yallist": "^4.0.0"
},
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "engines": {
+ "node": ">=8"
}
},
- "node_modules/node-llama-cpp/node_modules/is-interactive": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz",
- "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==",
+ "node_modules/minipass-sized/node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "license": "ISC",
+ "optional": true
+ },
+ "node_modules/minizlib": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz",
+ "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==",
"license": "MIT",
- "engines": {
- "node": ">=12"
+ "dependencies": {
+ "minipass": "^7.1.2"
},
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "engines": {
+ "node": ">= 18"
}
},
- "node_modules/node-llama-cpp/node_modules/is-unicode-supported": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz",
- "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==",
+ "node_modules/mkdirp": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
+ "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
"license": "MIT",
- "engines": {
- "node": ">=18"
+ "optional": true,
+ "bin": {
+ "mkdirp": "bin/cmd.js"
},
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "engines": {
+ "node": ">=10"
}
},
- "node_modules/node-llama-cpp/node_modules/jsonfile": {
- "version": "6.2.0",
- "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
- "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
+ "node_modules/mkdirp-classic": {
+ "version": "0.5.3",
+ "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
+ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
+ "license": "MIT"
+ },
+ "node_modules/motion": {
+ "version": "12.27.1",
+ "resolved": "https://registry.npmjs.org/motion/-/motion-12.27.1.tgz",
+ "integrity": "sha512-FAZTPDm1LccBdWSL46WLnEdTSHmdVx+fdWK8f61qBQn67MmFefXLXlrwy94rK2DDsd9A50Gj8H+LYCgQ/cQlFg==",
"license": "MIT",
"dependencies": {
- "universalify": "^2.0.0"
+ "framer-motion": "^12.27.1",
+ "tslib": "^2.4.0"
},
- "optionalDependencies": {
- "graceful-fs": "^4.1.6"
+ "peerDependencies": {
+ "@emotion/is-prop-valid": "*",
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/is-prop-valid": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ }
}
},
- "node_modules/node-llama-cpp/node_modules/log-symbols": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz",
- "integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==",
+ "node_modules/motion-dom": {
+ "version": "12.27.1",
+ "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.27.1.tgz",
+ "integrity": "sha512-V/53DA2nBqKl9O2PMJleSUb/G0dsMMeZplZwgIQf5+X0bxIu7Q1cTv6DrjvTTGYRm3+7Y5wMlRZ1wT61boU/bQ==",
"license": "MIT",
"dependencies": {
- "is-unicode-supported": "^2.0.0",
- "yoctocolors": "^2.1.1"
- },
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "motion-utils": "^12.24.10"
}
},
- "node_modules/node-llama-cpp/node_modules/nanoid": {
- "version": "5.1.6",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.6.tgz",
- "integrity": "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==",
+ "node_modules/motion-utils": {
+ "version": "12.24.10",
+ "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.24.10.tgz",
+ "integrity": "sha512-x5TFgkCIP4pPsRLpKoI86jv/q8t8FQOiM/0E8QKBzfMozWHfkKap2gA1hOki+B5g3IsBNpxbUnfOum1+dgvYww==",
+ "license": "MIT"
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"funding": [
{
"type": "github",
@@ -17403,184 +15997,266 @@
],
"license": "MIT",
"bin": {
- "nanoid": "bin/nanoid.js"
+ "nanoid": "bin/nanoid.cjs"
},
"engines": {
- "node": "^18 || >=20"
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
- "node_modules/node-llama-cpp/node_modules/node-addon-api": {
- "version": "8.5.0",
- "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz",
- "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==",
+ "node_modules/napi-build-utils": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
+ "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
+ "license": "MIT"
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/negotiator": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
+ "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
"license": "MIT",
"engines": {
- "node": "^18 || ^20 || >= 21"
+ "node": ">= 0.6"
}
},
- "node_modules/node-llama-cpp/node_modules/onetime": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz",
- "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==",
- "license": "MIT",
+ "node_modules/ngraph.events": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/ngraph.events/-/ngraph.events-1.4.0.tgz",
+ "integrity": "sha512-NeDGI4DSyjBNBRtA86222JoYietsmCXbs8CEB0dZ51Xeh4lhVl1y3wpWLumczvnha8sFQIW4E0vvVWwgmX2mGw==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/ngraph.forcelayout": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/ngraph.forcelayout/-/ngraph.forcelayout-3.3.1.tgz",
+ "integrity": "sha512-MKBuEh1wujyQHFTW57y5vd/uuEOK0XfXYxm3lC7kktjJLRdt/KEKEknyOlc6tjXflqBKEuYBBcu7Ax5VY+S6aw==",
+ "license": "BSD-3-Clause",
"dependencies": {
- "mimic-function": "^5.0.0"
- },
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "ngraph.events": "^1.0.0",
+ "ngraph.merge": "^1.0.0",
+ "ngraph.random": "^1.0.0"
+ }
+ },
+ "node_modules/ngraph.graph": {
+ "version": "20.1.1",
+ "resolved": "https://registry.npmjs.org/ngraph.graph/-/ngraph.graph-20.1.1.tgz",
+ "integrity": "sha512-KNtZWYzYe7SMOuG3vvROznU+fkPmL5cGYFsWjqt+Ob1uF5xZz5EjomtsNOZEIwVuD37/zokeEqNK1ghY4/fhDg==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "ngraph.events": "^1.4.0"
}
},
- "node_modules/node-llama-cpp/node_modules/ora": {
- "version": "8.2.0",
- "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz",
- "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==",
+ "node_modules/ngraph.merge": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/ngraph.merge/-/ngraph.merge-1.0.0.tgz",
+ "integrity": "sha512-5J8YjGITUJeapsomtTALYsw7rFveYkM+lBj3QiYZ79EymQcuri65Nw3knQtFxQBU1r5iOaVRXrSwMENUPK62Vg==",
+ "license": "MIT"
+ },
+ "node_modules/ngraph.random": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/ngraph.random/-/ngraph.random-1.2.0.tgz",
+ "integrity": "sha512-4EUeAGbB2HWX9njd6bP6tciN6ByJfoaAvmVL9QTaZSeXrW46eNGA9GajiXiPBbvFqxUWFkEbyo6x5qsACUuVfA==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/node-abi": {
+ "version": "4.33.0",
+ "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-4.33.0.tgz",
+ "integrity": "sha512-vLBWCKb+7LWsX+TbfzWOkw0W81m377tyx3hOweBTjO43CXZnRGS1/JPWs20fr0PgZyDXk6ROYrylsEycK8raDA==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "chalk": "^5.3.0",
- "cli-cursor": "^5.0.0",
- "cli-spinners": "^2.9.2",
- "is-interactive": "^2.0.0",
- "is-unicode-supported": "^2.0.0",
- "log-symbols": "^6.0.0",
- "stdin-discarder": "^0.2.2",
- "string-width": "^7.2.0",
- "strip-ansi": "^7.1.0"
+ "semver": "^7.6.3"
},
"engines": {
- "node": ">=18"
+ "node": ">=22.12.0"
+ }
+ },
+ "node_modules/node-abi/node_modules/semver": {
+ "version": "7.8.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
+ "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
},
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "engines": {
+ "node": ">=10"
}
},
- "node_modules/node-llama-cpp/node_modules/ora/node_modules/log-symbols": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz",
- "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==",
+ "node_modules/node-api-version": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.2.1.tgz",
+ "integrity": "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "chalk": "^5.3.0",
- "is-unicode-supported": "^1.3.0"
+ "semver": "^7.3.5"
+ }
+ },
+ "node_modules/node-api-version/node_modules/semver": {
+ "version": "7.8.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
+ "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
},
"engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "node": ">=10"
}
},
- "node_modules/node-llama-cpp/node_modules/ora/node_modules/log-symbols/node_modules/is-unicode-supported": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz",
- "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==",
+ "node_modules/node-ensure": {
+ "version": "0.0.0",
+ "resolved": "https://registry.npmjs.org/node-ensure/-/node-ensure-0.0.0.tgz",
+ "integrity": "sha512-DRI60hzo2oKN1ma0ckc6nQWlHU69RH6xN0sjQTjMpChPfTYvKZdcQFfdYK2RWbJcKyUizSIy/l8OTGxMAM1QDw==",
+ "license": "MIT"
+ },
+ "node_modules/node-fetch": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
+ "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "whatwg-url": "^5.0.0"
+ },
"engines": {
- "node": ">=12"
+ "node": "4.x || >=6.0.0"
},
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "peerDependencies": {
+ "encoding": "^0.1.0"
+ },
+ "peerDependenciesMeta": {
+ "encoding": {
+ "optional": true
+ }
}
},
- "node_modules/node-llama-cpp/node_modules/restore-cursor": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz",
- "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==",
+ "node_modules/node-gyp": {
+ "version": "12.4.0",
+ "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.4.0.tgz",
+ "integrity": "sha512-OMcPNvqTCFUnNaBlmdgq+lfNqY7gTiSmNRDjY3uAXRyudeKZEZxu3CLtjMQrx4zZxCX2b/mpNqTtwuCJgXhHkw==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "onetime": "^7.0.0",
- "signal-exit": "^4.1.0"
+ "env-paths": "^2.2.0",
+ "exponential-backoff": "^3.1.1",
+ "graceful-fs": "^4.2.6",
+ "nopt": "^9.0.0",
+ "proc-log": "^6.0.0",
+ "semver": "^7.3.5",
+ "tar": "^7.5.4",
+ "tinyglobby": "^0.2.12",
+ "undici": "^6.25.0",
+ "which": "^6.0.0"
+ },
+ "bin": {
+ "node-gyp": "bin/node-gyp.js"
},
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/node-gyp/node_modules/chownr": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
+ "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
"engines": {
"node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/node-llama-cpp/node_modules/semver": {
- "version": "7.7.3",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
- "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
- },
+ "node_modules/node-gyp/node_modules/isexe": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz",
+ "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
"engines": {
- "node": ">=10"
+ "node": ">=20"
}
},
- "node_modules/node-llama-cpp/node_modules/signal-exit": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
- "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+ "node_modules/node-gyp/node_modules/semver": {
+ "version": "7.8.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
+ "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
+ "dev": true,
"license": "ISC",
- "engines": {
- "node": ">=14"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
}
},
- "node_modules/node-llama-cpp/node_modules/slice-ansi": {
- "version": "7.1.2",
- "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz",
- "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==",
- "license": "MIT",
+ "node_modules/node-gyp/node_modules/tar": {
+ "version": "7.5.20",
+ "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.20.tgz",
+ "integrity": "sha512-9FcyK4PA6+WbzlTM9WhQm6vB5W7cP7dUiPsv1g7YDwEQnQ1CGpK3MGlKk/ITVWMk05kHZuBhmVhiv8LZoy/PFQ==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
"dependencies": {
- "ansi-styles": "^6.2.1",
- "is-fullwidth-code-point": "^5.0.0"
+ "@isaacs/fs-minipass": "^4.0.0",
+ "chownr": "^3.0.0",
+ "minipass": "^7.1.2",
+ "minizlib": "^3.1.0",
+ "yallist": "^5.0.0"
},
"engines": {
"node": ">=18"
- },
- "funding": {
- "url": "https://github.com/chalk/slice-ansi?sponsor=1"
}
},
- "node_modules/node-llama-cpp/node_modules/string-width": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
- "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
+ "node_modules/node-gyp/node_modules/undici": {
+ "version": "6.27.0",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-6.27.0.tgz",
+ "integrity": "sha512-YmfV3YnEDzXRC5lZ2jWtWWHKGUm1zIt8AhesR1tens+HTNv+YZlN/dp6G727LOvMJ8xjP9Be7Y2Sdr96LDm+pg==",
+ "dev": true,
"license": "MIT",
- "dependencies": {
- "emoji-regex": "^10.3.0",
- "get-east-asian-width": "^1.0.0",
- "strip-ansi": "^7.1.0"
- },
"engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "node": ">=18.17"
}
},
- "node_modules/node-llama-cpp/node_modules/strip-ansi": {
- "version": "7.1.2",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
- "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
- "license": "MIT",
+ "node_modules/node-gyp/node_modules/which": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz",
+ "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==",
+ "dev": true,
+ "license": "ISC",
"dependencies": {
- "ansi-regex": "^6.0.1"
+ "isexe": "^4.0.0"
},
- "engines": {
- "node": ">=12"
+ "bin": {
+ "node-which": "bin/which.js"
},
- "funding": {
- "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
}
},
- "node_modules/node-llama-cpp/node_modules/universalify": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
- "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
- "license": "MIT",
+ "node_modules/node-gyp/node_modules/yallist": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
+ "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
"engines": {
- "node": ">= 10.0.0"
+ "node": ">=18"
}
},
+ "node_modules/node-int64": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
+ "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/node-machine-id": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/node-machine-id/-/node-machine-id-1.1.12.tgz",
@@ -17595,19 +16271,19 @@
"license": "MIT"
},
"node_modules/nopt": {
- "version": "8.1.0",
- "resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz",
- "integrity": "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==",
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz",
+ "integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==",
"dev": true,
"license": "ISC",
"dependencies": {
- "abbrev": "^3.0.0"
+ "abbrev": "^4.0.0"
},
"bin": {
"nopt": "bin/nopt.js"
},
"engines": {
- "node": "^18.17.0 || >=20.5.0"
+ "node": "^20.17.0 || >=22.9.0"
}
},
"node_modules/normalize-url": {
@@ -17622,22 +16298,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/npmlog": {
- "version": "6.0.2",
- "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz",
- "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==",
- "deprecated": "This package is no longer supported.",
- "license": "ISC",
- "dependencies": {
- "are-we-there-yet": "^3.0.0",
- "console-control-strings": "^1.1.0",
- "gauge": "^4.0.3",
- "set-blocking": "^2.0.0"
- },
- "engines": {
- "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
- }
- },
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -17754,28 +16414,6 @@
],
"license": "MIT"
},
- "node_modules/octokit": {
- "version": "5.0.5",
- "resolved": "https://registry.npmjs.org/octokit/-/octokit-5.0.5.tgz",
- "integrity": "sha512-4+/OFSqOjoyULo7eN7EA97DE0Xydj/PW5aIckxqQIoFjFwqXKuFCvXUJObyJfBF9Khu4RL/jlDRI9FPaMGfPnw==",
- "license": "MIT",
- "dependencies": {
- "@octokit/app": "^16.1.2",
- "@octokit/core": "^7.0.6",
- "@octokit/oauth-app": "^8.0.3",
- "@octokit/plugin-paginate-graphql": "^6.0.0",
- "@octokit/plugin-paginate-rest": "^14.0.0",
- "@octokit/plugin-rest-endpoint-methods": "^17.0.0",
- "@octokit/plugin-retry": "^8.0.3",
- "@octokit/plugin-throttling": "^11.0.3",
- "@octokit/request-error": "^7.0.2",
- "@octokit/types": "^16.0.0",
- "@octokit/webhooks": "^14.0.0"
- },
- "engines": {
- "node": ">= 20"
- }
- },
"node_modules/ollama": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/ollama/-/ollama-0.6.3.tgz",
@@ -17806,22 +16444,6 @@
"wrappy": "1"
}
},
- "node_modules/onetime": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
- "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "mimic-fn": "^2.1.0"
- },
- "engines": {
- "node": ">=6"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/onnx-proto": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/onnx-proto/-/onnx-proto-4.0.4.tgz",
@@ -17866,24 +16488,6 @@
"platform": "^1.3.6"
}
},
- "node_modules/openai": {
- "version": "6.44.0",
- "resolved": "https://registry.npmjs.org/openai/-/openai-6.44.0.tgz",
- "integrity": "sha512-09/gH+8jH0RgUwsgWHAaxsKGRT5zVZ95IaJUnqAWj6XejIBmnFRwq2WUIF37VtDEsmGrtPmvCs5+yBSeZGWvkA==",
- "license": "Apache-2.0",
- "peerDependencies": {
- "ws": "^8.18.0",
- "zod": "^3.25 || ^4.0"
- },
- "peerDependenciesMeta": {
- "ws": {
- "optional": true
- },
- "zod": {
- "optional": true
- }
- }
- },
"node_modules/option": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/option/-/option-0.2.4.tgz",
@@ -17908,30 +16512,6 @@
"node": ">= 0.8.0"
}
},
- "node_modules/ora": {
- "version": "5.4.1",
- "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz",
- "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "bl": "^4.1.0",
- "chalk": "^4.1.0",
- "cli-cursor": "^3.1.0",
- "cli-spinners": "^2.5.0",
- "is-interactive": "^1.0.0",
- "is-unicode-supported": "^0.1.0",
- "log-symbols": "^4.1.0",
- "strip-ansi": "^6.0.0",
- "wcwidth": "^1.0.1"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/own-keys": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz",
@@ -17950,6 +16530,75 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/oxc-parser": {
+ "version": "0.137.0",
+ "resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.137.0.tgz",
+ "integrity": "sha512-yFImD+WLElJpLKy8llG1qe4DCmMsL18peRp8XP1JKfig/gISbJkglnpDtX2aTmAn10kZF7164HbN2H8QPsXxGg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@oxc-project/types": "^0.137.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/Boshen"
+ },
+ "optionalDependencies": {
+ "@oxc-parser/binding-android-arm-eabi": "0.137.0",
+ "@oxc-parser/binding-android-arm64": "0.137.0",
+ "@oxc-parser/binding-darwin-arm64": "0.137.0",
+ "@oxc-parser/binding-darwin-x64": "0.137.0",
+ "@oxc-parser/binding-freebsd-x64": "0.137.0",
+ "@oxc-parser/binding-linux-arm-gnueabihf": "0.137.0",
+ "@oxc-parser/binding-linux-arm-musleabihf": "0.137.0",
+ "@oxc-parser/binding-linux-arm64-gnu": "0.137.0",
+ "@oxc-parser/binding-linux-arm64-musl": "0.137.0",
+ "@oxc-parser/binding-linux-ppc64-gnu": "0.137.0",
+ "@oxc-parser/binding-linux-riscv64-gnu": "0.137.0",
+ "@oxc-parser/binding-linux-riscv64-musl": "0.137.0",
+ "@oxc-parser/binding-linux-s390x-gnu": "0.137.0",
+ "@oxc-parser/binding-linux-x64-gnu": "0.137.0",
+ "@oxc-parser/binding-linux-x64-musl": "0.137.0",
+ "@oxc-parser/binding-openharmony-arm64": "0.137.0",
+ "@oxc-parser/binding-wasm32-wasi": "0.137.0",
+ "@oxc-parser/binding-win32-arm64-msvc": "0.137.0",
+ "@oxc-parser/binding-win32-ia32-msvc": "0.137.0",
+ "@oxc-parser/binding-win32-x64-msvc": "0.137.0"
+ }
+ },
+ "node_modules/oxc-resolver": {
+ "version": "11.21.3",
+ "resolved": "https://registry.npmjs.org/oxc-resolver/-/oxc-resolver-11.21.3.tgz",
+ "integrity": "sha512-2Mx3fKQz7+xgrBONjsxOgCGtMHOn38/HxMzW1I5efwXB5a4lRN0Vp40gYUJFBWJslcrvwoofTrqoTnLbwTd3pA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/Boshen"
+ },
+ "optionalDependencies": {
+ "@oxc-resolver/binding-android-arm-eabi": "11.21.3",
+ "@oxc-resolver/binding-android-arm64": "11.21.3",
+ "@oxc-resolver/binding-darwin-arm64": "11.21.3",
+ "@oxc-resolver/binding-darwin-x64": "11.21.3",
+ "@oxc-resolver/binding-freebsd-x64": "11.21.3",
+ "@oxc-resolver/binding-linux-arm-gnueabihf": "11.21.3",
+ "@oxc-resolver/binding-linux-arm-musleabihf": "11.21.3",
+ "@oxc-resolver/binding-linux-arm64-gnu": "11.21.3",
+ "@oxc-resolver/binding-linux-arm64-musl": "11.21.3",
+ "@oxc-resolver/binding-linux-ppc64-gnu": "11.21.3",
+ "@oxc-resolver/binding-linux-riscv64-gnu": "11.21.3",
+ "@oxc-resolver/binding-linux-riscv64-musl": "11.21.3",
+ "@oxc-resolver/binding-linux-s390x-gnu": "11.21.3",
+ "@oxc-resolver/binding-linux-x64-gnu": "11.21.3",
+ "@oxc-resolver/binding-linux-x64-musl": "11.21.3",
+ "@oxc-resolver/binding-openharmony-arm64": "11.21.3",
+ "@oxc-resolver/binding-wasm32-wasi": "11.21.3",
+ "@oxc-resolver/binding-win32-arm64-msvc": "11.21.3",
+ "@oxc-resolver/binding-win32-x64-msvc": "11.21.3"
+ }
+ },
"node_modules/p-cancelable": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz",
@@ -17959,15 +16608,6 @@
"node": ">=8"
}
},
- "node_modules/p-finally": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
- "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==",
- "license": "MIT",
- "engines": {
- "node": ">=4"
- }
- },
"node_modules/p-limit": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
@@ -18000,74 +16640,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/p-map": {
- "version": "7.0.4",
- "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz",
- "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/p-queue": {
- "version": "6.6.2",
- "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz",
- "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==",
- "license": "MIT",
- "dependencies": {
- "eventemitter3": "^4.0.4",
- "p-timeout": "^3.2.0"
- },
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/p-queue/node_modules/eventemitter3": {
- "version": "4.0.7",
- "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
- "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
- "license": "MIT"
- },
- "node_modules/p-retry": {
- "version": "7.1.1",
- "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-7.1.1.tgz",
- "integrity": "sha512-J5ApzjyRkkf601HpEeykoiCvzHQjWxPAHhyjFcEUP2SWq0+35NKh8TLhpLw+Dkq5TZBFvUM6UigdE9hIVYTl5w==",
- "license": "MIT",
- "dependencies": {
- "is-network-error": "^1.1.0"
- },
- "engines": {
- "node": ">=20"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/p-timeout": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz",
- "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==",
- "license": "MIT",
- "dependencies": {
- "p-finally": "^1.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/package-json-from-dist": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
- "devOptional": true,
- "license": "BlueOak-1.0.0"
+ "license": "BlueOak-1.0.0",
+ "optional": true
},
"node_modules/pako": {
"version": "1.0.11",
@@ -18113,16 +16691,17 @@
"integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
"license": "MIT"
},
- "node_modules/parse-ms": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz",
- "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==",
+ "node_modules/parse5": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz",
+ "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==",
+ "dev": true,
"license": "MIT",
- "engines": {
- "node": ">=18"
+ "dependencies": {
+ "entities": "^8.0.0"
},
"funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
"node_modules/parseurl": {
@@ -18173,8 +16752,8 @@
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
"integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
- "devOptional": true,
"license": "BlueOak-1.0.0",
+ "optional": true,
"dependencies": {
"lru-cache": "^10.2.0",
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
@@ -18190,8 +16769,8 @@
"version": "10.4.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
- "devOptional": true,
- "license": "ISC"
+ "license": "ISC",
+ "optional": true
},
"node_modules/path-to-regexp": {
"version": "8.4.2",
@@ -18260,9 +16839,9 @@
"license": "ISC"
},
"node_modules/picomatch": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
- "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"license": "MIT",
"engines": {
"node": ">=12"
@@ -18280,6 +16859,37 @@
"node": ">=16.20.0"
}
},
+ "node_modules/pkijs": {
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/pkijs/-/pkijs-3.4.0.tgz",
+ "integrity": "sha512-emEcLuomt2j03vxD54giVB4SxTjnsqkU692xZOZXHDVoYyypEm+b3jpiTcc+Cf+myooc+/Ly0z01jqeNHVgJGw==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@noble/hashes": "1.4.0",
+ "asn1js": "^3.0.6",
+ "bytestreamjs": "^2.0.1",
+ "pvtsutils": "^1.3.6",
+ "pvutils": "^1.1.3",
+ "tslib": "^2.8.1"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/pkijs/node_modules/@noble/hashes": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz",
+ "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 16"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
"node_modules/platform": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz",
@@ -18405,33 +17015,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/posthog-js": {
- "version": "1.331.0",
- "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.331.0.tgz",
- "integrity": "sha512-9bUWkQXP95OhDxcjctKma8LSxpIxU+i7Xq+i/M4wjGjEvN7xt1ztNO4XKJYmEVT1Su3ZOLudidyWrSGL568F4A==",
- "license": "SEE LICENSE IN LICENSE",
- "dependencies": {
- "@opentelemetry/api": "^1.9.0",
- "@opentelemetry/api-logs": "^0.208.0",
- "@opentelemetry/exporter-logs-otlp-http": "^0.208.0",
- "@opentelemetry/resources": "^2.2.0",
- "@opentelemetry/sdk-logs": "^0.208.0",
- "@posthog/core": "1.11.0",
- "@posthog/types": "1.331.0",
- "core-js": "^3.38.1",
- "dompurify": "^3.3.1",
- "fflate": "^0.4.8",
- "preact": "^10.28.0",
- "query-selector-shadow-dom": "^1.0.1",
- "web-vitals": "^4.2.4"
- }
- },
- "node_modules/posthog-js/node_modules/fflate": {
- "version": "0.4.8",
- "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.4.8.tgz",
- "integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==",
- "license": "MIT"
- },
"node_modules/postject": {
"version": "1.0.0-alpha.6",
"resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz",
@@ -18561,41 +17144,52 @@
"node": ">=6.0.0"
}
},
- "node_modules/pretty-bytes": {
- "version": "6.1.1",
- "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz",
- "integrity": "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==",
+ "node_modules/pretty-format": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
+ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
+ "dev": true,
"license": "MIT",
- "engines": {
- "node": "^14.13.1 || >=16.0.0"
+ "peer": true,
+ "dependencies": {
+ "ansi-regex": "^5.0.1",
+ "ansi-styles": "^5.0.0",
+ "react-is": "^17.0.1"
},
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "engines": {
+ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
}
},
- "node_modules/pretty-ms": {
- "version": "9.3.0",
- "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz",
- "integrity": "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==",
+ "node_modules/pretty-format/node_modules/ansi-styles": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
+ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
+ "dev": true,
"license": "MIT",
- "dependencies": {
- "parse-ms": "^4.0.0"
- },
+ "peer": true,
"engines": {
- "node": ">=18"
+ "node": ">=10"
},
"funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
+ "node_modules/pretty-format/node_modules/react-is": {
+ "version": "17.0.2",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
+ "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/proc-log": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz",
- "integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==",
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz",
+ "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==",
"dev": true,
"license": "ISC",
"engines": {
- "node": "^18.17.0 || >=20.5.0"
+ "node": "^20.17.0 || >=22.9.0"
}
},
"node_modules/process-nextick-args": {
@@ -18627,6 +17221,20 @@
"node": ">=10"
}
},
+ "node_modules/prompts": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
+ "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "kleur": "^3.0.3",
+ "sisteransi": "^1.0.5"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
@@ -18642,6 +17250,7 @@
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz",
"integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.4",
@@ -18698,12 +17307,6 @@
"node": ">= 0.10"
}
},
- "node_modules/proxy-from-env": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
- "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
- "license": "MIT"
- },
"node_modules/pump": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
@@ -18721,7 +17324,27 @@
"dev": true,
"license": "MIT",
"engines": {
- "node": ">=6"
+ "node": ">=6"
+ }
+ },
+ "node_modules/pvtsutils": {
+ "version": "1.3.6",
+ "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz",
+ "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.8.1"
+ }
+ },
+ "node_modules/pvutils": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.5.tgz",
+ "integrity": "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=16.0.0"
}
},
"node_modules/qs": {
@@ -18739,12 +17362,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/query-selector-shadow-dom": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/query-selector-shadow-dom/-/query-selector-shadow-dom-1.0.1.tgz",
- "integrity": "sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw==",
- "license": "MIT"
- },
"node_modules/quick-lru": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
@@ -19073,21 +17690,6 @@
}
}
},
- "node_modules/react-use-measure": {
- "version": "2.1.7",
- "resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz",
- "integrity": "sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==",
- "license": "MIT",
- "peerDependencies": {
- "react": ">=16.13",
- "react-dom": ">=16.13"
- },
- "peerDependenciesMeta": {
- "react-dom": {
- "optional": true
- }
- }
- },
"node_modules/read-binary-file-arch": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz",
@@ -19115,6 +17717,54 @@
"node": ">= 6"
}
},
+ "node_modules/rechoir": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz",
+ "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "resolve": "^1.20.0"
+ },
+ "engines": {
+ "node": ">= 10.13.0"
+ }
+ },
+ "node_modules/rechoir/node_modules/resolve": {
+ "version": "1.22.12",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz",
+ "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "is-core-module": "^2.16.1",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/refa": {
+ "version": "0.12.1",
+ "resolved": "https://registry.npmjs.org/refa/-/refa-0.12.1.tgz",
+ "integrity": "sha512-J8rn6v4DBb2nnFqkqwy6/NnTYMcgLA+sLr0iIO41qpv0n+ngb7ksag2tMRl0inb1bbO/esUwzW1vbJi7K0sI0g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.8.0"
+ },
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
"node_modules/reflect-metadata": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz",
@@ -19144,6 +17794,30 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/regexp-ast-analysis": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/regexp-ast-analysis/-/regexp-ast-analysis-0.7.1.tgz",
+ "integrity": "sha512-sZuz1dYW/ZsfG17WSAG7eS85r5a0dDsvg+7BiiYR5o6lKCAtUrEwdmRmaGF6rwVj3LcmAeYkOWKEPlbPzN3Y3A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.8.0",
+ "refa": "^0.12.1"
+ },
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/regexp-tree": {
+ "version": "0.1.27",
+ "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz",
+ "integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "regexp-tree": "bin/regexp-tree"
+ }
+ },
"node_modules/regexp.prototype.flags": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
@@ -19250,6 +17924,7 @@
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -19316,6 +17991,16 @@
"node": ">=4"
}
},
+ "node_modules/resolve-pkg-maps": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
+ "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
+ }
+ },
"node_modules/responselike": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz",
@@ -19328,24 +18013,11 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/restore-cursor": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
- "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "onetime": "^5.1.0",
- "signal-exit": "^3.0.2"
- },
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/retry": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
"integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==",
+ "devOptional": true,
"license": "MIT",
"engines": {
"node": ">= 4"
@@ -19500,6 +18172,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/safe-regex": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-2.1.1.tgz",
+ "integrity": "sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "regexp-tree": "~0.1.1"
+ }
+ },
"node_modules/safe-regex-test": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
@@ -19525,9 +18207,9 @@
"license": "MIT"
},
"node_modules/sanitize-filename": {
- "version": "1.6.3",
- "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz",
- "integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==",
+ "version": "1.6.4",
+ "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.4.tgz",
+ "integrity": "sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg==",
"dev": true,
"license": "WTFPL OR ISC",
"dependencies": {
@@ -19543,12 +18225,40 @@
"node": ">=11.0.0"
}
},
+ "node_modules/saxes": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
+ "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "xmlchars": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=v12.22.7"
+ }
+ },
"node_modules/scheduler": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
"license": "MIT"
},
+ "node_modules/scslre": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/scslre/-/scslre-0.3.0.tgz",
+ "integrity": "sha512-3A6sD0WYP7+QrjbfNA2FN3FsOaGGFoekCVgTyypy53gPxhbkCIjtO6YWgdrfM+n/8sI8JeXZOIxsHjMTNxQ4nQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.8.0",
+ "refa": "^0.12.0",
+ "regexp-ast-analysis": "^0.7.0"
+ },
+ "engines": {
+ "node": "^14.0.0 || >=16.0.0"
+ }
+ },
"node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
@@ -19653,7 +18363,8 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
- "license": "ISC"
+ "license": "ISC",
+ "optional": true
},
"node_modules/set-function-length": {
"version": "1.2.2",
@@ -19876,6 +18587,7 @@
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+ "devOptional": true,
"license": "ISC"
},
"node_modules/simple-concat": {
@@ -19923,41 +18635,6 @@
"simple-concat": "^1.0.0"
}
},
- "node_modules/simple-git": {
- "version": "3.30.0",
- "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.30.0.tgz",
- "integrity": "sha512-q6lxyDsCmEal/MEGhP1aVyQ3oxnagGlBDOVSIB4XUVLl1iZh0Pah6ebC9V4xBap/RfgP2WlI8EKs0WS0rMEJHg==",
- "license": "MIT",
- "dependencies": {
- "@kwsites/file-exists": "^1.1.1",
- "@kwsites/promise-deferred": "^1.1.1",
- "debug": "^4.4.0"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/steveukx/git-js?sponsor=1"
- }
- },
- "node_modules/simple-icons": {
- "version": "16.6.0",
- "resolved": "https://registry.npmjs.org/simple-icons/-/simple-icons-16.6.0.tgz",
- "integrity": "sha512-lzSVlAhflhwud7EprwSalbCpHKpculOfaAk1P+S3QajO1bHG5nqwI1VeGnn4rwaE4xSSSKDsOFFL0XfIDv5iIQ==",
- "dev": true,
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/simple-icons"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/simple-icons"
- }
- ],
- "license": "CC0-1.0",
- "engines": {
- "node": ">=0.12.18"
- }
- },
"node_modules/simple-swizzle": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz",
@@ -19993,45 +18670,43 @@
"node": ">=10"
}
},
- "node_modules/sleep-promise": {
- "version": "9.1.0",
- "resolved": "https://registry.npmjs.org/sleep-promise/-/sleep-promise-9.1.0.tgz",
- "integrity": "sha512-UHYzVpz9Xn8b+jikYSD6bqvf754xL2uBUzDFwiU6NcdZeifPr6UfgU43xpkPu67VMS88+TI2PSI7Eohgqf2fKA==",
- "license": "MIT"
- },
- "node_modules/slice-ansi": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz",
- "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==",
+ "node_modules/sisteransi": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
+ "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==",
"dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "ansi-styles": "^4.0.0",
- "astral-regex": "^2.0.0",
- "is-fullwidth-code-point": "^3.0.0"
- },
- "engines": {
- "node": ">=8"
- }
+ "license": "MIT"
},
"node_modules/smart-buffer": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
"integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
- "devOptional": true,
"license": "MIT",
+ "optional": true,
"engines": {
"node": ">= 6.0.0",
"npm": ">= 3.0.0"
}
},
+ "node_modules/smol-toml": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.7.0.tgz",
+ "integrity": "sha512-aqVvWoyO21L23mb+drl4RmMXbf6N7FdHjAhTRA9ZBL7apWBgfWC16KjrASI+1p9GAroljyMHj6fK67i0UiTNvQ==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/cyyynthia"
+ }
+ },
"node_modules/socks": {
"version": "2.8.7",
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz",
"integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==",
- "devOptional": true,
"license": "MIT",
+ "optional": true,
"dependencies": {
"ip-address": "^10.0.1",
"smart-buffer": "^4.2.0"
@@ -20045,8 +18720,8 @@
"version": "8.0.5",
"resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz",
"integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==",
- "devOptional": true,
"license": "MIT",
+ "optional": true,
"dependencies": {
"agent-base": "^7.1.2",
"debug": "^4.3.4",
@@ -20102,19 +18777,6 @@
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
"license": "BSD-3-Clause"
},
- "node_modules/ssri": {
- "version": "12.0.0",
- "resolved": "https://registry.npmjs.org/ssri/-/ssri-12.0.0.tgz",
- "integrity": "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "minipass": "^7.0.3"
- },
- "engines": {
- "node": "^18.17.0 || >=20.5.0"
- }
- },
"node_modules/stackback": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
@@ -20148,107 +18810,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/stdin-discarder": {
- "version": "0.2.2",
- "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz",
- "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==",
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/stdout-update": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/stdout-update/-/stdout-update-4.0.1.tgz",
- "integrity": "sha512-wiS21Jthlvl1to+oorePvcyrIkiG/6M3D3VTmDUlJm7Cy6SbFhKkAvX+YBuHLxck/tO3mrdpC/cNesigQc3+UQ==",
- "license": "MIT",
- "dependencies": {
- "ansi-escapes": "^6.2.0",
- "ansi-styles": "^6.2.1",
- "string-width": "^7.1.0",
- "strip-ansi": "^7.1.0"
- },
- "engines": {
- "node": ">=16.0.0"
- }
- },
- "node_modules/stdout-update/node_modules/ansi-regex": {
- "version": "6.2.2",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
- "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-regex?sponsor=1"
- }
- },
- "node_modules/stdout-update/node_modules/ansi-styles": {
- "version": "6.2.3",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
- "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
- }
- },
- "node_modules/stdout-update/node_modules/emoji-regex": {
- "version": "10.6.0",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
- "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
- "license": "MIT"
- },
- "node_modules/stdout-update/node_modules/string-width": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
- "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
- "license": "MIT",
- "dependencies": {
- "emoji-regex": "^10.3.0",
- "get-east-asian-width": "^1.0.0",
- "strip-ansi": "^7.1.0"
- },
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/stdout-update/node_modules/strip-ansi": {
- "version": "7.1.2",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
- "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
- "license": "MIT",
- "dependencies": {
- "ansi-regex": "^6.0.1"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/strip-ansi?sponsor=1"
- }
- },
- "node_modules/steno": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/steno/-/steno-4.0.2.tgz",
- "integrity": "sha512-yhPIQXjrlt1xv7dyPQg2P17URmXbuM5pdGkpiMB3RenprfiBlvK415Lctfe0eshk90oA7/tNq7WEiMK8RSP39A==",
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/typicode"
- }
- },
"node_modules/stop-iteration-iterator": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
@@ -20287,6 +18848,7 @@
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
@@ -20302,8 +18864,8 @@
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
- "devOptional": true,
"license": "MIT",
+ "optional": true,
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
@@ -20429,6 +18991,7 @@
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
@@ -20442,8 +19005,8 @@
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
- "devOptional": true,
"license": "MIT",
+ "optional": true,
"dependencies": {
"ansi-regex": "^5.0.1"
},
@@ -20451,6 +19014,16 @@
"node": ">=8"
}
},
+ "node_modules/strip-bom": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
+ "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/strip-json-comments": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
@@ -20519,14 +19092,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/suspend-react": {
- "version": "0.1.3",
- "resolved": "https://registry.npmjs.org/suspend-react/-/suspend-react-0.1.3.tgz",
- "integrity": "sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==",
- "license": "MIT",
- "peerDependencies": {
- "react": ">=17.0"
- }
+ "node_modules/symbol-tree": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
+ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
+ "dev": true,
+ "license": "MIT"
},
"node_modules/synckit": {
"version": "0.11.12",
@@ -20583,9 +19154,9 @@
"license": "MIT"
},
"node_modules/tapable": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz",
- "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==",
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz",
+ "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==",
"license": "MIT",
"engines": {
"node": ">=6"
@@ -20600,6 +19171,7 @@
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
"integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
"license": "ISC",
+ "optional": true,
"dependencies": {
"chownr": "^2.0.0",
"fs-minipass": "^2.0.0",
@@ -20651,6 +19223,7 @@
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
"integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
"license": "ISC",
+ "optional": true,
"dependencies": {
"minipass": "^3.0.0"
},
@@ -20663,6 +19236,7 @@
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
"license": "ISC",
+ "optional": true,
"dependencies": {
"yallist": "^4.0.0"
},
@@ -20675,6 +19249,7 @@
"resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
"integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
"license": "ISC",
+ "optional": true,
"engines": {
"node": ">=8"
}
@@ -20684,6 +19259,7 @@
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
"integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
"license": "MIT",
+ "optional": true,
"dependencies": {
"minipass": "^3.0.0",
"yallist": "^4.0.0"
@@ -20697,6 +19273,7 @@
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
"license": "ISC",
+ "optional": true,
"dependencies": {
"yallist": "^4.0.0"
},
@@ -20708,7 +19285,8 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
- "license": "ISC"
+ "license": "ISC",
+ "optional": true
},
"node_modules/teex": {
"version": "1.0.1",
@@ -20761,9 +19339,9 @@
}
},
"node_modules/temp-file/node_modules/jsonfile": {
- "version": "6.2.0",
- "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
- "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz",
+ "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -20905,13 +19483,13 @@
}
},
"node_modules/tinyglobby": {
- "version": "0.2.15",
- "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
- "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+ "version": "0.2.17",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
+ "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
"license": "MIT",
"dependencies": {
"fdir": "^6.5.0",
- "picomatch": "^4.0.3"
+ "picomatch": "^4.0.4"
},
"engines": {
"node": ">=12.0.0"
@@ -20930,10 +19508,30 @@
"node": ">=14.0.0"
}
},
+ "node_modules/tldts": {
+ "version": "7.4.7",
+ "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.7.tgz",
+ "integrity": "sha512-56L0/9HELHSsG1bFCzay8UoLxzRL7kpFf7Wl5q/kSYwiSJGACvro61xnKzPNM+SadxllzdtXsKDSXE7HPeqIAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tldts-core": "^7.4.7"
+ },
+ "bin": {
+ "tldts": "bin/cli.js"
+ }
+ },
+ "node_modules/tldts-core": {
+ "version": "7.4.7",
+ "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.7.tgz",
+ "integrity": "sha512-rNlAI8fKn/JckBMUSbNL/ES2kmDiurWaE49l+ikwEc9A6lFR7gMx9AhgQMQKBK4H5w4pKLH64JzZfB99uRsGNQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/tmp": {
- "version": "0.2.5",
- "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz",
- "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==",
+ "version": "0.2.7",
+ "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz",
+ "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -20950,15 +19548,6 @@
"tmp": "^0.2.0"
}
},
- "node_modules/toad-cache": {
- "version": "3.7.0",
- "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz",
- "integrity": "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==",
- "license": "MIT",
- "engines": {
- "node": ">=12"
- }
- },
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
@@ -20968,6 +19557,19 @@
"node": ">=0.6"
}
},
+ "node_modules/tough-cookie": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz",
+ "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "tldts": "^7.0.5"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
@@ -21006,9 +19608,9 @@
}
},
"node_modules/ts-api-utils": {
- "version": "2.4.0",
- "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz",
- "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==",
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
+ "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -21018,6 +19620,37 @@
"typescript": ">=4.8.4"
}
},
+ "node_modules/tsconfig-paths": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz",
+ "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json5": "^2.2.2",
+ "minimist": "^1.2.6",
+ "strip-bom": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/tsconfig-paths-webpack-plugin": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.2.0.tgz",
+ "integrity": "sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^4.1.0",
+ "enhanced-resolve": "^5.7.0",
+ "tapable": "^2.2.1",
+ "tsconfig-paths": "^4.1.2"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
@@ -21199,7 +19832,7 @@
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
- "devOptional": true,
+ "dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
@@ -21210,16 +19843,16 @@
}
},
"node_modules/typescript-eslint": {
- "version": "8.53.0",
- "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.53.0.tgz",
- "integrity": "sha512-xHURCQNxZ1dsWn0sdOaOfCSQG0HKeqSj9OexIxrz6ypU6wHYOdX2I3D2b8s8wFSsSOYJb+6q283cLiLlkEsBYw==",
+ "version": "8.63.0",
+ "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.63.0.tgz",
+ "integrity": "sha512-xgwXyzG4sK9ALkBxbyGkTMMOS+imnW65iPhxCQMK83KhxyoDNW7l+IDqEf9vMdoUidHpOoS967RCq4eMiTexwQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/eslint-plugin": "8.53.0",
- "@typescript-eslint/parser": "8.53.0",
- "@typescript-eslint/typescript-estree": "8.53.0",
- "@typescript-eslint/utils": "8.53.0"
+ "@typescript-eslint/eslint-plugin": "8.63.0",
+ "@typescript-eslint/parser": "8.63.0",
+ "@typescript-eslint/typescript-estree": "8.63.0",
+ "@typescript-eslint/utils": "8.63.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -21229,8 +19862,8 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "eslint": "^8.57.0 || ^9.0.0",
- "typescript": ">=4.8.4 <6.0.0"
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/typical": {
@@ -21242,6 +19875,16 @@
"node": ">=8"
}
},
+ "node_modules/unbash": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/unbash/-/unbash-4.0.2.tgz",
+ "integrity": "sha512-8gwNZ29+0/3zmXw7ToIHZtg6wK37xnniRUdBt7B27xZxaxfgR5tGMaGHT0t0dLtBV9fXE7zurh0s6Z1DHVjfWg==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=14"
+ }
+ },
"node_modules/unbox-primitive": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
@@ -21267,6 +19910,16 @@
"integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==",
"license": "MIT"
},
+ "node_modules/undici": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz",
+ "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.18.1"
+ }
+ },
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
@@ -21292,32 +19945,6 @@
"url": "https://opencollective.com/unified"
}
},
- "node_modules/unique-filename": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-4.0.0.tgz",
- "integrity": "sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "unique-slug": "^5.0.0"
- },
- "engines": {
- "node": "^18.17.0 || >=20.5.0"
- }
- },
- "node_modules/unique-slug": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-5.0.0.tgz",
- "integrity": "sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "imurmurhash": "^0.1.4"
- },
- "engines": {
- "node": "^18.17.0 || >=20.5.0"
- }
- },
"node_modules/unist-util-is": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz",
@@ -21386,18 +20013,6 @@
"url": "https://opencollective.com/unified"
}
},
- "node_modules/universal-github-app-jwt": {
- "version": "2.2.2",
- "resolved": "https://registry.npmjs.org/universal-github-app-jwt/-/universal-github-app-jwt-2.2.2.tgz",
- "integrity": "sha512-dcmbeSrOdTnsjGjUfAlqNDJrhxXizjAz94ija9Qw8YkZ1uu0d+GoZzyH+Jb9tIIqvGsadUfwg+22k5aDqqwzbw==",
- "license": "MIT"
- },
- "node_modules/universal-user-agent": {
- "version": "7.0.3",
- "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz",
- "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==",
- "license": "ISC"
- },
"node_modules/universalify": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
@@ -21416,6 +20031,65 @@
"node": ">= 0.8"
}
},
+ "node_modules/unzipper": {
+ "version": "0.12.5",
+ "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.12.5.tgz",
+ "integrity": "sha512-tXYOi9R57Uj/2Z25SOs5RRSzq886MBQj2gY8dPL+xl/kv6s6SvByoKfAtvfVeEuhntWDgjd2o9p2lb4TVPAz0A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bluebird": "~3.7.2",
+ "duplexer2": "~0.1.4",
+ "fs-extra": "11.3.1",
+ "graceful-fs": "^4.2.2",
+ "node-int64": "^0.4.0"
+ }
+ },
+ "node_modules/unzipper/node_modules/bluebird": {
+ "version": "3.7.2",
+ "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
+ "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/unzipper/node_modules/fs-extra": {
+ "version": "11.3.1",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.1.tgz",
+ "integrity": "sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=14.14"
+ }
+ },
+ "node_modules/unzipper/node_modules/jsonfile": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz",
+ "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/unzipper/node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
"node_modules/update-browserslist-db": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
@@ -21457,12 +20131,6 @@
"punycode": "^2.1.0"
}
},
- "node_modules/url-join": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz",
- "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==",
- "license": "MIT"
- },
"node_modules/use-callback-ref": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz",
@@ -21506,15 +20174,6 @@
}
}
},
- "node_modules/use-sync-external-store": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
- "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
- "license": "MIT",
- "peerDependencies": {
- "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
- }
- },
"node_modules/utf8-byte-length": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz",
@@ -21528,15 +20187,6 @@
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
- "node_modules/validate-npm-package-name": {
- "version": "6.0.2",
- "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-6.0.2.tgz",
- "integrity": "sha512-IUoow1YUtvoBBC06dXs8bR8B9vuA3aJfmQNKMoaPG/OFsPmoQvw8xh+6Ye25Gx9DQhoEom3Pcu9MKHerm/NpUQ==",
- "license": "ISC",
- "engines": {
- "node": "^18.17.0 || >=20.5.0"
- }
- },
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
@@ -21546,22 +20196,6 @@
"node": ">= 0.8"
}
},
- "node_modules/verror": {
- "version": "1.10.1",
- "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz",
- "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "assert-plus": "^1.0.0",
- "core-util-is": "1.0.2",
- "extsprintf": "^1.2.0"
- },
- "engines": {
- "node": ">=0.6.0"
- }
- },
"node_modules/vfile": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
@@ -22211,21 +20845,55 @@
}
}
},
- "node_modules/wcwidth": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
- "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==",
+ "node_modules/w3c-xmlserializer": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
+ "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "defaults": "^1.0.3"
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
}
},
- "node_modules/web-vitals": {
- "version": "4.2.4",
- "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz",
- "integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==",
- "license": "Apache-2.0"
+ "node_modules/walk-up-path": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/walk-up-path/-/walk-up-path-4.0.0.tgz",
+ "integrity": "sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "node_modules/watskeburt": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/watskeburt/-/watskeburt-6.0.0.tgz",
+ "integrity": "sha512-jfiuDABaxSkC71T6oZ3vCS99roYkSHm/+As+G0Dz8taAHQb+SJBvLEm5RlsgG71XdfAj3rv7eudUBTgwcQUPlQ==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "watskeburt": "dist/run-cli.js"
+ },
+ "engines": {
+ "node": "^22.13||^24||>=26"
+ }
+ },
+ "node_modules/webcrypto-core": {
+ "version": "1.9.2",
+ "resolved": "https://registry.npmjs.org/webcrypto-core/-/webcrypto-core-1.9.2.tgz",
+ "integrity": "sha512-gsXecm82UQNlTBURJGuqOWy1Ww08S3kZUcr3aOJS02Pk0xLtkfeUAVC0u0xhgdonFme80edSJUIJyuvL/7250Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@peculiar/asn1-schema": "^2.7.0",
+ "@peculiar/json-schema": "^1.1.12",
+ "@peculiar/utils": "^2.0.2",
+ "asn1js": "^3.0.10",
+ "tslib": "^2.8.1"
+ }
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
@@ -22240,6 +20908,16 @@
"integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==",
"license": "MIT"
},
+ "node_modules/whatwg-mimetype": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz",
+ "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ }
+ },
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
@@ -22255,6 +20933,7 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz",
"integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==",
+ "dev": true,
"license": "ISC",
"dependencies": {
"isexe": "^3.1.1"
@@ -22377,6 +21056,7 @@
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz",
"integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==",
"license": "ISC",
+ "optional": true,
"dependencies": {
"string-width": "^1.0.2 || 2 || 3 || 4"
}
@@ -22404,6 +21084,7 @@
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
@@ -22422,8 +21103,8 @@
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
- "devOptional": true,
"license": "MIT",
+ "optional": true,
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
@@ -22442,6 +21123,16 @@
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC"
},
+ "node_modules/xml-name-validator": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
+ "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/xmlbuilder": {
"version": "15.1.1",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz",
@@ -22452,10 +21143,18 @@
"node": ">=8.0"
}
},
+ "node_modules/xmlchars": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
+ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+ "dev": true,
"license": "ISC",
"engines": {
"node": ">=10"
@@ -22468,10 +21167,27 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/yaml": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz",
+ "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==",
+ "devOptional": true,
+ "license": "ISC",
+ "bin": {
+ "yaml": "bin.mjs"
+ },
+ "engines": {
+ "node": ">= 14.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/eemeli"
+ }
+ },
"node_modules/yargs": {
- "version": "17.7.2",
- "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
- "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
+ "version": "17.7.3",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz",
+ "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"cliui": "^8.0.1",
@@ -22490,6 +21206,7 @@
"version": "21.1.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+ "dev": true,
"license": "ISC",
"engines": {
"node": ">=12"
@@ -22518,18 +21235,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/yoctocolors": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz",
- "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==",
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/zod": {
"version": "4.3.5",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz",
@@ -22561,35 +21266,6 @@
"zod": "^3.25.0 || ^4.0.0"
}
},
- "node_modules/zustand": {
- "version": "5.0.10",
- "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.10.tgz",
- "integrity": "sha512-U1AiltS1O9hSy3rul+Ub82ut2fqIAefiSuwECWt6jlMVUGejvf+5omLcRBSzqbRagSM3hQZbtzdeRc6QVScXTg==",
- "license": "MIT",
- "engines": {
- "node": ">=12.20.0"
- },
- "peerDependencies": {
- "@types/react": ">=18.0.0",
- "immer": ">=9.0.6",
- "react": ">=18.0.0",
- "use-sync-external-store": ">=1.2.0"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "immer": {
- "optional": true
- },
- "react": {
- "optional": true
- },
- "use-sync-external-store": {
- "optional": true
- }
- }
- },
"node_modules/zwitch": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
diff --git a/package.json b/package.json
index 7342d230..01a4bbae 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "off-grid-ai",
- "productName": "Off Grid AI",
+ "productName": "Off Grid AI Desktop",
"version": "0.0.38",
"description": "Off Grid AI — a private, local-first AI that runs open models (text, vision, image, voice) entirely on your device. No cloud, no accounts.",
"license": "AGPL-3.0-only",
@@ -31,41 +31,28 @@
"build:linux": "electron-vite build && electron-builder --linux",
"test:e2e": "electron-vite build && playwright test",
"smoke": "bash scripts/smoke-api.sh",
+ "smoke:packaged": "bash scripts/smoke-packaged.sh",
+ "smoke:dmg": "bash scripts/smoke-dmg-install.sh",
"test:swift": "cd swift-tests/TextExtractorKit && swift test",
- "test:db": "bash scripts/test-db.sh"
+ "test:db": "bash scripts/test-db.sh",
+ "depcruise": "depcruise --config .dependency-cruiser.cjs \"src/**/*.ts\" \"src/**/*.tsx\"",
+ "knip": "knip --no-gitignore"
},
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
- "@dnd-kit/utilities": "^3.2.2",
"@electron-toolkit/preload": "^3.0.2",
"@electron-toolkit/utils": "^4.0.0",
"@lancedb/lancedb": "^0.30.0",
- "@langchain/core": "^1.2.1",
- "@langchain/langgraph": "^1.4.5",
- "@langchain/openai": "^1.5.3",
"@modelcontextprotocol/sdk": "^1.29.0",
"@offgrid/clipboard": "file:./packages/clipboard",
"@offgrid/design": "file:./packages/design",
"@offgrid/models": "file:./packages/models",
"@offgrid/rag": "file:./packages/rag",
"@phosphor-icons/react": "^2.1.10",
- "@radix-ui/react-collapsible": "^1.1.14",
- "@radix-ui/react-dialog": "^1.1.17",
- "@radix-ui/react-dropdown-menu": "^2.1.18",
- "@radix-ui/react-scroll-area": "^1.2.12",
- "@radix-ui/react-select": "^2.3.1",
- "@radix-ui/react-separator": "^1.1.10",
- "@radix-ui/react-slot": "^1.3.0",
- "@radix-ui/react-tabs": "^1.1.13",
- "@radix-ui/react-tooltip": "^1.2.10",
- "@react-three/fiber": "^10.0.0-alpha.1",
"@scure/bip39": "^2.2.0",
"@tabler/icons-react": "^3.36.1",
"@tailwindcss/vite": "^4.1.18",
- "@tsparticles/engine": "^3.9.1",
- "@tsparticles/react": "^3.0.0",
- "@tsparticles/slim": "^3.9.1",
"@xenova/transformers": "^2.17.2",
"apache-arrow": "^18.1.0",
"async-mutex": "^0.5.0",
@@ -75,20 +62,16 @@
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"electron-updater": "^6.8.9",
- "framer-motion": "^12.27.1",
"get-windows": "^9.3.0",
"hash-wasm": "^4.12.0",
"jszip": "^3.10.1",
"kdbxweb": "^2.1.1",
"kokoro-js": "^1.2.1",
"mammoth": "^1.8.0",
- "mini-svg-data-uri": "^1.4.4",
"motion": "^12.27.1",
- "node-llama-cpp": "^3.15.0",
"node-machine-id": "^1.1.12",
"ollama": "^0.6.3",
"pdf-parse": "^1.1.1",
- "posthog-js": "^1.331.0",
"radix-ui": "^1.6.0",
"react-force-graph-3d": "^1.29.0",
"react-markdown": "^10.1.0",
@@ -104,6 +87,8 @@
"@electron-toolkit/eslint-config-ts": "^3.1.0",
"@electron-toolkit/tsconfig": "^2.0.0",
"@playwright/test": "^1.61.1",
+ "@testing-library/react": "^16.3.2",
+ "@testing-library/user-event": "^14.6.1",
"@types/better-sqlite3": "^7.6.13",
"@types/node": "^22.19.1",
"@types/react": "^19.2.7",
@@ -112,20 +97,24 @@
"@vitejs/plugin-react": "^5.1.1",
"@vitest/coverage-v8": "^4.1.10",
"autoprefixer": "^10.4.23",
+ "dependency-cruiser": "^18.0.0",
"electron": "^39.2.6",
- "electron-builder": "^26.0.12",
+ "electron-builder": "26.15.3",
"electron-vite": "^5.0.0",
"eslint": "^9.39.1",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
+ "eslint-plugin-sonarjs": "^4.1.0",
+ "jsdom": "^29.1.1",
+ "knip": "^6.25.0",
"postcss": "^8.5.6",
"prettier": "^3.7.4",
"react": "^19.2.1",
"react-dom": "^19.2.1",
- "simple-icons": "^16.6.0",
"tailwindcss": "^4.1.18",
"typescript": "^5.9.3",
+ "typescript-eslint": "^8.63.0",
"vite": "^7.2.6",
"vitest": "^4.0.17"
}
diff --git a/packages/clipboard/dist/adapters/electron.d.mts b/packages/clipboard/dist/adapters/electron.d.mts
index f163f5e9..289739c3 100644
--- a/packages/clipboard/dist/adapters/electron.d.mts
+++ b/packages/clipboard/dist/adapters/electron.d.mts
@@ -1,4 +1,4 @@
-import { C as ClipboardBridge, a as ClipboardRead, b as ClipboardItem } from '../types-hjEscIvQ.mjs';
+import { C as ClipboardBridge, e as ClipboardRead, b as ClipboardItem } from '../types-B7DdTBPa.mjs';
/** Minimal shape of Electron's nativeImage instances we use. */
interface ElectronImage {
diff --git a/packages/clipboard/dist/adapters/electron.d.ts b/packages/clipboard/dist/adapters/electron.d.ts
index ad52ce5b..9c6d5461 100644
--- a/packages/clipboard/dist/adapters/electron.d.ts
+++ b/packages/clipboard/dist/adapters/electron.d.ts
@@ -1,4 +1,4 @@
-import { C as ClipboardBridge, a as ClipboardRead, b as ClipboardItem } from '../types-hjEscIvQ.js';
+import { C as ClipboardBridge, e as ClipboardRead, b as ClipboardItem } from '../types-B7DdTBPa.js';
/** Minimal shape of Electron's nativeImage instances we use. */
interface ElectronImage {
diff --git a/packages/clipboard/dist/index.d.mts b/packages/clipboard/dist/index.d.mts
index 13ee3254..8dd14ec5 100644
--- a/packages/clipboard/dist/index.d.mts
+++ b/packages/clipboard/dist/index.d.mts
@@ -1,5 +1,5 @@
-import { C as ClipboardBridge, c as ClipboardStore, b as ClipboardItem, d as ClipboardItemDisplay, S as SearchResult } from './types-hjEscIvQ.mjs';
-export { a as ClipboardRead, e as ContentType } from './types-hjEscIvQ.mjs';
+import { C as ClipboardBridge, a as ClipboardStore, b as ClipboardItem, c as ClipboardItemDisplay, S as SearchResult } from './types-B7DdTBPa.mjs';
+export { e as ClipboardRead, d as ContentType } from './types-B7DdTBPa.mjs';
interface ClipboardEngineOptions {
bridge: ClipboardBridge;
diff --git a/packages/clipboard/dist/index.d.ts b/packages/clipboard/dist/index.d.ts
index a321bba4..758c6190 100644
--- a/packages/clipboard/dist/index.d.ts
+++ b/packages/clipboard/dist/index.d.ts
@@ -1,5 +1,5 @@
-import { C as ClipboardBridge, c as ClipboardStore, b as ClipboardItem, d as ClipboardItemDisplay, S as SearchResult } from './types-hjEscIvQ.js';
-export { a as ClipboardRead, e as ContentType } from './types-hjEscIvQ.js';
+import { C as ClipboardBridge, a as ClipboardStore, b as ClipboardItem, c as ClipboardItemDisplay, S as SearchResult } from './types-B7DdTBPa.js';
+export { e as ClipboardRead, d as ContentType } from './types-B7DdTBPa.js';
interface ClipboardEngineOptions {
bridge: ClipboardBridge;
diff --git a/packages/clipboard/dist/index.js b/packages/clipboard/dist/index.js
index 35900ae1..1110dfde 100644
--- a/packages/clipboard/dist/index.js
+++ b/packages/clipboard/dist/index.js
@@ -153,7 +153,7 @@ function findBestMatch(pattern, text) {
return { score, matches: ranges };
}
function isWordBoundary(char) {
- return /[\s\-_.,;:!?()[\]{}'"\/\\]/.test(char);
+ return /[\s\-_.,;:!?()[\]{}'"/\\]/.test(char);
}
function isUpperCase(char) {
return char >= "A" && char <= "Z";
@@ -186,7 +186,10 @@ function wordMatchBonus(query, text) {
const textLower = text.toLowerCase();
let bonus = 0;
for (const word of queryWords) {
- const wordRegex = new RegExp(`(?:^|[\\s\\-_.,;:!?()\\[\\]{}'"\\/\\\\])${escapeRegex(word)}(?:$|[\\s\\-_.,;:!?()\\[\\]{}'"\\/\\\\])`, "i");
+ const wordRegex = new RegExp(
+ `(?:^|[\\s\\-_.,;:!?()\\[\\]{}'"\\/\\\\])${escapeRegex(word)}(?:$|[\\s\\-_.,;:!?()\\[\\]{}'"\\/\\\\])`,
+ "i"
+ );
if (wordRegex.test(text)) {
bonus += 50;
} else if (textLower.includes(word)) {
diff --git a/packages/clipboard/dist/index.mjs b/packages/clipboard/dist/index.mjs
index 0210b32a..b852a0f4 100644
--- a/packages/clipboard/dist/index.mjs
+++ b/packages/clipboard/dist/index.mjs
@@ -125,7 +125,7 @@ function findBestMatch(pattern, text) {
return { score, matches: ranges };
}
function isWordBoundary(char) {
- return /[\s\-_.,;:!?()[\]{}'"\/\\]/.test(char);
+ return /[\s\-_.,;:!?()[\]{}'"/\\]/.test(char);
}
function isUpperCase(char) {
return char >= "A" && char <= "Z";
@@ -158,7 +158,10 @@ function wordMatchBonus(query, text) {
const textLower = text.toLowerCase();
let bonus = 0;
for (const word of queryWords) {
- const wordRegex = new RegExp(`(?:^|[\\s\\-_.,;:!?()\\[\\]{}'"\\/\\\\])${escapeRegex(word)}(?:$|[\\s\\-_.,;:!?()\\[\\]{}'"\\/\\\\])`, "i");
+ const wordRegex = new RegExp(
+ `(?:^|[\\s\\-_.,;:!?()\\[\\]{}'"\\/\\\\])${escapeRegex(word)}(?:$|[\\s\\-_.,;:!?()\\[\\]{}'"\\/\\\\])`,
+ "i"
+ );
if (wordRegex.test(text)) {
bonus += 50;
} else if (textLower.includes(word)) {
diff --git a/packages/clipboard/dist/types-hjEscIvQ.d.mts b/packages/clipboard/dist/types-B7DdTBPa.d.mts
similarity index 95%
rename from packages/clipboard/dist/types-hjEscIvQ.d.mts
rename to packages/clipboard/dist/types-B7DdTBPa.d.mts
index b6c9a278..605dabfa 100644
--- a/packages/clipboard/dist/types-hjEscIvQ.d.mts
+++ b/packages/clipboard/dist/types-B7DdTBPa.d.mts
@@ -58,4 +58,4 @@ interface ClipboardStore {
count(): number;
}
-export type { ClipboardBridge as C, SearchResult as S, ClipboardRead as a, ClipboardItem as b, ClipboardStore as c, ClipboardItemDisplay as d, ContentType as e };
+export type { ClipboardBridge as C, SearchResult as S, ClipboardStore as a, ClipboardItem as b, ClipboardItemDisplay as c, ContentType as d, ClipboardRead as e };
diff --git a/packages/clipboard/dist/types-hjEscIvQ.d.ts b/packages/clipboard/dist/types-B7DdTBPa.d.ts
similarity index 95%
rename from packages/clipboard/dist/types-hjEscIvQ.d.ts
rename to packages/clipboard/dist/types-B7DdTBPa.d.ts
index b6c9a278..605dabfa 100644
--- a/packages/clipboard/dist/types-hjEscIvQ.d.ts
+++ b/packages/clipboard/dist/types-B7DdTBPa.d.ts
@@ -58,4 +58,4 @@ interface ClipboardStore {
count(): number;
}
-export type { ClipboardBridge as C, SearchResult as S, ClipboardRead as a, ClipboardItem as b, ClipboardStore as c, ClipboardItemDisplay as d, ContentType as e };
+export type { ClipboardBridge as C, SearchResult as S, ClipboardStore as a, ClipboardItem as b, ClipboardItemDisplay as c, ContentType as d, ClipboardRead as e };
diff --git a/packages/clipboard/package.json b/packages/clipboard/package.json
index 386d522c..bb98cf1d 100644
--- a/packages/clipboard/package.json
+++ b/packages/clipboard/package.json
@@ -20,7 +20,7 @@
}
},
"scripts": {
- "build": "tsup src/index.ts src/adapters/electron.ts --format esm,cjs --dts",
+ "build": "tsup src/index.ts src/adapters/electron.ts --format esm,cjs --dts --clean",
"dev": "tsup src/index.ts src/adapters/electron.ts --format esm,cjs --dts --watch",
"typecheck": "tsc --noEmit"
}
diff --git a/packages/clipboard/src/adapters/electron.ts b/packages/clipboard/src/adapters/electron.ts
index 94fed68b..f6e6f753 100644
--- a/packages/clipboard/src/adapters/electron.ts
+++ b/packages/clipboard/src/adapters/electron.ts
@@ -3,36 +3,36 @@
// ClipboardBridge interface. Electron is injected by the host so this package
// never imports 'electron' directly and stays installable on mobile.
-import * as fs from 'fs';
-import * as path from 'path';
-import { execSync } from 'child_process';
-import type { ClipboardBridge, ClipboardItem, ClipboardRead, ContentType } from '../types';
+import * as fs from 'fs'
+import * as path from 'path'
+import { execSync } from 'child_process'
+import type { ClipboardBridge, ClipboardItem, ClipboardRead } from '../types'
/** Minimal shape of Electron's nativeImage instances we use. */
interface ElectronImage {
- isEmpty(): boolean;
- toPNG(): Buffer;
+ isEmpty(): boolean
+ toPNG(): Buffer
}
/** Minimal shape of Electron's clipboard module we use. */
export interface ElectronClipboard {
- availableFormats(): string[];
- readImage(): ElectronImage;
- readRTF(): string;
- readText(): string;
- readBuffer(format: string): Buffer;
- read(format: string): string;
- writeText(text: string): void;
- writeRTF(text: string): void;
- writeImage(image: ElectronImage): void;
+ availableFormats(): string[]
+ readImage(): ElectronImage
+ readRTF(): string
+ readText(): string
+ readBuffer(format: string): Buffer
+ read(format: string): string
+ writeText(text: string): void
+ writeRTF(text: string): void
+ writeImage(image: ElectronImage): void
}
/** Minimal shape of Electron's nativeImage module we use. */
export interface ElectronNativeImage {
- createFromBuffer(buffer: Buffer): ElectronImage;
+ createFromBuffer(buffer: Buffer): ElectronImage
}
-const MAX_FILE_SIZE = 20 * 1024 * 1024; // 20MB
+const MAX_FILE_SIZE = 20 * 1024 * 1024 // 20MB
export class ElectronClipboardBridge implements ClipboardBridge {
constructor(
@@ -41,29 +41,29 @@ export class ElectronClipboardBridge implements ClipboardBridge {
) {}
read(): ClipboardRead | null {
- const formats = this.clipboard.availableFormats();
- if (formats.length === 0) return null;
- const extracted = this.extract(formats);
- if (!extracted.rawData || extracted.rawData.length === 0) return null;
- return extracted;
+ const formats = this.clipboard.availableFormats()
+ if (formats.length === 0) return null
+ const extracted = this.extract(formats)
+ if (!extracted.rawData || extracted.rawData.length === 0) return null
+ return extracted
}
write(item: ClipboardItem): void {
switch (item.contentType) {
case 'image': {
- const img = this.nativeImage.createFromBuffer(Buffer.from(item.rawData));
- this.clipboard.writeImage(img);
- return;
+ const img = this.nativeImage.createFromBuffer(Buffer.from(item.rawData))
+ this.clipboard.writeImage(img)
+ return
}
case 'rtf': {
- const rtf = Buffer.from(item.rawData).toString('utf-8');
- this.clipboard.writeRTF(rtf);
- if (item.textContent) this.clipboard.writeText(item.textContent);
- return;
+ const rtf = Buffer.from(item.rawData).toString('utf-8')
+ this.clipboard.writeRTF(rtf)
+ if (item.textContent) this.clipboard.writeText(item.textContent)
+ return
}
default: {
- const text = item.textContent ?? Buffer.from(item.rawData).toString('utf-8');
- this.clipboard.writeText(text);
+ const text = item.textContent ?? Buffer.from(item.rawData).toString('utf-8')
+ this.clipboard.writeText(text)
}
}
}
@@ -71,34 +71,34 @@ export class ElectronClipboardBridge implements ClipboardBridge {
private extract(formats: string[]): ClipboardRead {
// Image first.
if (formats.some((f) => f.includes('image'))) {
- const image = this.clipboard.readImage();
+ const image = this.clipboard.readImage()
if (!image.isEmpty()) {
- return { contentType: 'image', rawData: image.toPNG(), textContent: null };
+ return { contentType: 'image', rawData: image.toPNG(), textContent: null }
}
}
// RTF.
if (formats.includes('text/rtf')) {
- const rtf = this.clipboard.readRTF();
- const text = this.clipboard.readText();
+ const rtf = this.clipboard.readRTF()
+ const text = this.clipboard.readText()
if (rtf) {
- return { contentType: 'rtf', rawData: Buffer.from(rtf, 'utf-8'), textContent: text || null };
+ return { contentType: 'rtf', rawData: Buffer.from(rtf, 'utf-8'), textContent: text || null }
}
}
// File paths (macOS Finder).
if (formats.includes('public.file-url') || formats.includes('text/uri-list')) {
- const fileRead = this.extractFile();
- if (fileRead) return fileRead;
+ const fileRead = this.extractFile()
+ if (fileRead) return fileRead
}
// Plain text.
- const text = this.clipboard.readText();
+ const text = this.clipboard.readText()
if (text) {
- return { contentType: 'text', rawData: Buffer.from(text, 'utf-8'), textContent: text };
+ return { contentType: 'text', rawData: Buffer.from(text, 'utf-8'), textContent: text }
}
- return { contentType: 'text', rawData: Buffer.from(''), textContent: null };
+ return { contentType: 'text', rawData: Buffer.from(''), textContent: null }
}
private extractFile(): ClipboardRead | null {
@@ -107,22 +107,22 @@ export class ElectronClipboardBridge implements ClipboardBridge {
'NSFilenamesPboardType',
'com.apple.nspasteboard.promised-file-url',
'dyn.ah62d4rv4gu8y',
- 'text/uri-list',
- ];
+ 'text/uri-list'
+ ]
- let fileUrl: string | null = null;
+ let fileUrl: string | null = null
for (const formatType of macOSFileTypes) {
- if (fileUrl) break;
+ if (fileUrl) break
try {
- const buffer = this.clipboard.readBuffer(formatType);
+ const buffer = this.clipboard.readBuffer(formatType)
if (buffer && buffer.length > 0) {
- let parsed = buffer.toString('utf-8').replace(/\0/g, '').trim();
+ let parsed = buffer.toString('utf-8').replace(/\0/g, '').trim()
if (formatType === 'NSFilenamesPboardType' && parsed.includes('([^<]+)<\/string>/);
- if (m) parsed = m[1];
+ const m = parsed.match(/([^<]+)<\/string>/)
+ if (m) parsed = m[1]
}
- if (parsed.includes('\n')) parsed = parsed.split('\n')[0].trim();
- if (parsed && (parsed.startsWith('/') || parsed.startsWith('file://'))) fileUrl = parsed;
+ if (parsed.includes('\n')) parsed = parsed.split('\n')[0].trim()
+ if (parsed && (parsed.startsWith('/') || parsed.startsWith('file://'))) fileUrl = parsed
}
} catch {
// not all formats are readable
@@ -130,40 +130,40 @@ export class ElectronClipboardBridge implements ClipboardBridge {
}
if (!fileUrl) {
- const text = this.clipboard.readText();
- if (text && (text.startsWith('/') || text.startsWith('file://'))) fileUrl = text;
+ const text = this.clipboard.readText()
+ if (text && (text.startsWith('/') || text.startsWith('file://'))) fileUrl = text
}
- if (!fileUrl || !(fileUrl.startsWith('/') || fileUrl.startsWith('file://'))) return null;
+ if (!fileUrl || !(fileUrl.startsWith('/') || fileUrl.startsWith('file://'))) return null
- const resolved = resolveFileReferenceUrl(fileUrl);
+ const resolved = resolveFileReferenceUrl(fileUrl)
const filePath = resolved
? resolved
: fileUrl.startsWith('file://')
? decodeURIComponent(fileUrl.replace('file://', ''))
- : fileUrl;
+ : fileUrl
try {
- const stats = fs.statSync(filePath);
+ const stats = fs.statSync(filePath)
if (stats.isFile() && stats.size <= MAX_FILE_SIZE) {
return {
contentType: 'file',
rawData: fs.readFileSync(filePath),
- textContent: path.basename(filePath),
- };
+ textContent: path.basename(filePath)
+ }
}
if (stats.isFile() && stats.size > MAX_FILE_SIZE) {
return {
contentType: 'text',
rawData: Buffer.from(fileUrl, 'utf-8'),
- textContent: `[File too large: ${path.basename(filePath)}]`,
- };
+ textContent: `[File too large: ${path.basename(filePath)}]`
+ }
}
} catch {
// fall through to storing the path as text
}
- return { contentType: 'text', rawData: Buffer.from(fileUrl, 'utf-8'), textContent: fileUrl };
+ return { contentType: 'text', rawData: Buffer.from(fileUrl, 'utf-8'), textContent: fileUrl }
}
}
@@ -172,7 +172,7 @@ export class ElectronClipboardBridge implements ClipboardBridge {
* via AppleScript / NSURL. Returns null for normal URLs. Ported from copyclip.
*/
function resolveFileReferenceUrl(fileUrl: string): string | null {
- if (!fileUrl.includes('/.file/id=')) return null;
+ if (!fileUrl.includes('/.file/id=')) return null
try {
const script = `
use framework "Foundation"
@@ -183,14 +183,14 @@ function resolveFileReferenceUrl(fileUrl: string): string | null {
else
return ""
end if
- `;
+ `
const result = execSync(`osascript -e '${script.replace(/'/g, "'\"'\"'")}'`, {
encoding: 'utf-8',
- timeout: 5000,
- }).trim();
- if (result && result.startsWith('/')) return result;
+ timeout: 5000
+ }).trim()
+ if (result && result.startsWith('/')) return result
} catch {
// ignore
}
- return null;
+ return null
}
diff --git a/packages/clipboard/src/engine.ts b/packages/clipboard/src/engine.ts
index 63761efd..5cecc1a7 100644
--- a/packages/clipboard/src/engine.ts
+++ b/packages/clipboard/src/engine.ts
@@ -6,71 +6,71 @@
// Adapted from copyclip's clipboard-monitor (MIT); the OS-specific reading now
// lives behind ClipboardBridge instead of being baked in.
-import type { ClipboardBridge, ClipboardItem, ClipboardStore } from './types';
+import type { ClipboardBridge, ClipboardItem, ClipboardStore } from './types'
export interface ClipboardEngineOptions {
- bridge: ClipboardBridge;
- store: ClipboardStore;
+ bridge: ClipboardBridge
+ store: ClipboardStore
/** Content hash (sha256 hex). Injected so the package stays dependency-free
* (host passes node crypto on desktop, a JS impl on mobile). */
- hash: (data: Uint8Array) => string;
+ hash: (data: Uint8Array) => string
/** Poll interval in ms. copyclip used 500ms. */
- pollIntervalMs?: number;
+ pollIntervalMs?: number
/** Schedule a repeating timer. Defaults to setInterval; injectable for tests
* or platforms with a different timer API. */
- setInterval?: (cb: () => void, ms: number) => unknown;
- clearInterval?: (handle: unknown) => void;
+ setInterval?: (cb: () => void, ms: number) => unknown
+ clearInterval?: (handle: unknown) => void
}
-type Listener = (item: ClipboardItem) => void;
+type Listener = (item: ClipboardItem) => void
export class ClipboardEngine {
private readonly opts: Required> &
- ClipboardEngineOptions;
- private handle: unknown = null;
- private lastHash = '';
- private listeners: Listener[] = [];
+ ClipboardEngineOptions
+ private handle: unknown = null
+ private lastHash = ''
+ private listeners: Listener[] = []
constructor(options: ClipboardEngineOptions) {
this.opts = {
pollIntervalMs: 500,
- ...options,
- };
+ ...options
+ }
}
/** Subscribe to new clipboard items. Returns an unsubscribe function. */
onItem(listener: Listener): () => void {
- this.listeners.push(listener);
+ this.listeners.push(listener)
return () => {
- this.listeners = this.listeners.filter((l) => l !== listener);
- };
+ this.listeners = this.listeners.filter((l) => l !== listener)
+ }
}
start(): void {
- if (this.handle != null) return;
+ if (this.handle != null) return
// Seed lastHash with the current clipboard so we do not re-capture what is
// already there on startup.
- const current = this.safeRead();
- this.lastHash = current ? this.opts.hash(current.rawData) : '';
+ const current = this.safeRead()
+ this.lastHash = current ? this.opts.hash(current.rawData) : ''
- const schedule = this.opts.setInterval ?? ((cb, ms) => setInterval(cb, ms));
- this.handle = schedule(() => this.tick(), this.opts.pollIntervalMs);
+ const schedule = this.opts.setInterval ?? ((cb, ms) => setInterval(cb, ms))
+ this.handle = schedule(() => this.tick(), this.opts.pollIntervalMs)
}
stop(): void {
- if (this.handle == null) return;
- const clear = this.opts.clearInterval ?? ((h) => clearInterval(h as never));
- clear(this.handle);
- this.handle = null;
+ if (this.handle == null) return
+ const clear = this.opts.clearInterval ?? ((h) => clearInterval(h as never))
+ clear(this.handle)
+ this.handle = null
}
/** Read the clipboard once and persist if it is new. Exposed for tests. */
tick(): ClipboardItem | null {
- const read = this.safeRead();
- if (!read || read.rawData.length === 0) return null;
+ const read = this.safeRead()
+ if (!read || read.rawData.length === 0) return null
- const hash = this.opts.hash(read.rawData);
- if (hash === this.lastHash) return null;
+ const hash = this.opts.hash(read.rawData)
+ if (hash === this.lastHash) return null
const inserted = this.opts.store.insert({
timestamp: Date.now(),
@@ -78,28 +78,32 @@ export class ClipboardEngine {
textContent: read.textContent,
rawData: read.rawData,
sourceApp: read.sourceApp ?? null,
- hash,
- });
+ hash
+ })
// Only mark this content as "seen" AFTER a successful store write — if insert
// throws, leave lastHash so the payload is retried on the next tick instead of
// being silently dropped.
- this.lastHash = hash;
+ this.lastHash = hash
if (inserted) {
// Isolate subscribers: a throwing listener must not escape the poll timer
// (that can take down the Electron main process) or block other listeners.
for (const l of this.listeners) {
- try { l(inserted); } catch (e) { console.error('[clipboard] onItem listener threw', e); }
+ try {
+ l(inserted)
+ } catch (e) {
+ console.error('[clipboard] onItem listener threw', e)
+ }
}
}
- return inserted;
+ return inserted
}
private safeRead() {
try {
- return this.opts.bridge.read();
+ return this.opts.bridge.read()
} catch {
- return null;
+ return null
}
}
}
diff --git a/packages/clipboard/src/fuzzy-search.ts b/packages/clipboard/src/fuzzy-search.ts
index 773ab0a3..aa966a55 100644
--- a/packages/clipboard/src/fuzzy-search.ts
+++ b/packages/clipboard/src/fuzzy-search.ts
@@ -1,7 +1,7 @@
// Fuzzy search, ported from copyclip (https://github.com/alichherawalla/copyclip, MIT).
// Pure TS, reused unchanged except the types import path.
-import type { ClipboardItemDisplay, SearchResult } from './types';
+import type { ClipboardItemDisplay, SearchResult } from './types'
/**
* Fuzzy search implementation ported from Swift version.
@@ -12,183 +12,189 @@ import type { ClipboardItemDisplay, SearchResult } from './types';
*/
interface FuzzyMatchResult {
- score: number;
- matches: Array<[number, number]>;
+ score: number
+ matches: Array<[number, number]>
}
export function fuzzyMatch(pattern: string, text: string): FuzzyMatchResult | null {
if (!pattern || !text) {
- return null;
+ return null
}
- const patternLower = pattern.toLowerCase();
- const textLower = text.toLowerCase();
+ const patternLower = pattern.toLowerCase()
+ const textLower = text.toLowerCase()
// Quick check: all pattern characters must exist in text
- let patternIndex = 0;
+ let patternIndex = 0
for (let i = 0; i < textLower.length && patternIndex < patternLower.length; i++) {
if (textLower[i] === patternLower[patternIndex]) {
- patternIndex++;
+ patternIndex++
}
}
if (patternIndex !== patternLower.length) {
- return null; // Not all characters found
+ return null // Not all characters found
}
// Find best match with scoring
- const result = findBestMatch(patternLower, textLower);
+ const result = findBestMatch(patternLower, textLower)
if (!result) {
- return null;
+ return null
}
- return result;
+ return result
}
function findBestMatch(pattern: string, text: string): FuzzyMatchResult | null {
- const matches: number[] = [];
- let score = 0;
- let patternIdx = 0;
- let consecutiveBonus = 0;
+ const matches: number[] = []
+ let score = 0
+ let patternIdx = 0
+ let consecutiveBonus = 0
for (let textIdx = 0; textIdx < text.length && patternIdx < pattern.length; textIdx++) {
if (text[textIdx] === pattern[patternIdx]) {
- matches.push(textIdx);
+ matches.push(textIdx)
// Base score for match
- let matchScore = 1;
+ let matchScore = 1
// Bonus for consecutive matches
if (matches.length > 1 && matches[matches.length - 1] === matches[matches.length - 2] + 1) {
- consecutiveBonus++;
- matchScore += consecutiveBonus * 2;
+ consecutiveBonus++
+ matchScore += consecutiveBonus * 2
} else {
- consecutiveBonus = 0;
+ consecutiveBonus = 0
}
// Bonus for word boundary (start of word)
if (textIdx === 0 || isWordBoundary(text[textIdx - 1])) {
- matchScore += 5;
+ matchScore += 5
}
// Bonus for camelCase boundary
if (textIdx > 0 && isUpperCase(text[textIdx]) && isLowerCase(text[textIdx - 1])) {
- matchScore += 3;
+ matchScore += 3
}
// Bonus for matching at start
if (textIdx === 0) {
- matchScore += 10;
+ matchScore += 10
}
- score += matchScore;
- patternIdx++;
+ score += matchScore
+ patternIdx++
}
}
if (patternIdx !== pattern.length) {
- return null;
+ return null
}
// Penalty for unmatched characters (to prefer shorter matches)
- const unmatchedPenalty = (text.length - matches.length) * 0.1;
- score = Math.max(0, score - unmatchedPenalty);
+ const unmatchedPenalty = (text.length - matches.length) * 0.1
+ score = Math.max(0, score - unmatchedPenalty)
// Normalize score
- score = score / pattern.length;
+ score = score / pattern.length
// Convert match indices to ranges
- const ranges = indicesToRanges(matches);
+ const ranges = indicesToRanges(matches)
- return { score, matches: ranges };
+ return { score, matches: ranges }
}
function isWordBoundary(char: string): boolean {
- return /[\s\-_.,;:!?()[\]{}'"\/\\]/.test(char);
+ return /[\s\-_.,;:!?()[\]{}'"/\\]/.test(char)
}
function isUpperCase(char: string): boolean {
- return char >= 'A' && char <= 'Z';
+ return char >= 'A' && char <= 'Z'
}
function isLowerCase(char: string): boolean {
- return char >= 'a' && char <= 'z';
+ return char >= 'a' && char <= 'z'
}
function indicesToRanges(indices: number[]): Array<[number, number]> {
if (indices.length === 0) {
- return [];
+ return []
}
- const ranges: Array<[number, number]> = [];
- let start = indices[0];
- let end = indices[0];
+ const ranges: Array<[number, number]> = []
+ let start = indices[0]
+ let end = indices[0]
for (let i = 1; i < indices.length; i++) {
if (indices[i] === end + 1) {
- end = indices[i];
+ end = indices[i]
} else {
- ranges.push([start, end + 1]);
- start = indices[i];
- end = indices[i];
+ ranges.push([start, end + 1])
+ start = indices[i]
+ end = indices[i]
}
}
- ranges.push([start, end + 1]);
- return ranges;
+ ranges.push([start, end + 1])
+ return ranges
}
function wordMatchBonus(query: string, text: string): number {
- const queryWords = query.toLowerCase().split(/\s+/).filter(w => w.length > 0);
- if (queryWords.length === 0) return 0;
+ const queryWords = query
+ .toLowerCase()
+ .split(/\s+/)
+ .filter((w) => w.length > 0)
+ if (queryWords.length === 0) return 0
- const textLower = text.toLowerCase();
- let bonus = 0;
+ const textLower = text.toLowerCase()
+ let bonus = 0
for (const word of queryWords) {
// Exact word match (bounded by word boundaries or string edges)
- const wordRegex = new RegExp(`(?:^|[\\s\\-_.,;:!?()\\[\\]{}'"\\/\\\\])${escapeRegex(word)}(?:$|[\\s\\-_.,;:!?()\\[\\]{}'"\\/\\\\])`, 'i');
+ const wordRegex = new RegExp(
+ `(?:^|[\\s\\-_.,;:!?()\\[\\]{}'"\\/\\\\])${escapeRegex(word)}(?:$|[\\s\\-_.,;:!?()\\[\\]{}'"\\/\\\\])`,
+ 'i'
+ )
if (wordRegex.test(text)) {
- bonus += 50;
+ bonus += 50
} else if (textLower.includes(word)) {
// Substring match (e.g. "kubectl" inside "run_kubectl_apply")
- bonus += 30;
+ bonus += 30
}
}
// Extra bonus when all query words are found as words
- const allWordsFound = queryWords.every(word => textLower.includes(word));
+ const allWordsFound = queryWords.every((word) => textLower.includes(word))
if (allWordsFound) {
- bonus += 20;
+ bonus += 20
}
- return bonus;
+ return bonus
}
function escapeRegex(str: string): string {
- return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
}
export function fuzzySearch(items: ClipboardItemDisplay[], query: string): SearchResult[] {
- const results: SearchResult[] = [];
+ const results: SearchResult[] = []
for (const item of items) {
- const text = item.textContent || item.preview;
- const matchResult = fuzzyMatch(query, text);
+ const text = item.textContent || item.preview
+ const matchResult = fuzzyMatch(query, text)
if (matchResult) {
- const bonus = wordMatchBonus(query, text);
+ const bonus = wordMatchBonus(query, text)
results.push({
item,
score: matchResult.score + bonus,
- matches: matchResult.matches,
- });
+ matches: matchResult.matches
+ })
}
}
// Sort by match score (best first), then recency as tiebreaker
- results.sort((a, b) => b.score - a.score || b.item.timestamp - a.item.timestamp);
+ results.sort((a, b) => b.score - a.score || b.item.timestamp - a.item.timestamp)
// Return top 100 results
- return results.slice(0, 100);
+ return results.slice(0, 100)
}
diff --git a/packages/clipboard/src/index.ts b/packages/clipboard/src/index.ts
index 8744ecb0..374e820c 100644
--- a/packages/clipboard/src/index.ts
+++ b/packages/clipboard/src/index.ts
@@ -7,7 +7,7 @@
// ClipboardStore (a local SQLite store today, an @offgrid/memory op-log later so
// the clipboard syncs across devices).
-export * from './types';
-export { ClipboardEngine } from './engine';
-export type { ClipboardEngineOptions } from './engine';
-export { fuzzyMatch, fuzzySearch } from './fuzzy-search';
+export * from './types'
+export { ClipboardEngine } from './engine'
+export type { ClipboardEngineOptions } from './engine'
+export { fuzzyMatch, fuzzySearch } from './fuzzy-search'
diff --git a/packages/clipboard/src/types.ts b/packages/clipboard/src/types.ts
index ca6cf665..b5ff0c78 100644
--- a/packages/clipboard/src/types.ts
+++ b/packages/clipboard/src/types.ts
@@ -2,43 +2,43 @@
// Adapted from copyclip (https://github.com/alichherawalla/copyclip, MIT);
// rawData uses Uint8Array (not Node Buffer) so the types work on mobile too.
-export type ContentType = 'text' | 'rtf' | 'image' | 'file';
+export type ContentType = 'text' | 'rtf' | 'image' | 'file'
/** A captured clipboard entry, including its raw bytes. */
export interface ClipboardItem {
- id: string;
- timestamp: number;
- contentType: ContentType;
- textContent: string | null;
- rawData: Uint8Array;
+ id: string
+ timestamp: number
+ contentType: ContentType
+ textContent: string | null
+ rawData: Uint8Array
/** App the content was copied from, when the platform can report it. */
- sourceApp: string | null;
+ sourceApp: string | null
/** sha256 of rawData, used for dedup and as the sync key. */
- hash: string;
+ hash: string
}
/** A clipboard entry without the heavy raw bytes, for lists/UI. */
export interface ClipboardItemDisplay {
- id: string;
- timestamp: number;
- contentType: ContentType;
- textContent: string | null;
- sourceApp: string | null;
- preview: string;
+ id: string
+ timestamp: number
+ contentType: ContentType
+ textContent: string | null
+ sourceApp: string | null
+ preview: string
}
export interface SearchResult {
- item: ClipboardItemDisplay;
- score: number;
- matches: Array<[number, number]>;
+ item: ClipboardItemDisplay
+ score: number
+ matches: Array<[number, number]>
}
/** What a platform bridge reads from the OS clipboard at a point in time. */
export interface ClipboardRead {
- contentType: ContentType;
- rawData: Uint8Array;
- textContent: string | null;
- sourceApp?: string | null;
+ contentType: ContentType
+ rawData: Uint8Array
+ textContent: string | null
+ sourceApp?: string | null
}
/**
@@ -48,9 +48,9 @@ export interface ClipboardRead {
*/
export interface ClipboardBridge {
/** Read the current clipboard contents, or null if empty/unsupported. */
- read(): ClipboardRead | null;
+ read(): ClipboardRead | null
/** Put an item back on the OS clipboard (used when the user picks one). */
- write(item: ClipboardItem): void;
+ write(item: ClipboardItem): void
}
/**
@@ -60,10 +60,10 @@ export interface ClipboardBridge {
export interface ClipboardStore {
/** Insert a new item. Returns null if a row with the same hash exists
* (the store should bump that row's timestamp instead). */
- insert(item: Omit): ClipboardItem | null;
- list(limit?: number): ClipboardItemDisplay[];
- get(id: string): ClipboardItem | null;
- remove(id: string): void;
- clear(): void;
- count(): number;
+ insert(item: Omit): ClipboardItem | null
+ list(limit?: number): ClipboardItemDisplay[]
+ get(id: string): ClipboardItem | null
+ remove(id: string): void
+ clear(): void
+ count(): number
}
diff --git a/packages/design/package.json b/packages/design/package.json
index 54447f64..70ea2d88 100644
--- a/packages/design/package.json
+++ b/packages/design/package.json
@@ -22,7 +22,7 @@
"tailwind-preset.js"
],
"scripts": {
- "build": "tsup src/index.ts --format esm,cjs --dts",
+ "build": "tsup src/index.ts --format esm,cjs --dts --clean",
"dev": "tsup src/index.ts --format esm,cjs --dts --watch",
"typecheck": "tsc --noEmit"
}
diff --git a/packages/design/src/index.ts b/packages/design/src/index.ts
index 298dc828..0d592b21 100644
--- a/packages/design/src/index.ts
+++ b/packages/design/src/index.ts
@@ -33,8 +33,8 @@ export const COLORS_LIGHT = {
info: '#525252',
overlay: 'rgba(0, 0, 0, 0.4)',
- divider: '#EBEBEB',
-} as const;
+ divider: '#EBEBEB'
+} as const
export const COLORS_DARK = {
primary: '#34D399',
@@ -63,20 +63,20 @@ export const COLORS_DARK = {
info: '#B0B0B0',
overlay: 'rgba(0, 0, 0, 0.7)',
- divider: '#1A1A1A',
-} as const;
+ divider: '#1A1A1A'
+} as const
-export type ThemeColors = typeof COLORS_LIGHT;
-export type ColorToken = keyof ThemeColors;
+export type ThemeColors = typeof COLORS_LIGHT
+export type ColorToken = keyof ThemeColors
// Monospace font stack. Menlo is the canonical Off Grid face; the rest are
// graceful fallbacks for non-macOS web environments.
export const FONT_MONO =
- "'Menlo', ui-monospace, SFMono-Regular, 'SF Mono', Consolas, 'Liberation Mono', monospace";
+ "'Menlo', ui-monospace, SFMono-Regular, 'SF Mono', Consolas, 'Liberation Mono', monospace"
export const FONTS = {
- mono: 'Menlo',
-} as const;
+ mono: 'Menlo'
+} as const
// Spacing scale in pixels (mobile/src/constants/index.ts SPACING).
export const SPACING = {
@@ -85,17 +85,17 @@ export const SPACING = {
md: 12,
lg: 16,
xl: 24,
- xxl: 32,
-} as const;
+ xxl: 32
+} as const
-export type SpacingToken = keyof typeof SPACING;
+export type SpacingToken = keyof typeof SPACING
// Single accent radius. The design guide favours crisp, sharp edges; 8px is
// the standard control radius, 2px for inline code and chips.
export const RADIUS = {
sm: 2,
- md: 8,
-} as const;
+ md: 8
+} as const
// Typography scale (mobile/src/constants/index.ts TYPOGRAPHY). Sizes in px,
// weights as numeric strings to match the RN tokens. Weights stay <= 400.
@@ -109,17 +109,17 @@ export const TYPOGRAPHY = {
label: { fontSize: 10, fontFamily: FONTS.mono, fontWeight: '400', letterSpacing: 0.3 },
labelSmall: { fontSize: 9, fontFamily: FONTS.mono, fontWeight: '400', letterSpacing: 0.3 },
meta: { fontSize: 10, fontFamily: FONTS.mono, fontWeight: '300', letterSpacing: 0 },
- metaSmall: { fontSize: 9, fontFamily: FONTS.mono, fontWeight: '300', letterSpacing: 0 },
-} as const;
+ metaSmall: { fontSize: 9, fontFamily: FONTS.mono, fontWeight: '300', letterSpacing: 0 }
+} as const
-export type TypographyToken = keyof typeof TYPOGRAPHY;
+export type TypographyToken = keyof typeof TYPOGRAPHY
// Build a CSS variable reference from a color token, e.g.
// cssVar('primary') -> 'var(--og-primary)'.
export function cssVar(token: ColorToken): string {
- return `var(--og-${kebab(token)})`;
+ return `var(--og-${kebab(token)})`
}
function kebab(s: string): string {
- return s.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
+ return s.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`)
}
diff --git a/packages/design/src/tokens.css b/packages/design/src/tokens.css
index 932c3472..4a1a8005 100644
--- a/packages/design/src/tokens.css
+++ b/packages/design/src/tokens.css
@@ -122,8 +122,8 @@
/* Theme-independent tokens */
:root {
- --og-font-mono: 'Menlo', ui-monospace, SFMono-Regular, 'SF Mono', Consolas,
- 'Liberation Mono', monospace;
+ --og-font-mono:
+ 'Menlo', ui-monospace, SFMono-Regular, 'SF Mono', Consolas, 'Liberation Mono', monospace;
--og-radius-sm: 2px;
--og-radius-md: 8px;
diff --git a/packages/design/tailwind-preset.js b/packages/design/tailwind-preset.js
index 54c7bc87..f90436f3 100644
--- a/packages/design/tailwind-preset.js
+++ b/packages/design/tailwind-preset.js
@@ -13,30 +13,30 @@
* `og()` is exported so apps can remap legacy color names (neutral, green, ...)
* onto the same channel variables for a zero-edit migration.
*/
-const og = (name) => `rgb(var(--og-rgb-${name}) / )`;
+const og = (name) => `rgb(var(--og-rgb-${name}) / )`
const colors = {
primary: {
DEFAULT: og('primary'),
dark: og('primary-dark'),
- light: og('primary-light'),
+ light: og('primary-light')
},
background: og('background'),
surface: {
DEFAULT: og('surface'),
light: og('surface-light'),
- hover: og('surface-hover'),
+ hover: og('surface-hover')
},
text: {
DEFAULT: og('text'),
secondary: og('text-secondary'),
muted: og('text-muted'),
- disabled: og('text-disabled'),
+ disabled: og('text-disabled')
},
border: {
DEFAULT: og('border'),
light: og('border-light'),
- focus: og('border-focus'),
+ focus: og('border-focus')
},
success: og('success'),
warning: og('warning'),
@@ -44,10 +44,18 @@ const colors = {
trending: og('trending'),
info: og('info'),
overlay: 'var(--og-overlay)',
- divider: 'var(--og-divider)',
-};
+ divider: 'var(--og-divider)'
+}
-const fontMono = ['Menlo', 'ui-monospace', 'SFMono-Regular', 'SF Mono', 'Consolas', 'Liberation Mono', 'monospace'];
+const fontMono = [
+ 'Menlo',
+ 'ui-monospace',
+ 'SFMono-Regular',
+ 'SF Mono',
+ 'Consolas',
+ 'Liberation Mono',
+ 'monospace'
+]
const preset = {
theme: {
@@ -55,7 +63,7 @@ const preset = {
colors,
fontFamily: {
mono: fontMono,
- sans: fontMono,
+ sans: fontMono
},
fontSize: {
display: ['22px', { lineHeight: '1.2', letterSpacing: '-0.5px', fontWeight: '200' }],
@@ -67,7 +75,7 @@ const preset = {
label: ['10px', { lineHeight: '1.4', letterSpacing: '0.3px', fontWeight: '400' }],
labelSmall: ['9px', { lineHeight: '1.4', letterSpacing: '0.3px', fontWeight: '400' }],
meta: ['10px', { lineHeight: '1.4', fontWeight: '300' }],
- metaSmall: ['9px', { lineHeight: '1.4', fontWeight: '300' }],
+ metaSmall: ['9px', { lineHeight: '1.4', fontWeight: '300' }]
},
spacing: {
xs: '4px',
@@ -75,17 +83,17 @@ const preset = {
md: '12px',
lg: '16px',
xl: '24px',
- xxl: '32px',
+ xxl: '32px'
},
borderRadius: {
sm: '2px',
DEFAULT: '8px',
- md: '8px',
- },
- },
- },
-};
+ md: '8px'
+ }
+ }
+ }
+}
-module.exports = preset;
-module.exports.og = og;
-module.exports.colors = colors;
+module.exports = preset
+module.exports.og = og
+module.exports.colors = colors
diff --git a/packages/models/dist/adapters/node.d.mts b/packages/models/dist/adapters/node.d.mts
index 21fa84d5..9fbf8700 100644
--- a/packages/models/dist/adapters/node.d.mts
+++ b/packages/models/dist/adapters/node.d.mts
@@ -1,4 +1,4 @@
-import { D as DownloadBridge } from '../types-Dbo7SGxu.mjs';
+import { D as DownloadBridge } from '../types-CQDbinZH.mjs';
declare class NodeDownloadBridge implements DownloadBridge {
private readonly modelsDir;
diff --git a/packages/models/dist/adapters/node.d.ts b/packages/models/dist/adapters/node.d.ts
index 4d16fdd5..7f5600a8 100644
--- a/packages/models/dist/adapters/node.d.ts
+++ b/packages/models/dist/adapters/node.d.ts
@@ -1,4 +1,4 @@
-import { D as DownloadBridge } from '../types-Dbo7SGxu.js';
+import { D as DownloadBridge } from '../types-CQDbinZH.js';
declare class NodeDownloadBridge implements DownloadBridge {
private readonly modelsDir;
diff --git a/packages/models/dist/adapters/node.js b/packages/models/dist/adapters/node.js
index 7ec7ef06..9110a5ca 100644
--- a/packages/models/dist/adapters/node.js
+++ b/packages/models/dist/adapters/node.js
@@ -1,3 +1,4 @@
+"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -52,7 +53,6 @@ var NodeDownloadBridge = class {
}
}
async download(url, destPath, opts) {
- var _a;
const tmp = `${destPath}.part`;
let start = 0;
try {
@@ -78,7 +78,7 @@ var NodeDownloadBridge = class {
if (done) break;
out.write(Buffer.from(value));
written += value.length;
- (_a = opts.onProgress) == null ? void 0 : _a.call(opts, written, total || written);
+ opts.onProgress?.(written, total || written);
}
} finally {
out.end();
diff --git a/packages/models/dist/adapters/node.mjs b/packages/models/dist/adapters/node.mjs
index 139cb359..bdefcbbd 100644
--- a/packages/models/dist/adapters/node.mjs
+++ b/packages/models/dist/adapters/node.mjs
@@ -19,7 +19,6 @@ var NodeDownloadBridge = class {
}
}
async download(url, destPath, opts) {
- var _a;
const tmp = `${destPath}.part`;
let start = 0;
try {
@@ -45,7 +44,7 @@ var NodeDownloadBridge = class {
if (done) break;
out.write(Buffer.from(value));
written += value.length;
- (_a = opts.onProgress) == null ? void 0 : _a.call(opts, written, total || written);
+ opts.onProgress?.(written, total || written);
}
} finally {
out.end();
diff --git a/packages/models/dist/index.d.mts b/packages/models/dist/index.d.mts
index 665ae28e..ecf70a74 100644
--- a/packages/models/dist/index.d.mts
+++ b/packages/models/dist/index.d.mts
@@ -1,5 +1,5 @@
-import { M as ModelEntry, a as ModelKind, b as ModelRecommendationTier, D as DownloadBridge, c as ModelStore, d as DownloadProgress } from './types-Dbo7SGxu.mjs';
-export { e as DownloadStatus, I as ImageGenMode, f as ImageGenProvider, g as ImageGenRequest, h as ImageGenResult, i as ModelFile, s as supportsMode, v as validateImageGenRequest } from './types-Dbo7SGxu.mjs';
+import { M as ModelEntry, a as ModelKind, b as ModelRecommendationTier, D as DownloadBridge, c as ModelStore, d as DownloadProgress } from './types-CQDbinZH.mjs';
+export { i as DownloadStatus, I as ImageGenMode, g as ImageGenProvider, e as ImageGenRequest, f as ImageGenResult, h as ModelFile, s as supportsMode, v as validateImageGenRequest } from './types-CQDbinZH.mjs';
declare const RECOMMENDATION_TIERS: ModelRecommendationTier[];
declare function recommendForRam(ramGb: number): ModelRecommendationTier;
diff --git a/packages/models/dist/index.d.ts b/packages/models/dist/index.d.ts
index 96e69dc9..de252739 100644
--- a/packages/models/dist/index.d.ts
+++ b/packages/models/dist/index.d.ts
@@ -1,5 +1,5 @@
-import { M as ModelEntry, a as ModelKind, b as ModelRecommendationTier, D as DownloadBridge, c as ModelStore, d as DownloadProgress } from './types-Dbo7SGxu.js';
-export { e as DownloadStatus, I as ImageGenMode, f as ImageGenProvider, g as ImageGenRequest, h as ImageGenResult, i as ModelFile, s as supportsMode, v as validateImageGenRequest } from './types-Dbo7SGxu.js';
+import { M as ModelEntry, a as ModelKind, b as ModelRecommendationTier, D as DownloadBridge, c as ModelStore, d as DownloadProgress } from './types-CQDbinZH.js';
+export { i as DownloadStatus, I as ImageGenMode, g as ImageGenProvider, e as ImageGenRequest, f as ImageGenResult, h as ModelFile, s as supportsMode, v as validateImageGenRequest } from './types-CQDbinZH.js';
declare const RECOMMENDATION_TIERS: ModelRecommendationTier[];
declare function recommendForRam(ramGb: number): ModelRecommendationTier;
diff --git a/packages/models/dist/index.js b/packages/models/dist/index.js
index c7e46ad2..dc641531 100644
--- a/packages/models/dist/index.js
+++ b/packages/models/dist/index.js
@@ -1,3 +1,4 @@
+"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
@@ -86,7 +87,14 @@ var CATALOG = [
minRamGb: 3,
quant: "Q4_K_M",
releaseDate: "2026-03-01",
- files: [{ name: "Qwen3.5-0.8B-Q4_K_M.gguf", url: resolve("unsloth/Qwen3.5-0.8B-GGUF", "Qwen3.5-0.8B-Q4_K_M.gguf"), sizeBytes: 53e7, role: "primary" }]
+ files: [
+ {
+ name: "Qwen3.5-0.8B-Q4_K_M.gguf",
+ url: resolve("unsloth/Qwen3.5-0.8B-GGUF", "Qwen3.5-0.8B-Q4_K_M.gguf"),
+ sizeBytes: 53e7,
+ role: "primary"
+ }
+ ]
},
{
id: "unsloth/Qwen3.5-2B-GGUF",
@@ -98,7 +106,14 @@ var CATALOG = [
minRamGb: 4,
quant: "Q4_K_M",
releaseDate: "2026-02-28",
- files: [{ name: "Qwen3.5-2B-Q4_K_M.gguf", url: resolve("unsloth/Qwen3.5-2B-GGUF", "Qwen3.5-2B-Q4_K_M.gguf"), sizeBytes: 128e7, role: "primary" }]
+ files: [
+ {
+ name: "Qwen3.5-2B-Q4_K_M.gguf",
+ url: resolve("unsloth/Qwen3.5-2B-GGUF", "Qwen3.5-2B-Q4_K_M.gguf"),
+ sizeBytes: 128e7,
+ role: "primary"
+ }
+ ]
},
{
id: "unsloth/Qwen3.5-4B-GGUF",
@@ -111,7 +126,14 @@ var CATALOG = [
quant: "Q4_K_M",
tags: ["Challenger"],
releaseDate: "2026-03-02",
- files: [{ name: "Qwen3.5-4B-Q4_K_M.gguf", url: resolve("unsloth/Qwen3.5-4B-GGUF", "Qwen3.5-4B-Q4_K_M.gguf"), sizeBytes: 274e7, role: "primary" }]
+ files: [
+ {
+ name: "Qwen3.5-4B-Q4_K_M.gguf",
+ url: resolve("unsloth/Qwen3.5-4B-GGUF", "Qwen3.5-4B-Q4_K_M.gguf"),
+ sizeBytes: 274e7,
+ role: "primary"
+ }
+ ]
},
{
id: "unsloth/Qwen3.5-9B-GGUF",
@@ -124,7 +146,14 @@ var CATALOG = [
quant: "Q4_K_M",
tags: ["Challenger"],
releaseDate: "2026-02-28",
- files: [{ name: "Qwen3.5-9B-Q4_K_M.gguf", url: resolve("unsloth/Qwen3.5-9B-GGUF", "Qwen3.5-9B-Q4_K_M.gguf"), sizeBytes: 568e7, role: "primary" }]
+ files: [
+ {
+ name: "Qwen3.5-9B-Q4_K_M.gguf",
+ url: resolve("unsloth/Qwen3.5-9B-GGUF", "Qwen3.5-9B-Q4_K_M.gguf"),
+ sizeBytes: 568e7,
+ role: "primary"
+ }
+ ]
},
{
id: "unsloth/Qwen3.5-27B-GGUF",
@@ -136,7 +165,14 @@ var CATALOG = [
minRamGb: 24,
quant: "Q4_K_M",
releaseDate: "2026-02-24",
- files: [{ name: "Qwen3.5-27B-Q4_K_M.gguf", url: resolve("unsloth/Qwen3.5-27B-GGUF", "Qwen3.5-27B-Q4_K_M.gguf"), sizeBytes: 1674e7, role: "primary" }]
+ files: [
+ {
+ name: "Qwen3.5-27B-Q4_K_M.gguf",
+ url: resolve("unsloth/Qwen3.5-27B-GGUF", "Qwen3.5-27B-Q4_K_M.gguf"),
+ sizeBytes: 1674e7,
+ role: "primary"
+ }
+ ]
},
{
id: "unsloth/gemma-4-E2B-it-GGUF",
@@ -148,7 +184,14 @@ var CATALOG = [
minRamGb: 5,
quant: "Q4_K_M",
releaseDate: "2026-04-01",
- files: [{ name: "gemma-4-E2B-it-Q4_K_M.gguf", url: resolve("unsloth/gemma-4-E2B-it-GGUF", "gemma-4-E2B-it-Q4_K_M.gguf"), sizeBytes: 311e7, role: "primary" }]
+ files: [
+ {
+ name: "gemma-4-E2B-it-Q4_K_M.gguf",
+ url: resolve("unsloth/gemma-4-E2B-it-GGUF", "gemma-4-E2B-it-Q4_K_M.gguf"),
+ sizeBytes: 311e7,
+ role: "primary"
+ }
+ ]
},
{
id: "unsloth/gemma-4-12b-it-GGUF",
@@ -161,7 +204,14 @@ var CATALOG = [
quant: "Q4_K_M",
tags: ["Challenger"],
releaseDate: "2026-05-29",
- files: [{ name: "gemma-4-12b-it-Q4_K_M.gguf", url: resolve("unsloth/gemma-4-12b-it-GGUF", "gemma-4-12b-it-Q4_K_M.gguf"), sizeBytes: 712e7, role: "primary" }]
+ files: [
+ {
+ name: "gemma-4-12b-it-Q4_K_M.gguf",
+ url: resolve("unsloth/gemma-4-12b-it-GGUF", "gemma-4-12b-it-Q4_K_M.gguf"),
+ sizeBytes: 712e7,
+ role: "primary"
+ }
+ ]
},
{
id: "unsloth/gemma-4-26B-A4B-it-GGUF",
@@ -174,7 +224,14 @@ var CATALOG = [
quant: "Q4_K_M",
tags: ["Challenger"],
releaseDate: "2026-04-01",
- files: [{ name: "gemma-4-26B-A4B-it-UD-Q4_K_M.gguf", url: resolve("unsloth/gemma-4-26B-A4B-it-GGUF", "gemma-4-26B-A4B-it-UD-Q4_K_M.gguf"), sizeBytes: 1695e7, role: "primary" }]
+ files: [
+ {
+ name: "gemma-4-26B-A4B-it-UD-Q4_K_M.gguf",
+ url: resolve("unsloth/gemma-4-26B-A4B-it-GGUF", "gemma-4-26B-A4B-it-UD-Q4_K_M.gguf"),
+ sizeBytes: 1695e7,
+ role: "primary"
+ }
+ ]
},
{
id: "unsloth/gemma-4-31B-it-GGUF",
@@ -186,7 +243,14 @@ var CATALOG = [
minRamGb: 24,
quant: "Q4_K_M",
releaseDate: "2026-04-01",
- files: [{ name: "gemma-4-31B-it-Q4_K_M.gguf", url: resolve("unsloth/gemma-4-31B-it-GGUF", "gemma-4-31B-it-Q4_K_M.gguf"), sizeBytes: 1832e7, role: "primary" }]
+ files: [
+ {
+ name: "gemma-4-31B-it-Q4_K_M.gguf",
+ url: resolve("unsloth/gemma-4-31B-it-GGUF", "gemma-4-31B-it-Q4_K_M.gguf"),
+ sizeBytes: 1832e7,
+ role: "primary"
+ }
+ ]
},
// --- vision (multimodal LLM) ---
{
@@ -201,8 +265,18 @@ var CATALOG = [
tags: ["Challenger"],
releaseDate: "2026-04-01",
files: [
- { name: "gemma-4-E4B-it-Q4_K_M.gguf", url: resolve("unsloth/gemma-4-E4B-it-GGUF", "gemma-4-E4B-it-Q4_K_M.gguf"), sizeBytes: 498e7, role: "primary" },
- { name: "mmproj-gemma-4-E4B-it-F16.gguf", url: resolve("unsloth/gemma-4-E4B-it-GGUF", "mmproj-F16.gguf"), sizeBytes: 99e7, role: "mmproj" }
+ {
+ name: "gemma-4-E4B-it-Q4_K_M.gguf",
+ url: resolve("unsloth/gemma-4-E4B-it-GGUF", "gemma-4-E4B-it-Q4_K_M.gguf"),
+ sizeBytes: 498e7,
+ role: "primary"
+ },
+ {
+ name: "mmproj-gemma-4-E4B-it-F16.gguf",
+ url: resolve("unsloth/gemma-4-E4B-it-GGUF", "mmproj-F16.gguf"),
+ sizeBytes: 99e7,
+ role: "mmproj"
+ }
]
},
{
@@ -216,8 +290,21 @@ var CATALOG = [
quant: "Q4_K_M",
releaseDate: "2025-04-21",
files: [
- { name: "SmolVLM2-2.2B-Instruct-Q4_K_M.gguf", url: resolve("ggml-org/SmolVLM2-2.2B-Instruct-GGUF", "SmolVLM2-2.2B-Instruct-Q4_K_M.gguf"), sizeBytes: 111e7, role: "primary" },
- { name: "mmproj-SmolVLM2-2.2B-Instruct-f16.gguf", url: resolve("ggml-org/SmolVLM2-2.2B-Instruct-GGUF", "mmproj-SmolVLM2-2.2B-Instruct-f16.gguf"), sizeBytes: 87e7, role: "mmproj" }
+ {
+ name: "SmolVLM2-2.2B-Instruct-Q4_K_M.gguf",
+ url: resolve("ggml-org/SmolVLM2-2.2B-Instruct-GGUF", "SmolVLM2-2.2B-Instruct-Q4_K_M.gguf"),
+ sizeBytes: 111e7,
+ role: "primary"
+ },
+ {
+ name: "mmproj-SmolVLM2-2.2B-Instruct-f16.gguf",
+ url: resolve(
+ "ggml-org/SmolVLM2-2.2B-Instruct-GGUF",
+ "mmproj-SmolVLM2-2.2B-Instruct-f16.gguf"
+ ),
+ sizeBytes: 87e7,
+ role: "mmproj"
+ }
]
},
{
@@ -231,8 +318,18 @@ var CATALOG = [
quant: "Q4_K_M",
releaseDate: "2025-10-30",
files: [
- { name: "Qwen3-VL-2B-Instruct-Q4_K_M.gguf", url: resolve("unsloth/Qwen3-VL-2B-Instruct-GGUF", "Qwen3-VL-2B-Instruct-Q4_K_M.gguf"), sizeBytes: 111e7, role: "primary" },
- { name: "mmproj-Qwen3-VL-2B-Instruct-F16.gguf", url: resolve("unsloth/Qwen3-VL-2B-Instruct-GGUF", "mmproj-BF16.gguf"), sizeBytes: 82e7, role: "mmproj" }
+ {
+ name: "Qwen3-VL-2B-Instruct-Q4_K_M.gguf",
+ url: resolve("unsloth/Qwen3-VL-2B-Instruct-GGUF", "Qwen3-VL-2B-Instruct-Q4_K_M.gguf"),
+ sizeBytes: 111e7,
+ role: "primary"
+ },
+ {
+ name: "mmproj-Qwen3-VL-2B-Instruct-F16.gguf",
+ url: resolve("unsloth/Qwen3-VL-2B-Instruct-GGUF", "mmproj-BF16.gguf"),
+ sizeBytes: 82e7,
+ role: "mmproj"
+ }
]
},
{
@@ -246,8 +343,18 @@ var CATALOG = [
quant: "Q4_K_M",
releaseDate: "2025-10-30",
files: [
- { name: "Qwen3-VL-8B-Instruct-Q4_K_M.gguf", url: resolve("unsloth/Qwen3-VL-8B-Instruct-GGUF", "Qwen3-VL-8B-Instruct-Q4_K_M.gguf"), sizeBytes: 503e7, role: "primary" },
- { name: "mmproj-Qwen3-VL-8B-Instruct-F16.gguf", url: resolve("unsloth/Qwen3-VL-8B-Instruct-GGUF", "mmproj-BF16.gguf"), sizeBytes: 116e7, role: "mmproj" }
+ {
+ name: "Qwen3-VL-8B-Instruct-Q4_K_M.gguf",
+ url: resolve("unsloth/Qwen3-VL-8B-Instruct-GGUF", "Qwen3-VL-8B-Instruct-Q4_K_M.gguf"),
+ sizeBytes: 503e7,
+ role: "primary"
+ },
+ {
+ name: "mmproj-Qwen3-VL-8B-Instruct-F16.gguf",
+ url: resolve("unsloth/Qwen3-VL-8B-Instruct-GGUF", "mmproj-BF16.gguf"),
+ sizeBytes: 116e7,
+ role: "mmproj"
+ }
]
},
// --- image generation — 2026 / fast few-step models only (open weight) ---
@@ -318,7 +425,12 @@ var CATALOG = [
minRamGb: 8,
imageModes: ["txt2img", "img2img"],
files: [
- { name: "sd_xl_turbo_1.0.q8_0.gguf", url: resolve("OlegSkutte/sdxl-turbo-GGUF", "sd_xl_turbo_1.0.q8_0.gguf"), role: "primary", sizeBytes: 41e8 }
+ {
+ name: "sd_xl_turbo_1.0.q8_0.gguf",
+ url: resolve("OlegSkutte/sdxl-turbo-GGUF", "sd_xl_turbo_1.0.q8_0.gguf"),
+ role: "primary",
+ sizeBytes: 41e8
+ }
]
},
// SDXL finetunes — Off Grid GGUF builds (q8). The community GGUF quants of these
@@ -335,7 +447,14 @@ var CATALOG = [
quant: "Q8_0",
releaseDate: "2024-08-05",
imageModes: ["txt2img", "img2img"],
- files: [{ name: "realvisxl-v5.0-Q8_0.gguf", url: resolve("offgrid-ai/realvisxl-v5.0-GGUF", "realvisxl-v5.0-Q8_0.gguf"), role: "primary", sizeBytes: 418e7 }]
+ files: [
+ {
+ name: "realvisxl-v5.0-Q8_0.gguf",
+ url: resolve("offgrid-ai/realvisxl-v5.0-GGUF", "realvisxl-v5.0-Q8_0.gguf"),
+ role: "primary",
+ sizeBytes: 418e7
+ }
+ ]
},
{
// Light (Q4_K) sibling — ~35% less memory, runs on a 16GB Mac. Tagged 'Light'
@@ -350,7 +469,14 @@ var CATALOG = [
quant: "Q4_K",
releaseDate: "2024-08-05",
imageModes: ["txt2img", "img2img"],
- files: [{ name: "realvisxl-v5.0-Q4_K.gguf", url: resolve("offgrid-ai/realvisxl-v5.0-GGUF", "realvisxl-v5.0-Q4_K.gguf"), role: "primary", sizeBytes: 28e8 }]
+ files: [
+ {
+ name: "realvisxl-v5.0-Q4_K.gguf",
+ url: resolve("offgrid-ai/realvisxl-v5.0-GGUF", "realvisxl-v5.0-Q4_K.gguf"),
+ role: "primary",
+ sizeBytes: 28e8
+ }
+ ]
},
{
id: "offgrid-ai/realvisxl-v5.0-lightning-GGUF",
@@ -365,7 +491,17 @@ var CATALOG = [
quant: "Q8_0",
releaseDate: "2024-09-02",
imageModes: ["txt2img", "img2img"],
- files: [{ name: "realvisxl-v5.0-lightning-Q8_0.gguf", url: resolve("offgrid-ai/realvisxl-v5.0-lightning-GGUF", "realvisxl-v5.0-lightning-Q8_0.gguf"), role: "primary", sizeBytes: 418e7 }]
+ files: [
+ {
+ name: "realvisxl-v5.0-lightning-Q8_0.gguf",
+ url: resolve(
+ "offgrid-ai/realvisxl-v5.0-lightning-GGUF",
+ "realvisxl-v5.0-lightning-Q8_0.gguf"
+ ),
+ role: "primary",
+ sizeBytes: 418e7
+ }
+ ]
},
{
// Light (Q4_K) sibling — few-step photoreal, ~35% less memory, 16GB-friendly.
@@ -379,7 +515,17 @@ var CATALOG = [
quant: "Q4_K",
releaseDate: "2024-09-02",
imageModes: ["txt2img", "img2img"],
- files: [{ name: "realvisxl-v5.0-lightning-Q4_K.gguf", url: resolve("offgrid-ai/realvisxl-v5.0-lightning-GGUF", "realvisxl-v5.0-lightning-Q4_K.gguf"), role: "primary", sizeBytes: 28e8 }]
+ files: [
+ {
+ name: "realvisxl-v5.0-lightning-Q4_K.gguf",
+ url: resolve(
+ "offgrid-ai/realvisxl-v5.0-lightning-GGUF",
+ "realvisxl-v5.0-lightning-Q4_K.gguf"
+ ),
+ role: "primary",
+ sizeBytes: 28e8
+ }
+ ]
},
{
id: "offgrid-ai/dreamshaper-xl-v2-turbo-GGUF",
@@ -394,7 +540,17 @@ var CATALOG = [
quant: "Q8_0",
releaseDate: "2024-02-07",
imageModes: ["txt2img", "img2img"],
- files: [{ name: "dreamshaper-xl-v2-turbo-Q8_0.gguf", url: resolve("offgrid-ai/dreamshaper-xl-v2-turbo-GGUF", "dreamshaper-xl-v2-turbo-Q8_0.gguf"), role: "primary", sizeBytes: 418e7 }]
+ files: [
+ {
+ name: "dreamshaper-xl-v2-turbo-Q8_0.gguf",
+ url: resolve(
+ "offgrid-ai/dreamshaper-xl-v2-turbo-GGUF",
+ "dreamshaper-xl-v2-turbo-Q8_0.gguf"
+ ),
+ role: "primary",
+ sizeBytes: 418e7
+ }
+ ]
},
{
// Lighter Q4_K quant of the same distilled turbo model — ~35% less memory
@@ -412,7 +568,17 @@ var CATALOG = [
quant: "Q4_K",
releaseDate: "2024-02-07",
imageModes: ["txt2img", "img2img"],
- files: [{ name: "dreamshaper-xl-v2-turbo-Q4_K.gguf", url: resolve("offgrid-ai/dreamshaper-xl-v2-turbo-GGUF", "dreamshaper-xl-v2-turbo-Q4_K.gguf"), role: "primary", sizeBytes: 28e8 }]
+ files: [
+ {
+ name: "dreamshaper-xl-v2-turbo-Q4_K.gguf",
+ url: resolve(
+ "offgrid-ai/dreamshaper-xl-v2-turbo-GGUF",
+ "dreamshaper-xl-v2-turbo-Q4_K.gguf"
+ ),
+ role: "primary",
+ sizeBytes: 28e8
+ }
+ ]
},
{
id: "offgrid-ai/juggernaut-xl-v9-GGUF",
@@ -425,7 +591,14 @@ var CATALOG = [
quant: "Q8_0",
releaseDate: "2024-02-18",
imageModes: ["txt2img", "img2img"],
- files: [{ name: "juggernaut-xl-v9-Q8_0.gguf", url: resolve("offgrid-ai/juggernaut-xl-v9-GGUF", "juggernaut-xl-v9-Q8_0.gguf"), role: "primary", sizeBytes: 435e7 }]
+ files: [
+ {
+ name: "juggernaut-xl-v9-Q8_0.gguf",
+ url: resolve("offgrid-ai/juggernaut-xl-v9-GGUF", "juggernaut-xl-v9-Q8_0.gguf"),
+ role: "primary",
+ sizeBytes: 435e7
+ }
+ ]
},
{
// Light (Q4_K) sibling — ~35% less memory, 16GB-friendly.
@@ -439,7 +612,14 @@ var CATALOG = [
quant: "Q4_K",
releaseDate: "2024-02-18",
imageModes: ["txt2img", "img2img"],
- files: [{ name: "juggernaut-xl-v9-Q4_K.gguf", url: resolve("offgrid-ai/juggernaut-xl-v9-GGUF", "juggernaut-xl-v9-Q4_K.gguf"), role: "primary", sizeBytes: 29e8 }]
+ files: [
+ {
+ name: "juggernaut-xl-v9-Q4_K.gguf",
+ url: resolve("offgrid-ai/juggernaut-xl-v9-GGUF", "juggernaut-xl-v9-Q4_K.gguf"),
+ role: "primary",
+ sizeBytes: 29e8
+ }
+ ]
},
{
id: "offgrid-ai/animagine-xl-4.0-GGUF",
@@ -452,7 +632,14 @@ var CATALOG = [
quant: "Q8_0",
releaseDate: "2025-01-10",
imageModes: ["txt2img", "img2img"],
- files: [{ name: "animagine-xl-4.0-Q8_0.gguf", url: resolve("offgrid-ai/animagine-xl-4.0-GGUF", "animagine-xl-4.0-Q8_0.gguf"), role: "primary", sizeBytes: 418e7 }]
+ files: [
+ {
+ name: "animagine-xl-4.0-Q8_0.gguf",
+ url: resolve("offgrid-ai/animagine-xl-4.0-GGUF", "animagine-xl-4.0-Q8_0.gguf"),
+ role: "primary",
+ sizeBytes: 418e7
+ }
+ ]
},
{
// Light (Q4_K) sibling — ~35% less memory, 16GB-friendly.
@@ -466,7 +653,14 @@ var CATALOG = [
quant: "Q4_K",
releaseDate: "2025-01-10",
imageModes: ["txt2img", "img2img"],
- files: [{ name: "animagine-xl-4.0-Q4_K.gguf", url: resolve("offgrid-ai/animagine-xl-4.0-GGUF", "animagine-xl-4.0-Q4_K.gguf"), role: "primary", sizeBytes: 28e8 }]
+ files: [
+ {
+ name: "animagine-xl-4.0-Q4_K.gguf",
+ url: resolve("offgrid-ai/animagine-xl-4.0-GGUF", "animagine-xl-4.0-Q4_K.gguf"),
+ role: "primary",
+ sizeBytes: 28e8
+ }
+ ]
},
{
id: "offgrid-ai/illustrious-xl-v2.0-GGUF",
@@ -479,7 +673,14 @@ var CATALOG = [
quant: "Q8_0",
releaseDate: "2025-04-18",
imageModes: ["txt2img", "img2img"],
- files: [{ name: "illustrious-xl-v2.0-Q8_0.gguf", url: resolve("offgrid-ai/illustrious-xl-v2.0-GGUF", "illustrious-xl-v2.0-Q8_0.gguf"), role: "primary", sizeBytes: 418e7 }]
+ files: [
+ {
+ name: "illustrious-xl-v2.0-Q8_0.gguf",
+ url: resolve("offgrid-ai/illustrious-xl-v2.0-GGUF", "illustrious-xl-v2.0-Q8_0.gguf"),
+ role: "primary",
+ sizeBytes: 418e7
+ }
+ ]
},
{
// Light (Q4_K) sibling — ~35% less memory, 16GB-friendly.
@@ -493,7 +694,14 @@ var CATALOG = [
quant: "Q4_K",
releaseDate: "2025-04-18",
imageModes: ["txt2img", "img2img"],
- files: [{ name: "illustrious-xl-v2.0-Q4_K.gguf", url: resolve("offgrid-ai/illustrious-xl-v2.0-GGUF", "illustrious-xl-v2.0-Q4_K.gguf"), role: "primary", sizeBytes: 28e8 }]
+ files: [
+ {
+ name: "illustrious-xl-v2.0-Q4_K.gguf",
+ url: resolve("offgrid-ai/illustrious-xl-v2.0-GGUF", "illustrious-xl-v2.0-Q4_K.gguf"),
+ role: "primary",
+ sizeBytes: 28e8
+ }
+ ]
},
{
id: "offgrid-ai/pony-diffusion-v6-xl-GGUF",
@@ -506,7 +714,14 @@ var CATALOG = [
quant: "Q8_0",
releaseDate: "2024-05-25",
imageModes: ["txt2img", "img2img"],
- files: [{ name: "pony-diffusion-v6-xl-Q8_0.gguf", url: resolve("offgrid-ai/pony-diffusion-v6-xl-GGUF", "pony-diffusion-v6-xl-Q8_0.gguf"), role: "primary", sizeBytes: 418e7 }]
+ files: [
+ {
+ name: "pony-diffusion-v6-xl-Q8_0.gguf",
+ url: resolve("offgrid-ai/pony-diffusion-v6-xl-GGUF", "pony-diffusion-v6-xl-Q8_0.gguf"),
+ role: "primary",
+ sizeBytes: 418e7
+ }
+ ]
},
{
// Light (Q4_K) sibling — ~35% less memory, 16GB-friendly.
@@ -520,7 +735,14 @@ var CATALOG = [
quant: "Q4_K",
releaseDate: "2024-05-25",
imageModes: ["txt2img", "img2img"],
- files: [{ name: "pony-diffusion-v6-xl-Q4_K.gguf", url: resolve("offgrid-ai/pony-diffusion-v6-xl-GGUF", "pony-diffusion-v6-xl-Q4_K.gguf"), role: "primary", sizeBytes: 28e8 }]
+ files: [
+ {
+ name: "pony-diffusion-v6-xl-Q4_K.gguf",
+ url: resolve("offgrid-ai/pony-diffusion-v6-xl-GGUF", "pony-diffusion-v6-xl-Q4_K.gguf"),
+ role: "primary",
+ sizeBytes: 28e8
+ }
+ ]
},
// --- voice (TTS); open models, ONNX runtime (no Python) ---
{
@@ -555,7 +777,10 @@ var CATALOG = [
},
{
name: "en_US-lessac-medium.onnx.json",
- url: resolve("rhasspy/piper-voices", "en/en_US/lessac/medium/en_US-lessac-medium.onnx.json"),
+ url: resolve(
+ "rhasspy/piper-voices",
+ "en/en_US/lessac/medium/en_US-lessac-medium.onnx.json"
+ ),
role: "aux"
}
]
@@ -568,7 +793,14 @@ var CATALOG = [
org: "ggerganov",
description: "Fastest, smallest \u2014 lowest accuracy",
minRamGb: 2,
- files: [{ name: "ggml-tiny.bin", url: resolve("ggerganov/whisper.cpp", "ggml-tiny.bin"), role: "primary", sizeBytes: 777e5 }]
+ files: [
+ {
+ name: "ggml-tiny.bin",
+ url: resolve("ggerganov/whisper.cpp", "ggml-tiny.bin"),
+ role: "primary",
+ sizeBytes: 777e5
+ }
+ ]
},
{
id: "ggerganov/whisper.cpp/base",
@@ -577,7 +809,14 @@ var CATALOG = [
org: "ggerganov",
description: "Offline speech-to-text (base) \u2014 good speed/quality default",
minRamGb: 3,
- files: [{ name: "ggml-base.bin", url: resolve("ggerganov/whisper.cpp", "ggml-base.bin"), role: "primary", sizeBytes: 147951e3 }]
+ files: [
+ {
+ name: "ggml-base.bin",
+ url: resolve("ggerganov/whisper.cpp", "ggml-base.bin"),
+ role: "primary",
+ sizeBytes: 147951e3
+ }
+ ]
},
{
id: "ggerganov/whisper.cpp/small",
@@ -586,7 +825,14 @@ var CATALOG = [
org: "ggerganov",
description: "Offline speech-to-text (higher accuracy)",
minRamGb: 4,
- files: [{ name: "ggml-small.bin", url: resolve("ggerganov/whisper.cpp", "ggml-small.bin"), role: "primary", sizeBytes: 487601e3 }]
+ files: [
+ {
+ name: "ggml-small.bin",
+ url: resolve("ggerganov/whisper.cpp", "ggml-small.bin"),
+ role: "primary",
+ sizeBytes: 487601e3
+ }
+ ]
},
{
id: "ggerganov/whisper.cpp/medium",
@@ -595,7 +841,14 @@ var CATALOG = [
org: "ggerganov",
description: "High accuracy; slower",
minRamGb: 6,
- files: [{ name: "ggml-medium.bin", url: resolve("ggerganov/whisper.cpp", "ggml-medium.bin"), role: "primary", sizeBytes: 1533e6 }]
+ files: [
+ {
+ name: "ggml-medium.bin",
+ url: resolve("ggerganov/whisper.cpp", "ggml-medium.bin"),
+ role: "primary",
+ sizeBytes: 1533e6
+ }
+ ]
},
{
id: "ggerganov/whisper.cpp/large-v3-turbo",
@@ -604,7 +857,14 @@ var CATALOG = [
org: "ggerganov",
description: "Near-large accuracy, much faster \u2014 recommended",
minRamGb: 6,
- files: [{ name: "ggml-large-v3-turbo.bin", url: resolve("ggerganov/whisper.cpp", "ggml-large-v3-turbo.bin"), role: "primary", sizeBytes: 1624e6 }]
+ files: [
+ {
+ name: "ggml-large-v3-turbo.bin",
+ url: resolve("ggerganov/whisper.cpp", "ggml-large-v3-turbo.bin"),
+ role: "primary",
+ sizeBytes: 1624e6
+ }
+ ]
},
{
id: "ggerganov/whisper.cpp/large-v3",
@@ -613,7 +873,14 @@ var CATALOG = [
org: "ggerganov",
description: "Highest accuracy (large); needs more RAM",
minRamGb: 8,
- files: [{ name: "ggml-large-v3.bin", url: resolve("ggerganov/whisper.cpp", "ggml-large-v3.bin"), role: "primary", sizeBytes: 3095e6 }]
+ files: [
+ {
+ name: "ggml-large-v3.bin",
+ url: resolve("ggerganov/whisper.cpp", "ggml-large-v3.bin"),
+ role: "primary",
+ sizeBytes: 3095e6
+ }
+ ]
},
// --- transcription (Parakeet, NVIDIA NeMo) — sherpa-onnx offline transducer (ONNX).
// A model is 4 files (encoder/decoder/joiner/tokens); on-disk names are slug-prefixed
@@ -629,10 +896,30 @@ var CATALOG = [
minRamGb: 4,
tags: ["Accurate", "English"],
files: [
- { name: "parakeet-v2.encoder.int8.onnx", url: resolve("csukuangfj/sherpa-onnx-nemo-parakeet-tdt-0.6b-v2-int8", "encoder.int8.onnx"), role: "primary", sizeBytes: 652e6 },
- { name: "parakeet-v2.decoder.int8.onnx", url: resolve("csukuangfj/sherpa-onnx-nemo-parakeet-tdt-0.6b-v2-int8", "decoder.int8.onnx"), role: "aux", sizeBytes: 726e4 },
- { name: "parakeet-v2.joiner.int8.onnx", url: resolve("csukuangfj/sherpa-onnx-nemo-parakeet-tdt-0.6b-v2-int8", "joiner.int8.onnx"), role: "aux", sizeBytes: 174e4 },
- { name: "parakeet-v2.tokens.txt", url: resolve("csukuangfj/sherpa-onnx-nemo-parakeet-tdt-0.6b-v2-int8", "tokens.txt"), role: "tokenizer", sizeBytes: 9600 }
+ {
+ name: "parakeet-v2.encoder.int8.onnx",
+ url: resolve("csukuangfj/sherpa-onnx-nemo-parakeet-tdt-0.6b-v2-int8", "encoder.int8.onnx"),
+ role: "primary",
+ sizeBytes: 652e6
+ },
+ {
+ name: "parakeet-v2.decoder.int8.onnx",
+ url: resolve("csukuangfj/sherpa-onnx-nemo-parakeet-tdt-0.6b-v2-int8", "decoder.int8.onnx"),
+ role: "aux",
+ sizeBytes: 726e4
+ },
+ {
+ name: "parakeet-v2.joiner.int8.onnx",
+ url: resolve("csukuangfj/sherpa-onnx-nemo-parakeet-tdt-0.6b-v2-int8", "joiner.int8.onnx"),
+ role: "aux",
+ sizeBytes: 174e4
+ },
+ {
+ name: "parakeet-v2.tokens.txt",
+ url: resolve("csukuangfj/sherpa-onnx-nemo-parakeet-tdt-0.6b-v2-int8", "tokens.txt"),
+ role: "tokenizer",
+ sizeBytes: 9600
+ }
]
},
{
@@ -646,10 +933,30 @@ var CATALOG = [
isNew: true,
tags: ["Accurate", "Multilingual"],
files: [
- { name: "parakeet-v3.encoder.int8.onnx", url: resolve("csukuangfj/sherpa-onnx-nemo-parakeet-tdt-0.6b-v3-int8", "encoder.int8.onnx"), role: "primary", sizeBytes: 652e6 },
- { name: "parakeet-v3.decoder.int8.onnx", url: resolve("csukuangfj/sherpa-onnx-nemo-parakeet-tdt-0.6b-v3-int8", "decoder.int8.onnx"), role: "aux", sizeBytes: 726e4 },
- { name: "parakeet-v3.joiner.int8.onnx", url: resolve("csukuangfj/sherpa-onnx-nemo-parakeet-tdt-0.6b-v3-int8", "joiner.int8.onnx"), role: "aux", sizeBytes: 174e4 },
- { name: "parakeet-v3.tokens.txt", url: resolve("csukuangfj/sherpa-onnx-nemo-parakeet-tdt-0.6b-v3-int8", "tokens.txt"), role: "tokenizer", sizeBytes: 9600 }
+ {
+ name: "parakeet-v3.encoder.int8.onnx",
+ url: resolve("csukuangfj/sherpa-onnx-nemo-parakeet-tdt-0.6b-v3-int8", "encoder.int8.onnx"),
+ role: "primary",
+ sizeBytes: 652e6
+ },
+ {
+ name: "parakeet-v3.decoder.int8.onnx",
+ url: resolve("csukuangfj/sherpa-onnx-nemo-parakeet-tdt-0.6b-v3-int8", "decoder.int8.onnx"),
+ role: "aux",
+ sizeBytes: 726e4
+ },
+ {
+ name: "parakeet-v3.joiner.int8.onnx",
+ url: resolve("csukuangfj/sherpa-onnx-nemo-parakeet-tdt-0.6b-v3-int8", "joiner.int8.onnx"),
+ role: "aux",
+ sizeBytes: 174e4
+ },
+ {
+ name: "parakeet-v3.tokens.txt",
+ url: resolve("csukuangfj/sherpa-onnx-nemo-parakeet-tdt-0.6b-v3-int8", "tokens.txt"),
+ role: "tokenizer",
+ sizeBytes: 9600
+ }
]
}
];
@@ -676,8 +983,7 @@ var ModelDownloader = class {
return this.store.isInstalled(modelId);
}
cancel(modelId) {
- var _a;
- (_a = this.aborts.get(modelId)) == null ? void 0 : _a.abort();
+ this.aborts.get(modelId)?.abort();
}
emit(p) {
for (const l of this.listeners) l(p);
@@ -739,16 +1045,66 @@ var ModelDownloader = class {
// src/quant.ts
var QUANTIZATION_INFO = {
- Q2_K: { bitsPerWeight: 2.625, quality: "Low", description: "Extreme compression, noticeable quality loss", recommended: false },
- Q3_K_S: { bitsPerWeight: 3.4375, quality: "Low-Medium", description: "High compression, some quality loss", recommended: false },
- Q3_K_M: { bitsPerWeight: 3.4375, quality: "Medium", description: "Good compression with acceptable quality", recommended: false },
- Q4_0: { bitsPerWeight: 4, quality: "Medium", description: "Basic 4-bit quantization", recommended: false },
- Q4_K_S: { bitsPerWeight: 4.5, quality: "Medium-Good", description: "Good balance of size and quality", recommended: true },
- Q4_K_M: { bitsPerWeight: 4.5, quality: "Good", description: "Optimal balance - best for most devices", recommended: true },
- Q5_K_S: { bitsPerWeight: 5.5, quality: "Good-High", description: "Higher quality, larger size", recommended: false },
- Q5_K_M: { bitsPerWeight: 5.5, quality: "High", description: "Near original quality", recommended: false },
- Q6_K: { bitsPerWeight: 6.5, quality: "Very High", description: "Minimal quality loss", recommended: false },
- Q8_0: { bitsPerWeight: 8, quality: "Excellent", description: "Best quality, largest size", recommended: false }
+ Q2_K: {
+ bitsPerWeight: 2.625,
+ quality: "Low",
+ description: "Extreme compression, noticeable quality loss",
+ recommended: false
+ },
+ Q3_K_S: {
+ bitsPerWeight: 3.4375,
+ quality: "Low-Medium",
+ description: "High compression, some quality loss",
+ recommended: false
+ },
+ Q3_K_M: {
+ bitsPerWeight: 3.4375,
+ quality: "Medium",
+ description: "Good compression with acceptable quality",
+ recommended: false
+ },
+ Q4_0: {
+ bitsPerWeight: 4,
+ quality: "Medium",
+ description: "Basic 4-bit quantization",
+ recommended: false
+ },
+ Q4_K_S: {
+ bitsPerWeight: 4.5,
+ quality: "Medium-Good",
+ description: "Good balance of size and quality",
+ recommended: true
+ },
+ Q4_K_M: {
+ bitsPerWeight: 4.5,
+ quality: "Good",
+ description: "Optimal balance - best for most devices",
+ recommended: true
+ },
+ Q5_K_S: {
+ bitsPerWeight: 5.5,
+ quality: "Good-High",
+ description: "Higher quality, larger size",
+ recommended: false
+ },
+ Q5_K_M: {
+ bitsPerWeight: 5.5,
+ quality: "High",
+ description: "Near original quality",
+ recommended: false
+ },
+ Q6_K: {
+ bitsPerWeight: 6.5,
+ quality: "Very High",
+ description: "Minimal quality loss",
+ recommended: false
+ },
+ Q8_0: {
+ bitsPerWeight: 8,
+ quality: "Excellent",
+ description: "Best quality, largest size",
+ recommended: false
+ }
};
function extractQuantization(fileName) {
const upper = fileName.toUpperCase();
@@ -815,9 +1171,17 @@ var VERIFIED_QUANTIZERS = {
"lmstudio-ai": "Community GGUF"
};
var CREDIBILITY_LABELS = {
- offgrid: { label: "Off Grid", description: "Curated & converted by Off Grid \u2014 verified to run on-device", color: "#34D399" },
+ offgrid: {
+ label: "Off Grid",
+ description: "Curated & converted by Off Grid \u2014 verified to run on-device",
+ color: "#34D399"
+ },
official: { label: "Official", description: "From the original model creator", color: "#22C55E" },
- "verified-quantizer": { label: "Verified", description: "From a trusted quantization provider", color: "#A78BFA" },
+ "verified-quantizer": {
+ label: "Verified",
+ description: "From a trusted quantization provider",
+ color: "#A78BFA"
+ },
community: { label: "Community", description: "Community contributed model", color: "#64748B" }
};
function determineCredibility(author) {
@@ -871,7 +1235,9 @@ function parseParamCount(nameOrId) {
function getModelType(name, tags = []) {
const n = name.toLowerCase();
const t = tags.map((x) => x.toLowerCase());
- if (t.some((x) => x.includes("diffusion") || x.includes("text-to-image") || x.includes("image-generation") || x.includes("diffusers")) || n.includes("stable-diffusion") || n.includes("sd-") || n.includes("sdxl") || n.includes("flux"))
+ if (t.some(
+ (x) => x.includes("diffusion") || x.includes("text-to-image") || x.includes("image-generation") || x.includes("diffusers")
+ ) || n.includes("stable-diffusion") || n.includes("sd-") || n.includes("sdxl") || n.includes("flux"))
return "image-gen";
if (t.some((x) => x.includes("vision") || x.includes("multimodal") || x.includes("image-text")) || n.includes("vision") || n.includes("vlm") || n.includes("-vl") || n.includes("llava"))
return "vision";
@@ -951,25 +1317,38 @@ async function searchHuggingFace(query, opts = {}) {
if (!kind || GGUF_KINDS.has(kind)) params.set("filter", "gguf");
else if (kind) params.set("pipeline_tag", KIND_PIPELINE[kind]);
if (query) params.set("search", query);
- const res = await fetchImpl(`${HF_API}/models?${params.toString()}`, { headers: { Accept: "application/json" } });
+ const res = await fetchImpl(`${HF_API}/models?${params.toString()}`, {
+ headers: { Accept: "application/json" }
+ });
if (!res.ok) throw new Error(`Hugging Face search failed: HTTP ${res.status}`);
const data = await res.json();
let out = data.map((m) => {
const id = m.id ?? m.modelId ?? "";
const org = id.split("/")[0] ?? "";
- return { id, name: baseName(id), org, downloads: m.downloads, likes: m.likes, lastModified: m.lastModified, credibility: determineCredibility(org) };
- });
- if (kind === "text") out = out.filter((m) => {
- const t = getModelType(m.name);
- return t === "text" || t === "code";
+ return {
+ id,
+ name: baseName(id),
+ org,
+ downloads: m.downloads,
+ likes: m.likes,
+ lastModified: m.lastModified,
+ credibility: determineCredibility(org)
+ };
});
+ if (kind === "text")
+ out = out.filter((m) => {
+ const t = getModelType(m.name);
+ return t === "text" || t === "code";
+ });
else if (kind === "vision") out = out.filter((m) => getModelType(m.name) === "vision");
else if (kind === "image") out = out.filter((m) => getModelType(m.name) === "image-gen");
return out.slice(0, opts.limit ?? 30);
}
async function getModelFiles(repoId, opts = {}) {
const fetchImpl = opts.fetchImpl ?? defaultFetch;
- const res = await fetchImpl(`${HF_API}/models/${repoId}`, { headers: { Accept: "application/json" } });
+ const res = await fetchImpl(`${HF_API}/models/${repoId}`, {
+ headers: { Accept: "application/json" }
+ });
if (!res.ok) return [];
const data = await res.json();
const gguf = (data.siblings ?? []).filter((f) => f.rfilename.endsWith(".gguf"));
@@ -993,8 +1372,8 @@ async function getModelFiles(repoId, opts = {}) {
return {
fileName: baseName(f.rfilename),
quant,
- quality: (info == null ? void 0 : info.quality) ?? "Unknown",
- recommended: (info == null ? void 0 : info.recommended) ?? false,
+ quality: info?.quality ?? "Unknown",
+ recommended: info?.recommended ?? false,
sizeBytes: f.size ?? 0,
downloadUrl: url(f.rfilename),
mmproj: matchMmproj(f.rfilename)
@@ -1003,7 +1382,9 @@ async function getModelFiles(repoId, opts = {}) {
}
async function resolveHuggingFaceModel(repoId, opts = {}) {
const fetchImpl = opts.fetchImpl ?? defaultFetch;
- const res = await fetchImpl(`${HF_API}/models/${repoId}`, { headers: { Accept: "application/json" } });
+ const res = await fetchImpl(`${HF_API}/models/${repoId}`, {
+ headers: { Accept: "application/json" }
+ });
if (!res.ok) return null;
const data = await res.json();
const siblings = data.siblings ?? [];
@@ -1017,15 +1398,35 @@ async function resolveHuggingFaceModel(repoId, opts = {}) {
name: baseName(repoId),
kind: "transcription",
org,
- files: [{ name: baseName(pick.rfilename), url: url(pick.rfilename), sizeBytes: pick.size, role: "primary" }]
+ files: [
+ {
+ name: baseName(pick.rfilename),
+ url: url(pick.rfilename),
+ sizeBytes: pick.size,
+ role: "primary"
+ }
+ ]
};
}
const onnx = siblings.filter((f) => /\.onnx$/i.test(f.rfilename));
if (onnx.length > 0 && siblings.every((f) => !f.rfilename.endsWith(".gguf"))) {
const pick = onnx.find((f) => /quant/i.test(f.rfilename)) ?? onnx[0];
- const files2 = [{ name: baseName(pick.rfilename), url: url(pick.rfilename), sizeBytes: pick.size, role: "primary" }];
+ const files2 = [
+ {
+ name: baseName(pick.rfilename),
+ url: url(pick.rfilename),
+ sizeBytes: pick.size,
+ role: "primary"
+ }
+ ];
const cfg = siblings.find((f) => f.rfilename === `${pick.rfilename}.json`);
- if (cfg) files2.push({ name: baseName(cfg.rfilename), url: url(cfg.rfilename), sizeBytes: cfg.size, role: "aux" });
+ if (cfg)
+ files2.push({
+ name: baseName(cfg.rfilename),
+ url: url(cfg.rfilename),
+ sizeBytes: cfg.size,
+ role: "aux"
+ });
return { id: repoId, name: baseName(repoId), kind: "voice", org, files: files2 };
}
const gguf = siblings.filter((f) => f.rfilename.endsWith(".gguf"));
@@ -1035,10 +1436,20 @@ async function resolveHuggingFaceModel(repoId, opts = {}) {
const primary = weights.find((f) => /q4_k_m/i.test(f.rfilename)) ?? weights[0] ?? gguf[0];
if (!primary) return null;
const files = [
- { name: baseName(primary.rfilename), url: url(primary.rfilename), sizeBytes: primary.size, role: "primary" }
+ {
+ name: baseName(primary.rfilename),
+ url: url(primary.rfilename),
+ sizeBytes: primary.size,
+ role: "primary"
+ }
];
if (mmprojFiles[0]) {
- files.push({ name: baseName(mmprojFiles[0].rfilename), url: url(mmprojFiles[0].rfilename), sizeBytes: mmprojFiles[0].size, role: "mmproj" });
+ files.push({
+ name: baseName(mmprojFiles[0].rfilename),
+ url: url(mmprojFiles[0].rfilename),
+ sizeBytes: mmprojFiles[0].size,
+ role: "mmproj"
+ });
}
return {
id: repoId,
@@ -1074,24 +1485,25 @@ function openAICompatibleProvider(cfg) {
id: cfg.id,
name: cfg.name,
async listModels() {
- const res = await f(`${cfg.endpoint}/models`, { headers: { Accept: "application/json", ...authHeaders(cfg.apiKey) } });
+ const res = await f(`${cfg.endpoint}/models`, {
+ headers: { Accept: "application/json", ...authHeaders(cfg.apiKey) }
+ });
if (!res.ok) throw new Error(`listModels failed: HTTP ${res.status}`);
const data = await res.json();
return (data.data ?? []).map((m) => ({ id: m.id, name: m.id }));
},
async *chat(messages, opts) {
- var _a, _b, _c;
const res = await f(`${cfg.endpoint}/chat/completions`, {
method: "POST",
headers: { "Content-Type": "application/json", ...authHeaders(cfg.apiKey) },
body: JSON.stringify({
- model: opts == null ? void 0 : opts.model,
+ model: opts?.model,
messages,
stream: true,
- temperature: opts == null ? void 0 : opts.temperature,
- max_tokens: opts == null ? void 0 : opts.maxTokens
+ temperature: opts?.temperature,
+ max_tokens: opts?.maxTokens
}),
- signal: opts == null ? void 0 : opts.signal
+ signal: opts?.signal
});
if (!res.ok || !res.body) throw new Error(`chat failed: HTTP ${res.status}`);
for await (const line of lines(res.body)) {
@@ -1101,7 +1513,7 @@ function openAICompatibleProvider(cfg) {
if (data === "[DONE]") return;
try {
const j = JSON.parse(data);
- const c = (_c = (_b = (_a = j.choices) == null ? void 0 : _a[0]) == null ? void 0 : _b.delta) == null ? void 0 : _c.content;
+ const c = j.choices?.[0]?.delta?.content;
if (c) yield c;
} catch {
}
@@ -1121,12 +1533,11 @@ function ollamaProvider(cfg) {
return (data.models ?? []).map((m) => ({ id: m.name, name: m.name }));
},
async *chat(messages, opts) {
- var _a;
const res = await f(`${cfg.endpoint}/api/chat`, {
method: "POST",
headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ model: opts == null ? void 0 : opts.model, messages, stream: true }),
- signal: opts == null ? void 0 : opts.signal
+ body: JSON.stringify({ model: opts?.model, messages, stream: true }),
+ signal: opts?.signal
});
if (!res.ok || !res.body) throw new Error(`chat failed: HTTP ${res.status}`);
for await (const line of lines(res.body)) {
@@ -1134,7 +1545,7 @@ function ollamaProvider(cfg) {
if (!t) continue;
try {
const j = JSON.parse(t);
- if ((_a = j.message) == null ? void 0 : _a.content) yield j.message.content;
+ if (j.message?.content) yield j.message.content;
if (j.done) return;
} catch {
}
@@ -1144,9 +1555,20 @@ function ollamaProvider(cfg) {
}
function createProvider(server, fetchImpl) {
if (server.kind === "ollama") {
- return ollamaProvider({ id: server.id, name: server.name, endpoint: server.endpoint, fetchImpl });
+ return ollamaProvider({
+ id: server.id,
+ name: server.name,
+ endpoint: server.endpoint,
+ fetchImpl
+ });
}
- return openAICompatibleProvider({ id: server.id, name: server.name, endpoint: server.endpoint, apiKey: server.apiKey, fetchImpl });
+ return openAICompatibleProvider({
+ id: server.id,
+ name: server.name,
+ endpoint: server.endpoint,
+ apiKey: server.apiKey,
+ fetchImpl
+ });
}
var ProviderRegistry = class {
providers = /* @__PURE__ */ new Map();
diff --git a/packages/models/dist/index.mjs b/packages/models/dist/index.mjs
index 4706be83..43d91853 100644
--- a/packages/models/dist/index.mjs
+++ b/packages/models/dist/index.mjs
@@ -25,7 +25,14 @@ var CATALOG = [
minRamGb: 3,
quant: "Q4_K_M",
releaseDate: "2026-03-01",
- files: [{ name: "Qwen3.5-0.8B-Q4_K_M.gguf", url: resolve("unsloth/Qwen3.5-0.8B-GGUF", "Qwen3.5-0.8B-Q4_K_M.gguf"), sizeBytes: 53e7, role: "primary" }]
+ files: [
+ {
+ name: "Qwen3.5-0.8B-Q4_K_M.gguf",
+ url: resolve("unsloth/Qwen3.5-0.8B-GGUF", "Qwen3.5-0.8B-Q4_K_M.gguf"),
+ sizeBytes: 53e7,
+ role: "primary"
+ }
+ ]
},
{
id: "unsloth/Qwen3.5-2B-GGUF",
@@ -37,7 +44,14 @@ var CATALOG = [
minRamGb: 4,
quant: "Q4_K_M",
releaseDate: "2026-02-28",
- files: [{ name: "Qwen3.5-2B-Q4_K_M.gguf", url: resolve("unsloth/Qwen3.5-2B-GGUF", "Qwen3.5-2B-Q4_K_M.gguf"), sizeBytes: 128e7, role: "primary" }]
+ files: [
+ {
+ name: "Qwen3.5-2B-Q4_K_M.gguf",
+ url: resolve("unsloth/Qwen3.5-2B-GGUF", "Qwen3.5-2B-Q4_K_M.gguf"),
+ sizeBytes: 128e7,
+ role: "primary"
+ }
+ ]
},
{
id: "unsloth/Qwen3.5-4B-GGUF",
@@ -50,7 +64,14 @@ var CATALOG = [
quant: "Q4_K_M",
tags: ["Challenger"],
releaseDate: "2026-03-02",
- files: [{ name: "Qwen3.5-4B-Q4_K_M.gguf", url: resolve("unsloth/Qwen3.5-4B-GGUF", "Qwen3.5-4B-Q4_K_M.gguf"), sizeBytes: 274e7, role: "primary" }]
+ files: [
+ {
+ name: "Qwen3.5-4B-Q4_K_M.gguf",
+ url: resolve("unsloth/Qwen3.5-4B-GGUF", "Qwen3.5-4B-Q4_K_M.gguf"),
+ sizeBytes: 274e7,
+ role: "primary"
+ }
+ ]
},
{
id: "unsloth/Qwen3.5-9B-GGUF",
@@ -63,7 +84,14 @@ var CATALOG = [
quant: "Q4_K_M",
tags: ["Challenger"],
releaseDate: "2026-02-28",
- files: [{ name: "Qwen3.5-9B-Q4_K_M.gguf", url: resolve("unsloth/Qwen3.5-9B-GGUF", "Qwen3.5-9B-Q4_K_M.gguf"), sizeBytes: 568e7, role: "primary" }]
+ files: [
+ {
+ name: "Qwen3.5-9B-Q4_K_M.gguf",
+ url: resolve("unsloth/Qwen3.5-9B-GGUF", "Qwen3.5-9B-Q4_K_M.gguf"),
+ sizeBytes: 568e7,
+ role: "primary"
+ }
+ ]
},
{
id: "unsloth/Qwen3.5-27B-GGUF",
@@ -75,7 +103,14 @@ var CATALOG = [
minRamGb: 24,
quant: "Q4_K_M",
releaseDate: "2026-02-24",
- files: [{ name: "Qwen3.5-27B-Q4_K_M.gguf", url: resolve("unsloth/Qwen3.5-27B-GGUF", "Qwen3.5-27B-Q4_K_M.gguf"), sizeBytes: 1674e7, role: "primary" }]
+ files: [
+ {
+ name: "Qwen3.5-27B-Q4_K_M.gguf",
+ url: resolve("unsloth/Qwen3.5-27B-GGUF", "Qwen3.5-27B-Q4_K_M.gguf"),
+ sizeBytes: 1674e7,
+ role: "primary"
+ }
+ ]
},
{
id: "unsloth/gemma-4-E2B-it-GGUF",
@@ -87,7 +122,14 @@ var CATALOG = [
minRamGb: 5,
quant: "Q4_K_M",
releaseDate: "2026-04-01",
- files: [{ name: "gemma-4-E2B-it-Q4_K_M.gguf", url: resolve("unsloth/gemma-4-E2B-it-GGUF", "gemma-4-E2B-it-Q4_K_M.gguf"), sizeBytes: 311e7, role: "primary" }]
+ files: [
+ {
+ name: "gemma-4-E2B-it-Q4_K_M.gguf",
+ url: resolve("unsloth/gemma-4-E2B-it-GGUF", "gemma-4-E2B-it-Q4_K_M.gguf"),
+ sizeBytes: 311e7,
+ role: "primary"
+ }
+ ]
},
{
id: "unsloth/gemma-4-12b-it-GGUF",
@@ -100,7 +142,14 @@ var CATALOG = [
quant: "Q4_K_M",
tags: ["Challenger"],
releaseDate: "2026-05-29",
- files: [{ name: "gemma-4-12b-it-Q4_K_M.gguf", url: resolve("unsloth/gemma-4-12b-it-GGUF", "gemma-4-12b-it-Q4_K_M.gguf"), sizeBytes: 712e7, role: "primary" }]
+ files: [
+ {
+ name: "gemma-4-12b-it-Q4_K_M.gguf",
+ url: resolve("unsloth/gemma-4-12b-it-GGUF", "gemma-4-12b-it-Q4_K_M.gguf"),
+ sizeBytes: 712e7,
+ role: "primary"
+ }
+ ]
},
{
id: "unsloth/gemma-4-26B-A4B-it-GGUF",
@@ -113,7 +162,14 @@ var CATALOG = [
quant: "Q4_K_M",
tags: ["Challenger"],
releaseDate: "2026-04-01",
- files: [{ name: "gemma-4-26B-A4B-it-UD-Q4_K_M.gguf", url: resolve("unsloth/gemma-4-26B-A4B-it-GGUF", "gemma-4-26B-A4B-it-UD-Q4_K_M.gguf"), sizeBytes: 1695e7, role: "primary" }]
+ files: [
+ {
+ name: "gemma-4-26B-A4B-it-UD-Q4_K_M.gguf",
+ url: resolve("unsloth/gemma-4-26B-A4B-it-GGUF", "gemma-4-26B-A4B-it-UD-Q4_K_M.gguf"),
+ sizeBytes: 1695e7,
+ role: "primary"
+ }
+ ]
},
{
id: "unsloth/gemma-4-31B-it-GGUF",
@@ -125,7 +181,14 @@ var CATALOG = [
minRamGb: 24,
quant: "Q4_K_M",
releaseDate: "2026-04-01",
- files: [{ name: "gemma-4-31B-it-Q4_K_M.gguf", url: resolve("unsloth/gemma-4-31B-it-GGUF", "gemma-4-31B-it-Q4_K_M.gguf"), sizeBytes: 1832e7, role: "primary" }]
+ files: [
+ {
+ name: "gemma-4-31B-it-Q4_K_M.gguf",
+ url: resolve("unsloth/gemma-4-31B-it-GGUF", "gemma-4-31B-it-Q4_K_M.gguf"),
+ sizeBytes: 1832e7,
+ role: "primary"
+ }
+ ]
},
// --- vision (multimodal LLM) ---
{
@@ -140,8 +203,18 @@ var CATALOG = [
tags: ["Challenger"],
releaseDate: "2026-04-01",
files: [
- { name: "gemma-4-E4B-it-Q4_K_M.gguf", url: resolve("unsloth/gemma-4-E4B-it-GGUF", "gemma-4-E4B-it-Q4_K_M.gguf"), sizeBytes: 498e7, role: "primary" },
- { name: "mmproj-gemma-4-E4B-it-F16.gguf", url: resolve("unsloth/gemma-4-E4B-it-GGUF", "mmproj-F16.gguf"), sizeBytes: 99e7, role: "mmproj" }
+ {
+ name: "gemma-4-E4B-it-Q4_K_M.gguf",
+ url: resolve("unsloth/gemma-4-E4B-it-GGUF", "gemma-4-E4B-it-Q4_K_M.gguf"),
+ sizeBytes: 498e7,
+ role: "primary"
+ },
+ {
+ name: "mmproj-gemma-4-E4B-it-F16.gguf",
+ url: resolve("unsloth/gemma-4-E4B-it-GGUF", "mmproj-F16.gguf"),
+ sizeBytes: 99e7,
+ role: "mmproj"
+ }
]
},
{
@@ -155,8 +228,21 @@ var CATALOG = [
quant: "Q4_K_M",
releaseDate: "2025-04-21",
files: [
- { name: "SmolVLM2-2.2B-Instruct-Q4_K_M.gguf", url: resolve("ggml-org/SmolVLM2-2.2B-Instruct-GGUF", "SmolVLM2-2.2B-Instruct-Q4_K_M.gguf"), sizeBytes: 111e7, role: "primary" },
- { name: "mmproj-SmolVLM2-2.2B-Instruct-f16.gguf", url: resolve("ggml-org/SmolVLM2-2.2B-Instruct-GGUF", "mmproj-SmolVLM2-2.2B-Instruct-f16.gguf"), sizeBytes: 87e7, role: "mmproj" }
+ {
+ name: "SmolVLM2-2.2B-Instruct-Q4_K_M.gguf",
+ url: resolve("ggml-org/SmolVLM2-2.2B-Instruct-GGUF", "SmolVLM2-2.2B-Instruct-Q4_K_M.gguf"),
+ sizeBytes: 111e7,
+ role: "primary"
+ },
+ {
+ name: "mmproj-SmolVLM2-2.2B-Instruct-f16.gguf",
+ url: resolve(
+ "ggml-org/SmolVLM2-2.2B-Instruct-GGUF",
+ "mmproj-SmolVLM2-2.2B-Instruct-f16.gguf"
+ ),
+ sizeBytes: 87e7,
+ role: "mmproj"
+ }
]
},
{
@@ -170,8 +256,18 @@ var CATALOG = [
quant: "Q4_K_M",
releaseDate: "2025-10-30",
files: [
- { name: "Qwen3-VL-2B-Instruct-Q4_K_M.gguf", url: resolve("unsloth/Qwen3-VL-2B-Instruct-GGUF", "Qwen3-VL-2B-Instruct-Q4_K_M.gguf"), sizeBytes: 111e7, role: "primary" },
- { name: "mmproj-Qwen3-VL-2B-Instruct-F16.gguf", url: resolve("unsloth/Qwen3-VL-2B-Instruct-GGUF", "mmproj-BF16.gguf"), sizeBytes: 82e7, role: "mmproj" }
+ {
+ name: "Qwen3-VL-2B-Instruct-Q4_K_M.gguf",
+ url: resolve("unsloth/Qwen3-VL-2B-Instruct-GGUF", "Qwen3-VL-2B-Instruct-Q4_K_M.gguf"),
+ sizeBytes: 111e7,
+ role: "primary"
+ },
+ {
+ name: "mmproj-Qwen3-VL-2B-Instruct-F16.gguf",
+ url: resolve("unsloth/Qwen3-VL-2B-Instruct-GGUF", "mmproj-BF16.gguf"),
+ sizeBytes: 82e7,
+ role: "mmproj"
+ }
]
},
{
@@ -185,8 +281,18 @@ var CATALOG = [
quant: "Q4_K_M",
releaseDate: "2025-10-30",
files: [
- { name: "Qwen3-VL-8B-Instruct-Q4_K_M.gguf", url: resolve("unsloth/Qwen3-VL-8B-Instruct-GGUF", "Qwen3-VL-8B-Instruct-Q4_K_M.gguf"), sizeBytes: 503e7, role: "primary" },
- { name: "mmproj-Qwen3-VL-8B-Instruct-F16.gguf", url: resolve("unsloth/Qwen3-VL-8B-Instruct-GGUF", "mmproj-BF16.gguf"), sizeBytes: 116e7, role: "mmproj" }
+ {
+ name: "Qwen3-VL-8B-Instruct-Q4_K_M.gguf",
+ url: resolve("unsloth/Qwen3-VL-8B-Instruct-GGUF", "Qwen3-VL-8B-Instruct-Q4_K_M.gguf"),
+ sizeBytes: 503e7,
+ role: "primary"
+ },
+ {
+ name: "mmproj-Qwen3-VL-8B-Instruct-F16.gguf",
+ url: resolve("unsloth/Qwen3-VL-8B-Instruct-GGUF", "mmproj-BF16.gguf"),
+ sizeBytes: 116e7,
+ role: "mmproj"
+ }
]
},
// --- image generation — 2026 / fast few-step models only (open weight) ---
@@ -257,7 +363,12 @@ var CATALOG = [
minRamGb: 8,
imageModes: ["txt2img", "img2img"],
files: [
- { name: "sd_xl_turbo_1.0.q8_0.gguf", url: resolve("OlegSkutte/sdxl-turbo-GGUF", "sd_xl_turbo_1.0.q8_0.gguf"), role: "primary", sizeBytes: 41e8 }
+ {
+ name: "sd_xl_turbo_1.0.q8_0.gguf",
+ url: resolve("OlegSkutte/sdxl-turbo-GGUF", "sd_xl_turbo_1.0.q8_0.gguf"),
+ role: "primary",
+ sizeBytes: 41e8
+ }
]
},
// SDXL finetunes — Off Grid GGUF builds (q8). The community GGUF quants of these
@@ -274,7 +385,14 @@ var CATALOG = [
quant: "Q8_0",
releaseDate: "2024-08-05",
imageModes: ["txt2img", "img2img"],
- files: [{ name: "realvisxl-v5.0-Q8_0.gguf", url: resolve("offgrid-ai/realvisxl-v5.0-GGUF", "realvisxl-v5.0-Q8_0.gguf"), role: "primary", sizeBytes: 418e7 }]
+ files: [
+ {
+ name: "realvisxl-v5.0-Q8_0.gguf",
+ url: resolve("offgrid-ai/realvisxl-v5.0-GGUF", "realvisxl-v5.0-Q8_0.gguf"),
+ role: "primary",
+ sizeBytes: 418e7
+ }
+ ]
},
{
// Light (Q4_K) sibling — ~35% less memory, runs on a 16GB Mac. Tagged 'Light'
@@ -289,7 +407,14 @@ var CATALOG = [
quant: "Q4_K",
releaseDate: "2024-08-05",
imageModes: ["txt2img", "img2img"],
- files: [{ name: "realvisxl-v5.0-Q4_K.gguf", url: resolve("offgrid-ai/realvisxl-v5.0-GGUF", "realvisxl-v5.0-Q4_K.gguf"), role: "primary", sizeBytes: 28e8 }]
+ files: [
+ {
+ name: "realvisxl-v5.0-Q4_K.gguf",
+ url: resolve("offgrid-ai/realvisxl-v5.0-GGUF", "realvisxl-v5.0-Q4_K.gguf"),
+ role: "primary",
+ sizeBytes: 28e8
+ }
+ ]
},
{
id: "offgrid-ai/realvisxl-v5.0-lightning-GGUF",
@@ -304,7 +429,17 @@ var CATALOG = [
quant: "Q8_0",
releaseDate: "2024-09-02",
imageModes: ["txt2img", "img2img"],
- files: [{ name: "realvisxl-v5.0-lightning-Q8_0.gguf", url: resolve("offgrid-ai/realvisxl-v5.0-lightning-GGUF", "realvisxl-v5.0-lightning-Q8_0.gguf"), role: "primary", sizeBytes: 418e7 }]
+ files: [
+ {
+ name: "realvisxl-v5.0-lightning-Q8_0.gguf",
+ url: resolve(
+ "offgrid-ai/realvisxl-v5.0-lightning-GGUF",
+ "realvisxl-v5.0-lightning-Q8_0.gguf"
+ ),
+ role: "primary",
+ sizeBytes: 418e7
+ }
+ ]
},
{
// Light (Q4_K) sibling — few-step photoreal, ~35% less memory, 16GB-friendly.
@@ -318,7 +453,17 @@ var CATALOG = [
quant: "Q4_K",
releaseDate: "2024-09-02",
imageModes: ["txt2img", "img2img"],
- files: [{ name: "realvisxl-v5.0-lightning-Q4_K.gguf", url: resolve("offgrid-ai/realvisxl-v5.0-lightning-GGUF", "realvisxl-v5.0-lightning-Q4_K.gguf"), role: "primary", sizeBytes: 28e8 }]
+ files: [
+ {
+ name: "realvisxl-v5.0-lightning-Q4_K.gguf",
+ url: resolve(
+ "offgrid-ai/realvisxl-v5.0-lightning-GGUF",
+ "realvisxl-v5.0-lightning-Q4_K.gguf"
+ ),
+ role: "primary",
+ sizeBytes: 28e8
+ }
+ ]
},
{
id: "offgrid-ai/dreamshaper-xl-v2-turbo-GGUF",
@@ -333,7 +478,17 @@ var CATALOG = [
quant: "Q8_0",
releaseDate: "2024-02-07",
imageModes: ["txt2img", "img2img"],
- files: [{ name: "dreamshaper-xl-v2-turbo-Q8_0.gguf", url: resolve("offgrid-ai/dreamshaper-xl-v2-turbo-GGUF", "dreamshaper-xl-v2-turbo-Q8_0.gguf"), role: "primary", sizeBytes: 418e7 }]
+ files: [
+ {
+ name: "dreamshaper-xl-v2-turbo-Q8_0.gguf",
+ url: resolve(
+ "offgrid-ai/dreamshaper-xl-v2-turbo-GGUF",
+ "dreamshaper-xl-v2-turbo-Q8_0.gguf"
+ ),
+ role: "primary",
+ sizeBytes: 418e7
+ }
+ ]
},
{
// Lighter Q4_K quant of the same distilled turbo model — ~35% less memory
@@ -351,7 +506,17 @@ var CATALOG = [
quant: "Q4_K",
releaseDate: "2024-02-07",
imageModes: ["txt2img", "img2img"],
- files: [{ name: "dreamshaper-xl-v2-turbo-Q4_K.gguf", url: resolve("offgrid-ai/dreamshaper-xl-v2-turbo-GGUF", "dreamshaper-xl-v2-turbo-Q4_K.gguf"), role: "primary", sizeBytes: 28e8 }]
+ files: [
+ {
+ name: "dreamshaper-xl-v2-turbo-Q4_K.gguf",
+ url: resolve(
+ "offgrid-ai/dreamshaper-xl-v2-turbo-GGUF",
+ "dreamshaper-xl-v2-turbo-Q4_K.gguf"
+ ),
+ role: "primary",
+ sizeBytes: 28e8
+ }
+ ]
},
{
id: "offgrid-ai/juggernaut-xl-v9-GGUF",
@@ -364,7 +529,14 @@ var CATALOG = [
quant: "Q8_0",
releaseDate: "2024-02-18",
imageModes: ["txt2img", "img2img"],
- files: [{ name: "juggernaut-xl-v9-Q8_0.gguf", url: resolve("offgrid-ai/juggernaut-xl-v9-GGUF", "juggernaut-xl-v9-Q8_0.gguf"), role: "primary", sizeBytes: 435e7 }]
+ files: [
+ {
+ name: "juggernaut-xl-v9-Q8_0.gguf",
+ url: resolve("offgrid-ai/juggernaut-xl-v9-GGUF", "juggernaut-xl-v9-Q8_0.gguf"),
+ role: "primary",
+ sizeBytes: 435e7
+ }
+ ]
},
{
// Light (Q4_K) sibling — ~35% less memory, 16GB-friendly.
@@ -378,7 +550,14 @@ var CATALOG = [
quant: "Q4_K",
releaseDate: "2024-02-18",
imageModes: ["txt2img", "img2img"],
- files: [{ name: "juggernaut-xl-v9-Q4_K.gguf", url: resolve("offgrid-ai/juggernaut-xl-v9-GGUF", "juggernaut-xl-v9-Q4_K.gguf"), role: "primary", sizeBytes: 29e8 }]
+ files: [
+ {
+ name: "juggernaut-xl-v9-Q4_K.gguf",
+ url: resolve("offgrid-ai/juggernaut-xl-v9-GGUF", "juggernaut-xl-v9-Q4_K.gguf"),
+ role: "primary",
+ sizeBytes: 29e8
+ }
+ ]
},
{
id: "offgrid-ai/animagine-xl-4.0-GGUF",
@@ -391,7 +570,14 @@ var CATALOG = [
quant: "Q8_0",
releaseDate: "2025-01-10",
imageModes: ["txt2img", "img2img"],
- files: [{ name: "animagine-xl-4.0-Q8_0.gguf", url: resolve("offgrid-ai/animagine-xl-4.0-GGUF", "animagine-xl-4.0-Q8_0.gguf"), role: "primary", sizeBytes: 418e7 }]
+ files: [
+ {
+ name: "animagine-xl-4.0-Q8_0.gguf",
+ url: resolve("offgrid-ai/animagine-xl-4.0-GGUF", "animagine-xl-4.0-Q8_0.gguf"),
+ role: "primary",
+ sizeBytes: 418e7
+ }
+ ]
},
{
// Light (Q4_K) sibling — ~35% less memory, 16GB-friendly.
@@ -405,7 +591,14 @@ var CATALOG = [
quant: "Q4_K",
releaseDate: "2025-01-10",
imageModes: ["txt2img", "img2img"],
- files: [{ name: "animagine-xl-4.0-Q4_K.gguf", url: resolve("offgrid-ai/animagine-xl-4.0-GGUF", "animagine-xl-4.0-Q4_K.gguf"), role: "primary", sizeBytes: 28e8 }]
+ files: [
+ {
+ name: "animagine-xl-4.0-Q4_K.gguf",
+ url: resolve("offgrid-ai/animagine-xl-4.0-GGUF", "animagine-xl-4.0-Q4_K.gguf"),
+ role: "primary",
+ sizeBytes: 28e8
+ }
+ ]
},
{
id: "offgrid-ai/illustrious-xl-v2.0-GGUF",
@@ -418,7 +611,14 @@ var CATALOG = [
quant: "Q8_0",
releaseDate: "2025-04-18",
imageModes: ["txt2img", "img2img"],
- files: [{ name: "illustrious-xl-v2.0-Q8_0.gguf", url: resolve("offgrid-ai/illustrious-xl-v2.0-GGUF", "illustrious-xl-v2.0-Q8_0.gguf"), role: "primary", sizeBytes: 418e7 }]
+ files: [
+ {
+ name: "illustrious-xl-v2.0-Q8_0.gguf",
+ url: resolve("offgrid-ai/illustrious-xl-v2.0-GGUF", "illustrious-xl-v2.0-Q8_0.gguf"),
+ role: "primary",
+ sizeBytes: 418e7
+ }
+ ]
},
{
// Light (Q4_K) sibling — ~35% less memory, 16GB-friendly.
@@ -432,7 +632,14 @@ var CATALOG = [
quant: "Q4_K",
releaseDate: "2025-04-18",
imageModes: ["txt2img", "img2img"],
- files: [{ name: "illustrious-xl-v2.0-Q4_K.gguf", url: resolve("offgrid-ai/illustrious-xl-v2.0-GGUF", "illustrious-xl-v2.0-Q4_K.gguf"), role: "primary", sizeBytes: 28e8 }]
+ files: [
+ {
+ name: "illustrious-xl-v2.0-Q4_K.gguf",
+ url: resolve("offgrid-ai/illustrious-xl-v2.0-GGUF", "illustrious-xl-v2.0-Q4_K.gguf"),
+ role: "primary",
+ sizeBytes: 28e8
+ }
+ ]
},
{
id: "offgrid-ai/pony-diffusion-v6-xl-GGUF",
@@ -445,7 +652,14 @@ var CATALOG = [
quant: "Q8_0",
releaseDate: "2024-05-25",
imageModes: ["txt2img", "img2img"],
- files: [{ name: "pony-diffusion-v6-xl-Q8_0.gguf", url: resolve("offgrid-ai/pony-diffusion-v6-xl-GGUF", "pony-diffusion-v6-xl-Q8_0.gguf"), role: "primary", sizeBytes: 418e7 }]
+ files: [
+ {
+ name: "pony-diffusion-v6-xl-Q8_0.gguf",
+ url: resolve("offgrid-ai/pony-diffusion-v6-xl-GGUF", "pony-diffusion-v6-xl-Q8_0.gguf"),
+ role: "primary",
+ sizeBytes: 418e7
+ }
+ ]
},
{
// Light (Q4_K) sibling — ~35% less memory, 16GB-friendly.
@@ -459,7 +673,14 @@ var CATALOG = [
quant: "Q4_K",
releaseDate: "2024-05-25",
imageModes: ["txt2img", "img2img"],
- files: [{ name: "pony-diffusion-v6-xl-Q4_K.gguf", url: resolve("offgrid-ai/pony-diffusion-v6-xl-GGUF", "pony-diffusion-v6-xl-Q4_K.gguf"), role: "primary", sizeBytes: 28e8 }]
+ files: [
+ {
+ name: "pony-diffusion-v6-xl-Q4_K.gguf",
+ url: resolve("offgrid-ai/pony-diffusion-v6-xl-GGUF", "pony-diffusion-v6-xl-Q4_K.gguf"),
+ role: "primary",
+ sizeBytes: 28e8
+ }
+ ]
},
// --- voice (TTS); open models, ONNX runtime (no Python) ---
{
@@ -494,7 +715,10 @@ var CATALOG = [
},
{
name: "en_US-lessac-medium.onnx.json",
- url: resolve("rhasspy/piper-voices", "en/en_US/lessac/medium/en_US-lessac-medium.onnx.json"),
+ url: resolve(
+ "rhasspy/piper-voices",
+ "en/en_US/lessac/medium/en_US-lessac-medium.onnx.json"
+ ),
role: "aux"
}
]
@@ -507,7 +731,14 @@ var CATALOG = [
org: "ggerganov",
description: "Fastest, smallest \u2014 lowest accuracy",
minRamGb: 2,
- files: [{ name: "ggml-tiny.bin", url: resolve("ggerganov/whisper.cpp", "ggml-tiny.bin"), role: "primary", sizeBytes: 777e5 }]
+ files: [
+ {
+ name: "ggml-tiny.bin",
+ url: resolve("ggerganov/whisper.cpp", "ggml-tiny.bin"),
+ role: "primary",
+ sizeBytes: 777e5
+ }
+ ]
},
{
id: "ggerganov/whisper.cpp/base",
@@ -516,7 +747,14 @@ var CATALOG = [
org: "ggerganov",
description: "Offline speech-to-text (base) \u2014 good speed/quality default",
minRamGb: 3,
- files: [{ name: "ggml-base.bin", url: resolve("ggerganov/whisper.cpp", "ggml-base.bin"), role: "primary", sizeBytes: 147951e3 }]
+ files: [
+ {
+ name: "ggml-base.bin",
+ url: resolve("ggerganov/whisper.cpp", "ggml-base.bin"),
+ role: "primary",
+ sizeBytes: 147951e3
+ }
+ ]
},
{
id: "ggerganov/whisper.cpp/small",
@@ -525,7 +763,14 @@ var CATALOG = [
org: "ggerganov",
description: "Offline speech-to-text (higher accuracy)",
minRamGb: 4,
- files: [{ name: "ggml-small.bin", url: resolve("ggerganov/whisper.cpp", "ggml-small.bin"), role: "primary", sizeBytes: 487601e3 }]
+ files: [
+ {
+ name: "ggml-small.bin",
+ url: resolve("ggerganov/whisper.cpp", "ggml-small.bin"),
+ role: "primary",
+ sizeBytes: 487601e3
+ }
+ ]
},
{
id: "ggerganov/whisper.cpp/medium",
@@ -534,7 +779,14 @@ var CATALOG = [
org: "ggerganov",
description: "High accuracy; slower",
minRamGb: 6,
- files: [{ name: "ggml-medium.bin", url: resolve("ggerganov/whisper.cpp", "ggml-medium.bin"), role: "primary", sizeBytes: 1533e6 }]
+ files: [
+ {
+ name: "ggml-medium.bin",
+ url: resolve("ggerganov/whisper.cpp", "ggml-medium.bin"),
+ role: "primary",
+ sizeBytes: 1533e6
+ }
+ ]
},
{
id: "ggerganov/whisper.cpp/large-v3-turbo",
@@ -543,7 +795,14 @@ var CATALOG = [
org: "ggerganov",
description: "Near-large accuracy, much faster \u2014 recommended",
minRamGb: 6,
- files: [{ name: "ggml-large-v3-turbo.bin", url: resolve("ggerganov/whisper.cpp", "ggml-large-v3-turbo.bin"), role: "primary", sizeBytes: 1624e6 }]
+ files: [
+ {
+ name: "ggml-large-v3-turbo.bin",
+ url: resolve("ggerganov/whisper.cpp", "ggml-large-v3-turbo.bin"),
+ role: "primary",
+ sizeBytes: 1624e6
+ }
+ ]
},
{
id: "ggerganov/whisper.cpp/large-v3",
@@ -552,7 +811,14 @@ var CATALOG = [
org: "ggerganov",
description: "Highest accuracy (large); needs more RAM",
minRamGb: 8,
- files: [{ name: "ggml-large-v3.bin", url: resolve("ggerganov/whisper.cpp", "ggml-large-v3.bin"), role: "primary", sizeBytes: 3095e6 }]
+ files: [
+ {
+ name: "ggml-large-v3.bin",
+ url: resolve("ggerganov/whisper.cpp", "ggml-large-v3.bin"),
+ role: "primary",
+ sizeBytes: 3095e6
+ }
+ ]
},
// --- transcription (Parakeet, NVIDIA NeMo) — sherpa-onnx offline transducer (ONNX).
// A model is 4 files (encoder/decoder/joiner/tokens); on-disk names are slug-prefixed
@@ -568,10 +834,30 @@ var CATALOG = [
minRamGb: 4,
tags: ["Accurate", "English"],
files: [
- { name: "parakeet-v2.encoder.int8.onnx", url: resolve("csukuangfj/sherpa-onnx-nemo-parakeet-tdt-0.6b-v2-int8", "encoder.int8.onnx"), role: "primary", sizeBytes: 652e6 },
- { name: "parakeet-v2.decoder.int8.onnx", url: resolve("csukuangfj/sherpa-onnx-nemo-parakeet-tdt-0.6b-v2-int8", "decoder.int8.onnx"), role: "aux", sizeBytes: 726e4 },
- { name: "parakeet-v2.joiner.int8.onnx", url: resolve("csukuangfj/sherpa-onnx-nemo-parakeet-tdt-0.6b-v2-int8", "joiner.int8.onnx"), role: "aux", sizeBytes: 174e4 },
- { name: "parakeet-v2.tokens.txt", url: resolve("csukuangfj/sherpa-onnx-nemo-parakeet-tdt-0.6b-v2-int8", "tokens.txt"), role: "tokenizer", sizeBytes: 9600 }
+ {
+ name: "parakeet-v2.encoder.int8.onnx",
+ url: resolve("csukuangfj/sherpa-onnx-nemo-parakeet-tdt-0.6b-v2-int8", "encoder.int8.onnx"),
+ role: "primary",
+ sizeBytes: 652e6
+ },
+ {
+ name: "parakeet-v2.decoder.int8.onnx",
+ url: resolve("csukuangfj/sherpa-onnx-nemo-parakeet-tdt-0.6b-v2-int8", "decoder.int8.onnx"),
+ role: "aux",
+ sizeBytes: 726e4
+ },
+ {
+ name: "parakeet-v2.joiner.int8.onnx",
+ url: resolve("csukuangfj/sherpa-onnx-nemo-parakeet-tdt-0.6b-v2-int8", "joiner.int8.onnx"),
+ role: "aux",
+ sizeBytes: 174e4
+ },
+ {
+ name: "parakeet-v2.tokens.txt",
+ url: resolve("csukuangfj/sherpa-onnx-nemo-parakeet-tdt-0.6b-v2-int8", "tokens.txt"),
+ role: "tokenizer",
+ sizeBytes: 9600
+ }
]
},
{
@@ -585,10 +871,30 @@ var CATALOG = [
isNew: true,
tags: ["Accurate", "Multilingual"],
files: [
- { name: "parakeet-v3.encoder.int8.onnx", url: resolve("csukuangfj/sherpa-onnx-nemo-parakeet-tdt-0.6b-v3-int8", "encoder.int8.onnx"), role: "primary", sizeBytes: 652e6 },
- { name: "parakeet-v3.decoder.int8.onnx", url: resolve("csukuangfj/sherpa-onnx-nemo-parakeet-tdt-0.6b-v3-int8", "decoder.int8.onnx"), role: "aux", sizeBytes: 726e4 },
- { name: "parakeet-v3.joiner.int8.onnx", url: resolve("csukuangfj/sherpa-onnx-nemo-parakeet-tdt-0.6b-v3-int8", "joiner.int8.onnx"), role: "aux", sizeBytes: 174e4 },
- { name: "parakeet-v3.tokens.txt", url: resolve("csukuangfj/sherpa-onnx-nemo-parakeet-tdt-0.6b-v3-int8", "tokens.txt"), role: "tokenizer", sizeBytes: 9600 }
+ {
+ name: "parakeet-v3.encoder.int8.onnx",
+ url: resolve("csukuangfj/sherpa-onnx-nemo-parakeet-tdt-0.6b-v3-int8", "encoder.int8.onnx"),
+ role: "primary",
+ sizeBytes: 652e6
+ },
+ {
+ name: "parakeet-v3.decoder.int8.onnx",
+ url: resolve("csukuangfj/sherpa-onnx-nemo-parakeet-tdt-0.6b-v3-int8", "decoder.int8.onnx"),
+ role: "aux",
+ sizeBytes: 726e4
+ },
+ {
+ name: "parakeet-v3.joiner.int8.onnx",
+ url: resolve("csukuangfj/sherpa-onnx-nemo-parakeet-tdt-0.6b-v3-int8", "joiner.int8.onnx"),
+ role: "aux",
+ sizeBytes: 174e4
+ },
+ {
+ name: "parakeet-v3.tokens.txt",
+ url: resolve("csukuangfj/sherpa-onnx-nemo-parakeet-tdt-0.6b-v3-int8", "tokens.txt"),
+ role: "tokenizer",
+ sizeBytes: 9600
+ }
]
}
];
@@ -615,8 +921,7 @@ var ModelDownloader = class {
return this.store.isInstalled(modelId);
}
cancel(modelId) {
- var _a;
- (_a = this.aborts.get(modelId)) == null ? void 0 : _a.abort();
+ this.aborts.get(modelId)?.abort();
}
emit(p) {
for (const l of this.listeners) l(p);
@@ -678,16 +983,66 @@ var ModelDownloader = class {
// src/quant.ts
var QUANTIZATION_INFO = {
- Q2_K: { bitsPerWeight: 2.625, quality: "Low", description: "Extreme compression, noticeable quality loss", recommended: false },
- Q3_K_S: { bitsPerWeight: 3.4375, quality: "Low-Medium", description: "High compression, some quality loss", recommended: false },
- Q3_K_M: { bitsPerWeight: 3.4375, quality: "Medium", description: "Good compression with acceptable quality", recommended: false },
- Q4_0: { bitsPerWeight: 4, quality: "Medium", description: "Basic 4-bit quantization", recommended: false },
- Q4_K_S: { bitsPerWeight: 4.5, quality: "Medium-Good", description: "Good balance of size and quality", recommended: true },
- Q4_K_M: { bitsPerWeight: 4.5, quality: "Good", description: "Optimal balance - best for most devices", recommended: true },
- Q5_K_S: { bitsPerWeight: 5.5, quality: "Good-High", description: "Higher quality, larger size", recommended: false },
- Q5_K_M: { bitsPerWeight: 5.5, quality: "High", description: "Near original quality", recommended: false },
- Q6_K: { bitsPerWeight: 6.5, quality: "Very High", description: "Minimal quality loss", recommended: false },
- Q8_0: { bitsPerWeight: 8, quality: "Excellent", description: "Best quality, largest size", recommended: false }
+ Q2_K: {
+ bitsPerWeight: 2.625,
+ quality: "Low",
+ description: "Extreme compression, noticeable quality loss",
+ recommended: false
+ },
+ Q3_K_S: {
+ bitsPerWeight: 3.4375,
+ quality: "Low-Medium",
+ description: "High compression, some quality loss",
+ recommended: false
+ },
+ Q3_K_M: {
+ bitsPerWeight: 3.4375,
+ quality: "Medium",
+ description: "Good compression with acceptable quality",
+ recommended: false
+ },
+ Q4_0: {
+ bitsPerWeight: 4,
+ quality: "Medium",
+ description: "Basic 4-bit quantization",
+ recommended: false
+ },
+ Q4_K_S: {
+ bitsPerWeight: 4.5,
+ quality: "Medium-Good",
+ description: "Good balance of size and quality",
+ recommended: true
+ },
+ Q4_K_M: {
+ bitsPerWeight: 4.5,
+ quality: "Good",
+ description: "Optimal balance - best for most devices",
+ recommended: true
+ },
+ Q5_K_S: {
+ bitsPerWeight: 5.5,
+ quality: "Good-High",
+ description: "Higher quality, larger size",
+ recommended: false
+ },
+ Q5_K_M: {
+ bitsPerWeight: 5.5,
+ quality: "High",
+ description: "Near original quality",
+ recommended: false
+ },
+ Q6_K: {
+ bitsPerWeight: 6.5,
+ quality: "Very High",
+ description: "Minimal quality loss",
+ recommended: false
+ },
+ Q8_0: {
+ bitsPerWeight: 8,
+ quality: "Excellent",
+ description: "Best quality, largest size",
+ recommended: false
+ }
};
function extractQuantization(fileName) {
const upper = fileName.toUpperCase();
@@ -754,9 +1109,17 @@ var VERIFIED_QUANTIZERS = {
"lmstudio-ai": "Community GGUF"
};
var CREDIBILITY_LABELS = {
- offgrid: { label: "Off Grid", description: "Curated & converted by Off Grid \u2014 verified to run on-device", color: "#34D399" },
+ offgrid: {
+ label: "Off Grid",
+ description: "Curated & converted by Off Grid \u2014 verified to run on-device",
+ color: "#34D399"
+ },
official: { label: "Official", description: "From the original model creator", color: "#22C55E" },
- "verified-quantizer": { label: "Verified", description: "From a trusted quantization provider", color: "#A78BFA" },
+ "verified-quantizer": {
+ label: "Verified",
+ description: "From a trusted quantization provider",
+ color: "#A78BFA"
+ },
community: { label: "Community", description: "Community contributed model", color: "#64748B" }
};
function determineCredibility(author) {
@@ -810,7 +1173,9 @@ function parseParamCount(nameOrId) {
function getModelType(name, tags = []) {
const n = name.toLowerCase();
const t = tags.map((x) => x.toLowerCase());
- if (t.some((x) => x.includes("diffusion") || x.includes("text-to-image") || x.includes("image-generation") || x.includes("diffusers")) || n.includes("stable-diffusion") || n.includes("sd-") || n.includes("sdxl") || n.includes("flux"))
+ if (t.some(
+ (x) => x.includes("diffusion") || x.includes("text-to-image") || x.includes("image-generation") || x.includes("diffusers")
+ ) || n.includes("stable-diffusion") || n.includes("sd-") || n.includes("sdxl") || n.includes("flux"))
return "image-gen";
if (t.some((x) => x.includes("vision") || x.includes("multimodal") || x.includes("image-text")) || n.includes("vision") || n.includes("vlm") || n.includes("-vl") || n.includes("llava"))
return "vision";
@@ -890,25 +1255,38 @@ async function searchHuggingFace(query, opts = {}) {
if (!kind || GGUF_KINDS.has(kind)) params.set("filter", "gguf");
else if (kind) params.set("pipeline_tag", KIND_PIPELINE[kind]);
if (query) params.set("search", query);
- const res = await fetchImpl(`${HF_API}/models?${params.toString()}`, { headers: { Accept: "application/json" } });
+ const res = await fetchImpl(`${HF_API}/models?${params.toString()}`, {
+ headers: { Accept: "application/json" }
+ });
if (!res.ok) throw new Error(`Hugging Face search failed: HTTP ${res.status}`);
const data = await res.json();
let out = data.map((m) => {
const id = m.id ?? m.modelId ?? "";
const org = id.split("/")[0] ?? "";
- return { id, name: baseName(id), org, downloads: m.downloads, likes: m.likes, lastModified: m.lastModified, credibility: determineCredibility(org) };
- });
- if (kind === "text") out = out.filter((m) => {
- const t = getModelType(m.name);
- return t === "text" || t === "code";
+ return {
+ id,
+ name: baseName(id),
+ org,
+ downloads: m.downloads,
+ likes: m.likes,
+ lastModified: m.lastModified,
+ credibility: determineCredibility(org)
+ };
});
+ if (kind === "text")
+ out = out.filter((m) => {
+ const t = getModelType(m.name);
+ return t === "text" || t === "code";
+ });
else if (kind === "vision") out = out.filter((m) => getModelType(m.name) === "vision");
else if (kind === "image") out = out.filter((m) => getModelType(m.name) === "image-gen");
return out.slice(0, opts.limit ?? 30);
}
async function getModelFiles(repoId, opts = {}) {
const fetchImpl = opts.fetchImpl ?? defaultFetch;
- const res = await fetchImpl(`${HF_API}/models/${repoId}`, { headers: { Accept: "application/json" } });
+ const res = await fetchImpl(`${HF_API}/models/${repoId}`, {
+ headers: { Accept: "application/json" }
+ });
if (!res.ok) return [];
const data = await res.json();
const gguf = (data.siblings ?? []).filter((f) => f.rfilename.endsWith(".gguf"));
@@ -932,8 +1310,8 @@ async function getModelFiles(repoId, opts = {}) {
return {
fileName: baseName(f.rfilename),
quant,
- quality: (info == null ? void 0 : info.quality) ?? "Unknown",
- recommended: (info == null ? void 0 : info.recommended) ?? false,
+ quality: info?.quality ?? "Unknown",
+ recommended: info?.recommended ?? false,
sizeBytes: f.size ?? 0,
downloadUrl: url(f.rfilename),
mmproj: matchMmproj(f.rfilename)
@@ -942,7 +1320,9 @@ async function getModelFiles(repoId, opts = {}) {
}
async function resolveHuggingFaceModel(repoId, opts = {}) {
const fetchImpl = opts.fetchImpl ?? defaultFetch;
- const res = await fetchImpl(`${HF_API}/models/${repoId}`, { headers: { Accept: "application/json" } });
+ const res = await fetchImpl(`${HF_API}/models/${repoId}`, {
+ headers: { Accept: "application/json" }
+ });
if (!res.ok) return null;
const data = await res.json();
const siblings = data.siblings ?? [];
@@ -956,15 +1336,35 @@ async function resolveHuggingFaceModel(repoId, opts = {}) {
name: baseName(repoId),
kind: "transcription",
org,
- files: [{ name: baseName(pick.rfilename), url: url(pick.rfilename), sizeBytes: pick.size, role: "primary" }]
+ files: [
+ {
+ name: baseName(pick.rfilename),
+ url: url(pick.rfilename),
+ sizeBytes: pick.size,
+ role: "primary"
+ }
+ ]
};
}
const onnx = siblings.filter((f) => /\.onnx$/i.test(f.rfilename));
if (onnx.length > 0 && siblings.every((f) => !f.rfilename.endsWith(".gguf"))) {
const pick = onnx.find((f) => /quant/i.test(f.rfilename)) ?? onnx[0];
- const files2 = [{ name: baseName(pick.rfilename), url: url(pick.rfilename), sizeBytes: pick.size, role: "primary" }];
+ const files2 = [
+ {
+ name: baseName(pick.rfilename),
+ url: url(pick.rfilename),
+ sizeBytes: pick.size,
+ role: "primary"
+ }
+ ];
const cfg = siblings.find((f) => f.rfilename === `${pick.rfilename}.json`);
- if (cfg) files2.push({ name: baseName(cfg.rfilename), url: url(cfg.rfilename), sizeBytes: cfg.size, role: "aux" });
+ if (cfg)
+ files2.push({
+ name: baseName(cfg.rfilename),
+ url: url(cfg.rfilename),
+ sizeBytes: cfg.size,
+ role: "aux"
+ });
return { id: repoId, name: baseName(repoId), kind: "voice", org, files: files2 };
}
const gguf = siblings.filter((f) => f.rfilename.endsWith(".gguf"));
@@ -974,10 +1374,20 @@ async function resolveHuggingFaceModel(repoId, opts = {}) {
const primary = weights.find((f) => /q4_k_m/i.test(f.rfilename)) ?? weights[0] ?? gguf[0];
if (!primary) return null;
const files = [
- { name: baseName(primary.rfilename), url: url(primary.rfilename), sizeBytes: primary.size, role: "primary" }
+ {
+ name: baseName(primary.rfilename),
+ url: url(primary.rfilename),
+ sizeBytes: primary.size,
+ role: "primary"
+ }
];
if (mmprojFiles[0]) {
- files.push({ name: baseName(mmprojFiles[0].rfilename), url: url(mmprojFiles[0].rfilename), sizeBytes: mmprojFiles[0].size, role: "mmproj" });
+ files.push({
+ name: baseName(mmprojFiles[0].rfilename),
+ url: url(mmprojFiles[0].rfilename),
+ sizeBytes: mmprojFiles[0].size,
+ role: "mmproj"
+ });
}
return {
id: repoId,
@@ -1013,24 +1423,25 @@ function openAICompatibleProvider(cfg) {
id: cfg.id,
name: cfg.name,
async listModels() {
- const res = await f(`${cfg.endpoint}/models`, { headers: { Accept: "application/json", ...authHeaders(cfg.apiKey) } });
+ const res = await f(`${cfg.endpoint}/models`, {
+ headers: { Accept: "application/json", ...authHeaders(cfg.apiKey) }
+ });
if (!res.ok) throw new Error(`listModels failed: HTTP ${res.status}`);
const data = await res.json();
return (data.data ?? []).map((m) => ({ id: m.id, name: m.id }));
},
async *chat(messages, opts) {
- var _a, _b, _c;
const res = await f(`${cfg.endpoint}/chat/completions`, {
method: "POST",
headers: { "Content-Type": "application/json", ...authHeaders(cfg.apiKey) },
body: JSON.stringify({
- model: opts == null ? void 0 : opts.model,
+ model: opts?.model,
messages,
stream: true,
- temperature: opts == null ? void 0 : opts.temperature,
- max_tokens: opts == null ? void 0 : opts.maxTokens
+ temperature: opts?.temperature,
+ max_tokens: opts?.maxTokens
}),
- signal: opts == null ? void 0 : opts.signal
+ signal: opts?.signal
});
if (!res.ok || !res.body) throw new Error(`chat failed: HTTP ${res.status}`);
for await (const line of lines(res.body)) {
@@ -1040,7 +1451,7 @@ function openAICompatibleProvider(cfg) {
if (data === "[DONE]") return;
try {
const j = JSON.parse(data);
- const c = (_c = (_b = (_a = j.choices) == null ? void 0 : _a[0]) == null ? void 0 : _b.delta) == null ? void 0 : _c.content;
+ const c = j.choices?.[0]?.delta?.content;
if (c) yield c;
} catch {
}
@@ -1060,12 +1471,11 @@ function ollamaProvider(cfg) {
return (data.models ?? []).map((m) => ({ id: m.name, name: m.name }));
},
async *chat(messages, opts) {
- var _a;
const res = await f(`${cfg.endpoint}/api/chat`, {
method: "POST",
headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ model: opts == null ? void 0 : opts.model, messages, stream: true }),
- signal: opts == null ? void 0 : opts.signal
+ body: JSON.stringify({ model: opts?.model, messages, stream: true }),
+ signal: opts?.signal
});
if (!res.ok || !res.body) throw new Error(`chat failed: HTTP ${res.status}`);
for await (const line of lines(res.body)) {
@@ -1073,7 +1483,7 @@ function ollamaProvider(cfg) {
if (!t) continue;
try {
const j = JSON.parse(t);
- if ((_a = j.message) == null ? void 0 : _a.content) yield j.message.content;
+ if (j.message?.content) yield j.message.content;
if (j.done) return;
} catch {
}
@@ -1083,9 +1493,20 @@ function ollamaProvider(cfg) {
}
function createProvider(server, fetchImpl) {
if (server.kind === "ollama") {
- return ollamaProvider({ id: server.id, name: server.name, endpoint: server.endpoint, fetchImpl });
+ return ollamaProvider({
+ id: server.id,
+ name: server.name,
+ endpoint: server.endpoint,
+ fetchImpl
+ });
}
- return openAICompatibleProvider({ id: server.id, name: server.name, endpoint: server.endpoint, apiKey: server.apiKey, fetchImpl });
+ return openAICompatibleProvider({
+ id: server.id,
+ name: server.name,
+ endpoint: server.endpoint,
+ apiKey: server.apiKey,
+ fetchImpl
+ });
}
var ProviderRegistry = class {
providers = /* @__PURE__ */ new Map();
diff --git a/packages/models/dist/types-Dbo7SGxu.d.mts b/packages/models/dist/types-CQDbinZH.d.mts
similarity index 97%
rename from packages/models/dist/types-Dbo7SGxu.d.mts
rename to packages/models/dist/types-CQDbinZH.d.mts
index cce3f2c6..58379a7c 100644
--- a/packages/models/dist/types-Dbo7SGxu.d.mts
+++ b/packages/models/dist/types-CQDbinZH.d.mts
@@ -111,4 +111,4 @@ interface ModelStore {
remove(modelId: string): void;
}
-export { type DownloadBridge as D, type ImageGenMode as I, type ModelEntry as M, type ModelKind as a, type ModelRecommendationTier as b, type ModelStore as c, type DownloadProgress as d, type DownloadStatus as e, type ImageGenProvider as f, type ImageGenRequest as g, type ImageGenResult as h, type ModelFile as i, supportsMode as s, validateImageGenRequest as v };
+export { type DownloadBridge as D, type ImageGenMode as I, type ModelEntry as M, type ModelKind as a, type ModelRecommendationTier as b, type ModelStore as c, type DownloadProgress as d, type ImageGenRequest as e, type ImageGenResult as f, type ImageGenProvider as g, type ModelFile as h, type DownloadStatus as i, supportsMode as s, validateImageGenRequest as v };
diff --git a/packages/models/dist/types-Dbo7SGxu.d.ts b/packages/models/dist/types-CQDbinZH.d.ts
similarity index 97%
rename from packages/models/dist/types-Dbo7SGxu.d.ts
rename to packages/models/dist/types-CQDbinZH.d.ts
index cce3f2c6..58379a7c 100644
--- a/packages/models/dist/types-Dbo7SGxu.d.ts
+++ b/packages/models/dist/types-CQDbinZH.d.ts
@@ -111,4 +111,4 @@ interface ModelStore {
remove(modelId: string): void;
}
-export { type DownloadBridge as D, type ImageGenMode as I, type ModelEntry as M, type ModelKind as a, type ModelRecommendationTier as b, type ModelStore as c, type DownloadProgress as d, type DownloadStatus as e, type ImageGenProvider as f, type ImageGenRequest as g, type ImageGenResult as h, type ModelFile as i, supportsMode as s, validateImageGenRequest as v };
+export { type DownloadBridge as D, type ImageGenMode as I, type ModelEntry as M, type ModelKind as a, type ModelRecommendationTier as b, type ModelStore as c, type DownloadProgress as d, type ImageGenRequest as e, type ImageGenResult as f, type ImageGenProvider as g, type ModelFile as h, type DownloadStatus as i, supportsMode as s, validateImageGenRequest as v };
diff --git a/packages/models/package.json b/packages/models/package.json
index 98ee04a9..580e1e8c 100644
--- a/packages/models/package.json
+++ b/packages/models/package.json
@@ -20,7 +20,7 @@
}
},
"scripts": {
- "build": "tsup src/index.ts src/adapters/node.ts --format esm,cjs --dts",
+ "build": "tsup src/index.ts src/adapters/node.ts --format esm,cjs --dts --clean",
"dev": "tsup src/index.ts src/adapters/node.ts --format esm,cjs --dts --watch",
"typecheck": "tsc --noEmit",
"test": "npm run build && node --test test/"
diff --git a/packages/models/src/adapters/node.ts b/packages/models/src/adapters/node.ts
index c4b21227..957e6ee6 100644
--- a/packages/models/src/adapters/node.ts
+++ b/packages/models/src/adapters/node.ts
@@ -2,25 +2,25 @@
// (HTTP Range) and progress. Uses global fetch (Node 18+/Electron). RN supplies
// its own bridge (background downloader); this lives at @offgrid/models/node.
-import fs from 'fs';
-import path from 'path';
-import type { DownloadBridge } from '../types';
+import fs from 'fs'
+import path from 'path'
+import type { DownloadBridge } from '../types'
export class NodeDownloadBridge implements DownloadBridge {
constructor(private readonly modelsDir: string) {
- fs.mkdirSync(modelsDir, { recursive: true });
+ fs.mkdirSync(modelsDir, { recursive: true })
}
pathFor(fileName: string): string {
- return path.join(this.modelsDir, fileName);
+ return path.join(this.modelsDir, fileName)
}
async exists(destPath: string, expectedBytes?: number): Promise {
try {
- const st = fs.statSync(destPath);
- return expectedBytes ? st.size === expectedBytes : st.size > 0;
+ const st = fs.statSync(destPath)
+ return expectedBytes ? st.size === expectedBytes : st.size > 0
} catch {
- return false;
+ return false
}
}
@@ -29,44 +29,44 @@ export class NodeDownloadBridge implements DownloadBridge {
destPath: string,
opts: { onProgress?: (written: number, total: number) => void; signal?: AbortSignal }
): Promise {
- const tmp = `${destPath}.part`;
- let start = 0;
+ const tmp = `${destPath}.part`
+ let start = 0
try {
- start = fs.statSync(tmp).size;
+ start = fs.statSync(tmp).size
} catch {
- start = 0;
+ start = 0
}
- const headers: Record = {};
- if (start > 0) headers.Range = `bytes=${start}-`;
+ const headers: Record = {}
+ if (start > 0) headers.Range = `bytes=${start}-`
- const res = await fetch(url, { headers, signal: opts.signal });
+ const res = await fetch(url, { headers, signal: opts.signal })
if (!res.ok && res.status !== 206) {
- throw new Error(`download failed: HTTP ${res.status} for ${url}`);
+ throw new Error(`download failed: HTTP ${res.status} for ${url}`)
}
- if (!res.body) throw new Error('download failed: empty body');
+ if (!res.body) throw new Error('download failed: empty body')
- const contentLength = Number(res.headers.get('content-length') ?? 0);
- const total = contentLength + (res.status === 206 ? start : 0);
+ const contentLength = Number(res.headers.get('content-length') ?? 0)
+ const total = contentLength + (res.status === 206 ? start : 0)
- const out = fs.createWriteStream(tmp, { flags: start > 0 && res.status === 206 ? 'a' : 'w' });
- let written = res.status === 206 ? start : 0;
+ const out = fs.createWriteStream(tmp, { flags: start > 0 && res.status === 206 ? 'a' : 'w' })
+ let written = res.status === 206 ? start : 0
- const reader = res.body.getReader();
+ const reader = res.body.getReader()
try {
for (;;) {
- const { done, value } = await reader.read();
- if (done) break;
- out.write(Buffer.from(value));
- written += value.length;
- opts.onProgress?.(written, total || written);
+ const { done, value } = await reader.read()
+ if (done) break
+ out.write(Buffer.from(value))
+ written += value.length
+ opts.onProgress?.(written, total || written)
}
} finally {
- out.end();
- await new Promise((resolve) => out.on('finish', () => resolve()));
+ out.end()
+ await new Promise((resolve) => out.on('finish', () => resolve()))
}
- fs.renameSync(tmp, destPath);
- return written;
+ fs.renameSync(tmp, destPath)
+ return written
}
}
diff --git a/packages/models/src/catalog.ts b/packages/models/src/catalog.ts
index 69a5c5e9..8086904a 100644
--- a/packages/models/src/catalog.ts
+++ b/packages/models/src/catalog.ts
@@ -3,10 +3,10 @@
// come. Entries point at Hugging Face resolve URLs. This is editorial/default;
// the HF browser (hf.ts) lets users find anything else.
-import type { ModelEntry, ModelKind, ModelRecommendationTier } from './types';
+import type { ModelEntry, ModelKind, ModelRecommendationTier } from './types'
-const HF = 'https://huggingface.co';
-const resolve = (repo: string, file: string): string => `${HF}/${repo}/resolve/main/${file}`;
+const HF = 'https://huggingface.co'
+const resolve = (repo: string, file: string): string => `${HF}/${repo}/resolve/main/${file}`
// RAM tier -> max LLM size + quant (ported from mobile MODEL_RECOMMENDATIONS).
export const RECOMMENDATION_TIERS: ModelRecommendationTier[] = [
@@ -15,14 +15,14 @@ export const RECOMMENDATION_TIERS: ModelRecommendationTier[] = [
{ minRamGb: 6, maxRamGb: 8, maxParams: 4, quantization: 'Q4_K_M' },
{ minRamGb: 8, maxRamGb: 12, maxParams: 8, quantization: 'Q4_K_M' },
{ minRamGb: 12, maxRamGb: 16, maxParams: 13, quantization: 'Q4_K_M' },
- { minRamGb: 16, maxRamGb: Infinity, maxParams: 30, quantization: 'Q4_K_M' },
-];
+ { minRamGb: 16, maxRamGb: Infinity, maxParams: 30, quantization: 'Q4_K_M' }
+]
export function recommendForRam(ramGb: number): ModelRecommendationTier {
return (
RECOMMENDATION_TIERS.find((t) => ramGb >= t.minRamGb && ramGb < t.maxRamGb) ??
RECOMMENDATION_TIERS[RECOMMENDATION_TIERS.length - 1]
- );
+ )
}
export const CATALOG: ModelEntry[] = [
@@ -38,7 +38,14 @@ export const CATALOG: ModelEntry[] = [
minRamGb: 3,
quant: 'Q4_K_M',
releaseDate: '2026-03-01',
- files: [{ name: 'Qwen3.5-0.8B-Q4_K_M.gguf', url: resolve('unsloth/Qwen3.5-0.8B-GGUF', 'Qwen3.5-0.8B-Q4_K_M.gguf'), sizeBytes: 530000000, role: 'primary' }],
+ files: [
+ {
+ name: 'Qwen3.5-0.8B-Q4_K_M.gguf',
+ url: resolve('unsloth/Qwen3.5-0.8B-GGUF', 'Qwen3.5-0.8B-Q4_K_M.gguf'),
+ sizeBytes: 530000000,
+ role: 'primary'
+ }
+ ]
},
{
id: 'unsloth/Qwen3.5-2B-GGUF',
@@ -50,7 +57,14 @@ export const CATALOG: ModelEntry[] = [
minRamGb: 4,
quant: 'Q4_K_M',
releaseDate: '2026-02-28',
- files: [{ name: 'Qwen3.5-2B-Q4_K_M.gguf', url: resolve('unsloth/Qwen3.5-2B-GGUF', 'Qwen3.5-2B-Q4_K_M.gguf'), sizeBytes: 1280000000, role: 'primary' }],
+ files: [
+ {
+ name: 'Qwen3.5-2B-Q4_K_M.gguf',
+ url: resolve('unsloth/Qwen3.5-2B-GGUF', 'Qwen3.5-2B-Q4_K_M.gguf'),
+ sizeBytes: 1280000000,
+ role: 'primary'
+ }
+ ]
},
{
id: 'unsloth/Qwen3.5-4B-GGUF',
@@ -63,7 +77,14 @@ export const CATALOG: ModelEntry[] = [
quant: 'Q4_K_M',
tags: ['Challenger'],
releaseDate: '2026-03-02',
- files: [{ name: 'Qwen3.5-4B-Q4_K_M.gguf', url: resolve('unsloth/Qwen3.5-4B-GGUF', 'Qwen3.5-4B-Q4_K_M.gguf'), sizeBytes: 2740000000, role: 'primary' }],
+ files: [
+ {
+ name: 'Qwen3.5-4B-Q4_K_M.gguf',
+ url: resolve('unsloth/Qwen3.5-4B-GGUF', 'Qwen3.5-4B-Q4_K_M.gguf'),
+ sizeBytes: 2740000000,
+ role: 'primary'
+ }
+ ]
},
{
id: 'unsloth/Qwen3.5-9B-GGUF',
@@ -76,7 +97,14 @@ export const CATALOG: ModelEntry[] = [
quant: 'Q4_K_M',
tags: ['Challenger'],
releaseDate: '2026-02-28',
- files: [{ name: 'Qwen3.5-9B-Q4_K_M.gguf', url: resolve('unsloth/Qwen3.5-9B-GGUF', 'Qwen3.5-9B-Q4_K_M.gguf'), sizeBytes: 5680000000, role: 'primary' }],
+ files: [
+ {
+ name: 'Qwen3.5-9B-Q4_K_M.gguf',
+ url: resolve('unsloth/Qwen3.5-9B-GGUF', 'Qwen3.5-9B-Q4_K_M.gguf'),
+ sizeBytes: 5680000000,
+ role: 'primary'
+ }
+ ]
},
{
id: 'unsloth/Qwen3.5-27B-GGUF',
@@ -88,7 +116,14 @@ export const CATALOG: ModelEntry[] = [
minRamGb: 24,
quant: 'Q4_K_M',
releaseDate: '2026-02-24',
- files: [{ name: 'Qwen3.5-27B-Q4_K_M.gguf', url: resolve('unsloth/Qwen3.5-27B-GGUF', 'Qwen3.5-27B-Q4_K_M.gguf'), sizeBytes: 16740000000, role: 'primary' }],
+ files: [
+ {
+ name: 'Qwen3.5-27B-Q4_K_M.gguf',
+ url: resolve('unsloth/Qwen3.5-27B-GGUF', 'Qwen3.5-27B-Q4_K_M.gguf'),
+ sizeBytes: 16740000000,
+ role: 'primary'
+ }
+ ]
},
{
id: 'unsloth/gemma-4-E2B-it-GGUF',
@@ -100,7 +135,14 @@ export const CATALOG: ModelEntry[] = [
minRamGb: 5,
quant: 'Q4_K_M',
releaseDate: '2026-04-01',
- files: [{ name: 'gemma-4-E2B-it-Q4_K_M.gguf', url: resolve('unsloth/gemma-4-E2B-it-GGUF', 'gemma-4-E2B-it-Q4_K_M.gguf'), sizeBytes: 3110000000, role: 'primary' }],
+ files: [
+ {
+ name: 'gemma-4-E2B-it-Q4_K_M.gguf',
+ url: resolve('unsloth/gemma-4-E2B-it-GGUF', 'gemma-4-E2B-it-Q4_K_M.gguf'),
+ sizeBytes: 3110000000,
+ role: 'primary'
+ }
+ ]
},
{
id: 'unsloth/gemma-4-12b-it-GGUF',
@@ -113,7 +155,14 @@ export const CATALOG: ModelEntry[] = [
quant: 'Q4_K_M',
tags: ['Challenger'],
releaseDate: '2026-05-29',
- files: [{ name: 'gemma-4-12b-it-Q4_K_M.gguf', url: resolve('unsloth/gemma-4-12b-it-GGUF', 'gemma-4-12b-it-Q4_K_M.gguf'), sizeBytes: 7120000000, role: 'primary' }],
+ files: [
+ {
+ name: 'gemma-4-12b-it-Q4_K_M.gguf',
+ url: resolve('unsloth/gemma-4-12b-it-GGUF', 'gemma-4-12b-it-Q4_K_M.gguf'),
+ sizeBytes: 7120000000,
+ role: 'primary'
+ }
+ ]
},
{
id: 'unsloth/gemma-4-26B-A4B-it-GGUF',
@@ -126,7 +175,14 @@ export const CATALOG: ModelEntry[] = [
quant: 'Q4_K_M',
tags: ['Challenger'],
releaseDate: '2026-04-01',
- files: [{ name: 'gemma-4-26B-A4B-it-UD-Q4_K_M.gguf', url: resolve('unsloth/gemma-4-26B-A4B-it-GGUF', 'gemma-4-26B-A4B-it-UD-Q4_K_M.gguf'), sizeBytes: 16950000000, role: 'primary' }],
+ files: [
+ {
+ name: 'gemma-4-26B-A4B-it-UD-Q4_K_M.gguf',
+ url: resolve('unsloth/gemma-4-26B-A4B-it-GGUF', 'gemma-4-26B-A4B-it-UD-Q4_K_M.gguf'),
+ sizeBytes: 16950000000,
+ role: 'primary'
+ }
+ ]
},
{
id: 'unsloth/gemma-4-31B-it-GGUF',
@@ -138,7 +194,14 @@ export const CATALOG: ModelEntry[] = [
minRamGb: 24,
quant: 'Q4_K_M',
releaseDate: '2026-04-01',
- files: [{ name: 'gemma-4-31B-it-Q4_K_M.gguf', url: resolve('unsloth/gemma-4-31B-it-GGUF', 'gemma-4-31B-it-Q4_K_M.gguf'), sizeBytes: 18320000000, role: 'primary' }],
+ files: [
+ {
+ name: 'gemma-4-31B-it-Q4_K_M.gguf',
+ url: resolve('unsloth/gemma-4-31B-it-GGUF', 'gemma-4-31B-it-Q4_K_M.gguf'),
+ sizeBytes: 18320000000,
+ role: 'primary'
+ }
+ ]
},
// --- vision (multimodal LLM) ---
{
@@ -153,9 +216,19 @@ export const CATALOG: ModelEntry[] = [
tags: ['Challenger'],
releaseDate: '2026-04-01',
files: [
- { name: 'gemma-4-E4B-it-Q4_K_M.gguf', url: resolve('unsloth/gemma-4-E4B-it-GGUF', 'gemma-4-E4B-it-Q4_K_M.gguf'), sizeBytes: 4980000000, role: 'primary' },
- { name: 'mmproj-gemma-4-E4B-it-F16.gguf', url: resolve('unsloth/gemma-4-E4B-it-GGUF', 'mmproj-F16.gguf'), sizeBytes: 990000000, role: 'mmproj' },
- ],
+ {
+ name: 'gemma-4-E4B-it-Q4_K_M.gguf',
+ url: resolve('unsloth/gemma-4-E4B-it-GGUF', 'gemma-4-E4B-it-Q4_K_M.gguf'),
+ sizeBytes: 4980000000,
+ role: 'primary'
+ },
+ {
+ name: 'mmproj-gemma-4-E4B-it-F16.gguf',
+ url: resolve('unsloth/gemma-4-E4B-it-GGUF', 'mmproj-F16.gguf'),
+ sizeBytes: 990000000,
+ role: 'mmproj'
+ }
+ ]
},
{
id: 'ggml-org/SmolVLM2-2.2B-Instruct-GGUF',
@@ -168,9 +241,22 @@ export const CATALOG: ModelEntry[] = [
quant: 'Q4_K_M',
releaseDate: '2025-04-21',
files: [
- { name: 'SmolVLM2-2.2B-Instruct-Q4_K_M.gguf', url: resolve('ggml-org/SmolVLM2-2.2B-Instruct-GGUF', 'SmolVLM2-2.2B-Instruct-Q4_K_M.gguf'), sizeBytes: 1110000000, role: 'primary' },
- { name: 'mmproj-SmolVLM2-2.2B-Instruct-f16.gguf', url: resolve('ggml-org/SmolVLM2-2.2B-Instruct-GGUF', 'mmproj-SmolVLM2-2.2B-Instruct-f16.gguf'), sizeBytes: 870000000, role: 'mmproj' },
- ],
+ {
+ name: 'SmolVLM2-2.2B-Instruct-Q4_K_M.gguf',
+ url: resolve('ggml-org/SmolVLM2-2.2B-Instruct-GGUF', 'SmolVLM2-2.2B-Instruct-Q4_K_M.gguf'),
+ sizeBytes: 1110000000,
+ role: 'primary'
+ },
+ {
+ name: 'mmproj-SmolVLM2-2.2B-Instruct-f16.gguf',
+ url: resolve(
+ 'ggml-org/SmolVLM2-2.2B-Instruct-GGUF',
+ 'mmproj-SmolVLM2-2.2B-Instruct-f16.gguf'
+ ),
+ sizeBytes: 870000000,
+ role: 'mmproj'
+ }
+ ]
},
{
id: 'unsloth/Qwen3-VL-2B-Instruct-GGUF',
@@ -183,9 +269,19 @@ export const CATALOG: ModelEntry[] = [
quant: 'Q4_K_M',
releaseDate: '2025-10-30',
files: [
- { name: 'Qwen3-VL-2B-Instruct-Q4_K_M.gguf', url: resolve('unsloth/Qwen3-VL-2B-Instruct-GGUF', 'Qwen3-VL-2B-Instruct-Q4_K_M.gguf'), sizeBytes: 1110000000, role: 'primary' },
- { name: 'mmproj-Qwen3-VL-2B-Instruct-F16.gguf', url: resolve('unsloth/Qwen3-VL-2B-Instruct-GGUF', 'mmproj-BF16.gguf'), sizeBytes: 820000000, role: 'mmproj' },
- ],
+ {
+ name: 'Qwen3-VL-2B-Instruct-Q4_K_M.gguf',
+ url: resolve('unsloth/Qwen3-VL-2B-Instruct-GGUF', 'Qwen3-VL-2B-Instruct-Q4_K_M.gguf'),
+ sizeBytes: 1110000000,
+ role: 'primary'
+ },
+ {
+ name: 'mmproj-Qwen3-VL-2B-Instruct-F16.gguf',
+ url: resolve('unsloth/Qwen3-VL-2B-Instruct-GGUF', 'mmproj-BF16.gguf'),
+ sizeBytes: 820000000,
+ role: 'mmproj'
+ }
+ ]
},
{
id: 'unsloth/Qwen3-VL-8B-Instruct-GGUF',
@@ -198,9 +294,19 @@ export const CATALOG: ModelEntry[] = [
quant: 'Q4_K_M',
releaseDate: '2025-10-30',
files: [
- { name: 'Qwen3-VL-8B-Instruct-Q4_K_M.gguf', url: resolve('unsloth/Qwen3-VL-8B-Instruct-GGUF', 'Qwen3-VL-8B-Instruct-Q4_K_M.gguf'), sizeBytes: 5030000000, role: 'primary' },
- { name: 'mmproj-Qwen3-VL-8B-Instruct-F16.gguf', url: resolve('unsloth/Qwen3-VL-8B-Instruct-GGUF', 'mmproj-BF16.gguf'), sizeBytes: 1160000000, role: 'mmproj' },
- ],
+ {
+ name: 'Qwen3-VL-8B-Instruct-Q4_K_M.gguf',
+ url: resolve('unsloth/Qwen3-VL-8B-Instruct-GGUF', 'Qwen3-VL-8B-Instruct-Q4_K_M.gguf'),
+ sizeBytes: 5030000000,
+ role: 'primary'
+ },
+ {
+ name: 'mmproj-Qwen3-VL-8B-Instruct-F16.gguf',
+ url: resolve('unsloth/Qwen3-VL-8B-Instruct-GGUF', 'mmproj-BF16.gguf'),
+ sizeBytes: 1160000000,
+ role: 'mmproj'
+ }
+ ]
},
// --- image generation — 2026 / fast few-step models only (open weight) ---
{
@@ -213,7 +319,8 @@ export const CATALOG: ModelEntry[] = [
// models verified fast on-device (dreamshaper-turbo, realvis-lightning).
tags: ['Recommended', '2026', 'Top quality'],
org: 'Alibaba Tongyi',
- description: 'Flagship 2026 model — 1024px in ~8 steps, top quality-per-byte, strong bilingual text. Apache-2.0. (diffusion + Qwen3 encoder + VAE)',
+ description:
+ 'Flagship 2026 model — 1024px in ~8 steps, top quality-per-byte, strong bilingual text. Apache-2.0. (diffusion + Qwen3 encoder + VAE)',
minRamGb: 12,
imageModes: ['txt2img'],
files: [
@@ -221,21 +328,21 @@ export const CATALOG: ModelEntry[] = [
name: 'z_image_turbo-Q4_K.gguf',
url: resolve('leejet/Z-Image-Turbo-GGUF', 'z_image_turbo-Q4_K.gguf'),
role: 'primary',
- sizeBytes: 3860000000,
+ sizeBytes: 3860000000
},
{
name: 'Qwen3-4B-Instruct-2507-Q4_K_M.gguf',
url: resolve('unsloth/Qwen3-4B-Instruct-2507-GGUF', 'Qwen3-4B-Instruct-2507-Q4_K_M.gguf'),
role: 'aux',
- sizeBytes: 2500000000,
+ sizeBytes: 2500000000
},
{
name: 'ae.safetensors',
url: resolve('second-state/FLUX.1-schnell-GGUF', 'ae.safetensors'),
role: 'aux',
- sizeBytes: 340000000,
- },
- ],
+ sizeBytes: 340000000
+ }
+ ]
},
// NOTE: MLX/mflux image models are PARKED (2026-06-23) — the only non-gated
// on-device MLX LoRA options are too large to ship (Z-Image ~13GB 8-bit / ~33GB
@@ -248,7 +355,8 @@ export const CATALOG: ModelEntry[] = [
kind: 'image',
tags: ['Recommended', 'Fast'],
org: 'ByteDance',
- description: 'Near-SDXL quality at 1024px in 4 steps (~7× faster). ~4GB model. Best balance — recommended.',
+ description:
+ 'Near-SDXL quality at 1024px in 4 steps (~7× faster). ~4GB model. Best balance — recommended.',
minRamGb: 8,
imageModes: ['txt2img', 'img2img'],
files: [
@@ -256,9 +364,9 @@ export const CATALOG: ModelEntry[] = [
name: 'sdxl_lightning_4step.q8_0.gguf',
url: resolve('mzwing/SDXL-Lightning-GGUF', 'sdxl_lightning_4step.q8_0.gguf'),
role: 'primary',
- sizeBytes: 4099000000,
- },
- ],
+ sizeBytes: 4099000000
+ }
+ ]
},
{
id: 'OlegSkutte/sdxl-turbo-GGUF',
@@ -266,12 +374,18 @@ export const CATALOG: ModelEntry[] = [
kind: 'image',
tags: ['Fastest', 'Drafts'],
org: 'Stability AI',
- description: 'Distilled SDXL — 1-4 steps, ~10s drafts at 512px. Fastest option; lower fidelity.',
+ description:
+ 'Distilled SDXL — 1-4 steps, ~10s drafts at 512px. Fastest option; lower fidelity.',
minRamGb: 8,
imageModes: ['txt2img', 'img2img'],
files: [
- { name: 'sd_xl_turbo_1.0.q8_0.gguf', url: resolve('OlegSkutte/sdxl-turbo-GGUF', 'sd_xl_turbo_1.0.q8_0.gguf'), role: 'primary', sizeBytes: 4100000000 },
- ],
+ {
+ name: 'sd_xl_turbo_1.0.q8_0.gguf',
+ url: resolve('OlegSkutte/sdxl-turbo-GGUF', 'sd_xl_turbo_1.0.q8_0.gguf'),
+ role: 'primary',
+ sizeBytes: 4100000000
+ }
+ ]
},
// SDXL finetunes — Off Grid GGUF builds (q8). The community GGUF quants of these
// are mis-exported and won't load in sd.cpp, so we converted the official
@@ -287,7 +401,14 @@ export const CATALOG: ModelEntry[] = [
quant: 'Q8_0',
releaseDate: '2024-08-05',
imageModes: ['txt2img', 'img2img'],
- files: [{ name: 'realvisxl-v5.0-Q8_0.gguf', url: resolve('offgrid-ai/realvisxl-v5.0-GGUF', 'realvisxl-v5.0-Q8_0.gguf'), role: 'primary', sizeBytes: 4180000000 }],
+ files: [
+ {
+ name: 'realvisxl-v5.0-Q8_0.gguf',
+ url: resolve('offgrid-ai/realvisxl-v5.0-GGUF', 'realvisxl-v5.0-Q8_0.gguf'),
+ role: 'primary',
+ sizeBytes: 4180000000
+ }
+ ]
},
{
// Light (Q4_K) sibling — ~35% less memory, runs on a 16GB Mac. Tagged 'Light'
@@ -297,12 +418,20 @@ export const CATALOG: ModelEntry[] = [
kind: 'image',
tags: ['Photoreal', 'Light'],
org: 'RealVis',
- description: 'Top photorealism SDXL. Q4 quant: ~35% less memory, small quality trade-off. Runs on a 16GB Mac. Off Grid GGUF build of SG161222/RealVisXL_V5.0.',
+ description:
+ 'Top photorealism SDXL. Q4 quant: ~35% less memory, small quality trade-off. Runs on a 16GB Mac. Off Grid GGUF build of SG161222/RealVisXL_V5.0.',
minRamGb: 8,
quant: 'Q4_K',
releaseDate: '2024-08-05',
imageModes: ['txt2img', 'img2img'],
- files: [{ name: 'realvisxl-v5.0-Q4_K.gguf', url: resolve('offgrid-ai/realvisxl-v5.0-GGUF', 'realvisxl-v5.0-Q4_K.gguf'), role: 'primary', sizeBytes: 2800000000 }],
+ files: [
+ {
+ name: 'realvisxl-v5.0-Q4_K.gguf',
+ url: resolve('offgrid-ai/realvisxl-v5.0-GGUF', 'realvisxl-v5.0-Q4_K.gguf'),
+ role: 'primary',
+ sizeBytes: 2800000000
+ }
+ ]
},
{
id: 'offgrid-ai/realvisxl-v5.0-lightning-GGUF',
@@ -312,12 +441,23 @@ export const CATALOG: ModelEntry[] = [
// Light (Q4) sibling that's both few-step AND memory-safe.
tags: ['Photoreal'],
org: 'RealVis',
- description: 'Photoreal SDXL, few-step (fast) — Off Grid GGUF build of SG161222/RealVisXL_V5.0_Lightning.',
+ description:
+ 'Photoreal SDXL, few-step (fast) — Off Grid GGUF build of SG161222/RealVisXL_V5.0_Lightning.',
minRamGb: 8,
quant: 'Q8_0',
releaseDate: '2024-09-02',
imageModes: ['txt2img', 'img2img'],
- files: [{ name: 'realvisxl-v5.0-lightning-Q8_0.gguf', url: resolve('offgrid-ai/realvisxl-v5.0-lightning-GGUF', 'realvisxl-v5.0-lightning-Q8_0.gguf'), role: 'primary', sizeBytes: 4180000000 }],
+ files: [
+ {
+ name: 'realvisxl-v5.0-lightning-Q8_0.gguf',
+ url: resolve(
+ 'offgrid-ai/realvisxl-v5.0-lightning-GGUF',
+ 'realvisxl-v5.0-lightning-Q8_0.gguf'
+ ),
+ role: 'primary',
+ sizeBytes: 4180000000
+ }
+ ]
},
{
// Light (Q4_K) sibling — few-step photoreal, ~35% less memory, 16GB-friendly.
@@ -326,12 +466,23 @@ export const CATALOG: ModelEntry[] = [
kind: 'image',
tags: ['Fast', 'Photoreal', 'Light'],
org: 'RealVis',
- description: 'Photoreal SDXL, few-step (fast). Q4 quant: ~35% less memory, small quality trade-off. Runs on a 16GB Mac. Off Grid GGUF build of SG161222/RealVisXL_V5.0_Lightning.',
+ description:
+ 'Photoreal SDXL, few-step (fast). Q4 quant: ~35% less memory, small quality trade-off. Runs on a 16GB Mac. Off Grid GGUF build of SG161222/RealVisXL_V5.0_Lightning.',
minRamGb: 8,
quant: 'Q4_K',
releaseDate: '2024-09-02',
imageModes: ['txt2img', 'img2img'],
- files: [{ name: 'realvisxl-v5.0-lightning-Q4_K.gguf', url: resolve('offgrid-ai/realvisxl-v5.0-lightning-GGUF', 'realvisxl-v5.0-lightning-Q4_K.gguf'), role: 'primary', sizeBytes: 2800000000 }],
+ files: [
+ {
+ name: 'realvisxl-v5.0-lightning-Q4_K.gguf',
+ url: resolve(
+ 'offgrid-ai/realvisxl-v5.0-lightning-GGUF',
+ 'realvisxl-v5.0-lightning-Q4_K.gguf'
+ ),
+ role: 'primary',
+ sizeBytes: 2800000000
+ }
+ ]
},
{
id: 'offgrid-ai/dreamshaper-xl-v2-turbo-GGUF',
@@ -341,12 +492,23 @@ export const CATALOG: ModelEntry[] = [
// Light (Q4) sibling that's both few-step AND memory-safe.
tags: ['Versatile'],
org: 'Lykon',
- description: 'The all-rounder — photoreal, art, fantasy, 3D. Off Grid GGUF build of Lykon/dreamshaper-xl-v2-turbo. Full Q8 quant (best quality); best on 24GB+ RAM.',
+ description:
+ 'The all-rounder — photoreal, art, fantasy, 3D. Off Grid GGUF build of Lykon/dreamshaper-xl-v2-turbo. Full Q8 quant (best quality); best on 24GB+ RAM.',
minRamGb: 8,
quant: 'Q8_0',
releaseDate: '2024-02-07',
imageModes: ['txt2img', 'img2img'],
- files: [{ name: 'dreamshaper-xl-v2-turbo-Q8_0.gguf', url: resolve('offgrid-ai/dreamshaper-xl-v2-turbo-GGUF', 'dreamshaper-xl-v2-turbo-Q8_0.gguf'), role: 'primary', sizeBytes: 4180000000 }],
+ files: [
+ {
+ name: 'dreamshaper-xl-v2-turbo-Q8_0.gguf',
+ url: resolve(
+ 'offgrid-ai/dreamshaper-xl-v2-turbo-GGUF',
+ 'dreamshaper-xl-v2-turbo-Q8_0.gguf'
+ ),
+ role: 'primary',
+ sizeBytes: 4180000000
+ }
+ ]
},
{
// Lighter Q4_K quant of the same distilled turbo model — ~35% less memory
@@ -359,12 +521,23 @@ export const CATALOG: ModelEntry[] = [
kind: 'image',
tags: ['Versatile', 'Fast', 'Light'],
org: 'Lykon',
- description: 'The all-rounder — photoreal, art, fantasy, 3D. Q4 quant: ~35% less memory than the full model, small quality trade-off. Runs on a 16GB Mac. Off Grid GGUF build of Lykon/dreamshaper-xl-v2-turbo.',
+ description:
+ 'The all-rounder — photoreal, art, fantasy, 3D. Q4 quant: ~35% less memory than the full model, small quality trade-off. Runs on a 16GB Mac. Off Grid GGUF build of Lykon/dreamshaper-xl-v2-turbo.',
minRamGb: 8,
quant: 'Q4_K',
releaseDate: '2024-02-07',
imageModes: ['txt2img', 'img2img'],
- files: [{ name: 'dreamshaper-xl-v2-turbo-Q4_K.gguf', url: resolve('offgrid-ai/dreamshaper-xl-v2-turbo-GGUF', 'dreamshaper-xl-v2-turbo-Q4_K.gguf'), role: 'primary', sizeBytes: 2800000000 }],
+ files: [
+ {
+ name: 'dreamshaper-xl-v2-turbo-Q4_K.gguf',
+ url: resolve(
+ 'offgrid-ai/dreamshaper-xl-v2-turbo-GGUF',
+ 'dreamshaper-xl-v2-turbo-Q4_K.gguf'
+ ),
+ role: 'primary',
+ sizeBytes: 2800000000
+ }
+ ]
},
{
id: 'offgrid-ai/juggernaut-xl-v9-GGUF',
@@ -372,12 +545,20 @@ export const CATALOG: ModelEntry[] = [
kind: 'image',
tags: ['High quality', 'Photoreal'],
org: 'RunDiffusion',
- description: 'Versatile photoreal SDXL — cinematic, portraits, landscapes. Off Grid GGUF build of RunDiffusion/Juggernaut-XL-v9.',
+ description:
+ 'Versatile photoreal SDXL — cinematic, portraits, landscapes. Off Grid GGUF build of RunDiffusion/Juggernaut-XL-v9.',
minRamGb: 8,
quant: 'Q8_0',
releaseDate: '2024-02-18',
imageModes: ['txt2img', 'img2img'],
- files: [{ name: 'juggernaut-xl-v9-Q8_0.gguf', url: resolve('offgrid-ai/juggernaut-xl-v9-GGUF', 'juggernaut-xl-v9-Q8_0.gguf'), role: 'primary', sizeBytes: 4350000000 }],
+ files: [
+ {
+ name: 'juggernaut-xl-v9-Q8_0.gguf',
+ url: resolve('offgrid-ai/juggernaut-xl-v9-GGUF', 'juggernaut-xl-v9-Q8_0.gguf'),
+ role: 'primary',
+ sizeBytes: 4350000000
+ }
+ ]
},
{
// Light (Q4_K) sibling — ~35% less memory, 16GB-friendly.
@@ -386,12 +567,20 @@ export const CATALOG: ModelEntry[] = [
kind: 'image',
tags: ['Photoreal', 'Light'],
org: 'RunDiffusion',
- description: 'Versatile photoreal SDXL. Q4 quant: ~35% less memory, small quality trade-off. Runs on a 16GB Mac. Off Grid GGUF build of RunDiffusion/Juggernaut-XL-v9.',
+ description:
+ 'Versatile photoreal SDXL. Q4 quant: ~35% less memory, small quality trade-off. Runs on a 16GB Mac. Off Grid GGUF build of RunDiffusion/Juggernaut-XL-v9.',
minRamGb: 8,
quant: 'Q4_K',
releaseDate: '2024-02-18',
imageModes: ['txt2img', 'img2img'],
- files: [{ name: 'juggernaut-xl-v9-Q4_K.gguf', url: resolve('offgrid-ai/juggernaut-xl-v9-GGUF', 'juggernaut-xl-v9-Q4_K.gguf'), role: 'primary', sizeBytes: 2900000000 }],
+ files: [
+ {
+ name: 'juggernaut-xl-v9-Q4_K.gguf',
+ url: resolve('offgrid-ai/juggernaut-xl-v9-GGUF', 'juggernaut-xl-v9-Q4_K.gguf'),
+ role: 'primary',
+ sizeBytes: 2900000000
+ }
+ ]
},
{
id: 'offgrid-ai/animagine-xl-4.0-GGUF',
@@ -399,12 +588,20 @@ export const CATALOG: ModelEntry[] = [
kind: 'image',
tags: ['High quality', 'Anime'],
org: 'Cagliostro',
- description: 'Leading anime SDXL — strong character knowledge. Off Grid GGUF build of cagliostrolab/animagine-xl-4.0.',
+ description:
+ 'Leading anime SDXL — strong character knowledge. Off Grid GGUF build of cagliostrolab/animagine-xl-4.0.',
minRamGb: 8,
quant: 'Q8_0',
releaseDate: '2025-01-10',
imageModes: ['txt2img', 'img2img'],
- files: [{ name: 'animagine-xl-4.0-Q8_0.gguf', url: resolve('offgrid-ai/animagine-xl-4.0-GGUF', 'animagine-xl-4.0-Q8_0.gguf'), role: 'primary', sizeBytes: 4180000000 }],
+ files: [
+ {
+ name: 'animagine-xl-4.0-Q8_0.gguf',
+ url: resolve('offgrid-ai/animagine-xl-4.0-GGUF', 'animagine-xl-4.0-Q8_0.gguf'),
+ role: 'primary',
+ sizeBytes: 4180000000
+ }
+ ]
},
{
// Light (Q4_K) sibling — ~35% less memory, 16GB-friendly.
@@ -413,12 +610,20 @@ export const CATALOG: ModelEntry[] = [
kind: 'image',
tags: ['Anime', 'Light'],
org: 'Cagliostro',
- description: 'Leading anime SDXL — strong character knowledge. Q4 quant: ~35% less memory, small quality trade-off. Runs on a 16GB Mac. Off Grid GGUF build of cagliostrolab/animagine-xl-4.0.',
+ description:
+ 'Leading anime SDXL — strong character knowledge. Q4 quant: ~35% less memory, small quality trade-off. Runs on a 16GB Mac. Off Grid GGUF build of cagliostrolab/animagine-xl-4.0.',
minRamGb: 8,
quant: 'Q4_K',
releaseDate: '2025-01-10',
imageModes: ['txt2img', 'img2img'],
- files: [{ name: 'animagine-xl-4.0-Q4_K.gguf', url: resolve('offgrid-ai/animagine-xl-4.0-GGUF', 'animagine-xl-4.0-Q4_K.gguf'), role: 'primary', sizeBytes: 2800000000 }],
+ files: [
+ {
+ name: 'animagine-xl-4.0-Q4_K.gguf',
+ url: resolve('offgrid-ai/animagine-xl-4.0-GGUF', 'animagine-xl-4.0-Q4_K.gguf'),
+ role: 'primary',
+ sizeBytes: 2800000000
+ }
+ ]
},
{
id: 'offgrid-ai/illustrious-xl-v2.0-GGUF',
@@ -426,12 +631,20 @@ export const CATALOG: ModelEntry[] = [
kind: 'image',
tags: ['High quality', 'Anime'],
org: 'OnomaAI',
- description: 'Top anime / illustration SDXL base. Off Grid GGUF build of OnomaAIResearch/Illustrious-XL-v2.0.',
+ description:
+ 'Top anime / illustration SDXL base. Off Grid GGUF build of OnomaAIResearch/Illustrious-XL-v2.0.',
minRamGb: 8,
quant: 'Q8_0',
releaseDate: '2025-04-18',
imageModes: ['txt2img', 'img2img'],
- files: [{ name: 'illustrious-xl-v2.0-Q8_0.gguf', url: resolve('offgrid-ai/illustrious-xl-v2.0-GGUF', 'illustrious-xl-v2.0-Q8_0.gguf'), role: 'primary', sizeBytes: 4180000000 }],
+ files: [
+ {
+ name: 'illustrious-xl-v2.0-Q8_0.gguf',
+ url: resolve('offgrid-ai/illustrious-xl-v2.0-GGUF', 'illustrious-xl-v2.0-Q8_0.gguf'),
+ role: 'primary',
+ sizeBytes: 4180000000
+ }
+ ]
},
{
// Light (Q4_K) sibling — ~35% less memory, 16GB-friendly.
@@ -440,12 +653,20 @@ export const CATALOG: ModelEntry[] = [
kind: 'image',
tags: ['Anime', 'Light'],
org: 'OnomaAI',
- description: 'Top anime / illustration SDXL base. Q4 quant: ~35% less memory, small quality trade-off. Runs on a 16GB Mac. Off Grid GGUF build of OnomaAIResearch/Illustrious-XL-v2.0.',
+ description:
+ 'Top anime / illustration SDXL base. Q4 quant: ~35% less memory, small quality trade-off. Runs on a 16GB Mac. Off Grid GGUF build of OnomaAIResearch/Illustrious-XL-v2.0.',
minRamGb: 8,
quant: 'Q4_K',
releaseDate: '2025-04-18',
imageModes: ['txt2img', 'img2img'],
- files: [{ name: 'illustrious-xl-v2.0-Q4_K.gguf', url: resolve('offgrid-ai/illustrious-xl-v2.0-GGUF', 'illustrious-xl-v2.0-Q4_K.gguf'), role: 'primary', sizeBytes: 2800000000 }],
+ files: [
+ {
+ name: 'illustrious-xl-v2.0-Q4_K.gguf',
+ url: resolve('offgrid-ai/illustrious-xl-v2.0-GGUF', 'illustrious-xl-v2.0-Q4_K.gguf'),
+ role: 'primary',
+ sizeBytes: 2800000000
+ }
+ ]
},
{
id: 'offgrid-ai/pony-diffusion-v6-xl-GGUF',
@@ -453,12 +674,20 @@ export const CATALOG: ModelEntry[] = [
kind: 'image',
tags: ['High quality', 'Stylized'],
org: 'PurpleSmartAI',
- description: 'Dominant SDXL for stylized characters & illustration; highly promptable. Off Grid GGUF build of Pony Diffusion V6 XL.',
+ description:
+ 'Dominant SDXL for stylized characters & illustration; highly promptable. Off Grid GGUF build of Pony Diffusion V6 XL.',
minRamGb: 8,
quant: 'Q8_0',
releaseDate: '2024-05-25',
imageModes: ['txt2img', 'img2img'],
- files: [{ name: 'pony-diffusion-v6-xl-Q8_0.gguf', url: resolve('offgrid-ai/pony-diffusion-v6-xl-GGUF', 'pony-diffusion-v6-xl-Q8_0.gguf'), role: 'primary', sizeBytes: 4180000000 }],
+ files: [
+ {
+ name: 'pony-diffusion-v6-xl-Q8_0.gguf',
+ url: resolve('offgrid-ai/pony-diffusion-v6-xl-GGUF', 'pony-diffusion-v6-xl-Q8_0.gguf'),
+ role: 'primary',
+ sizeBytes: 4180000000
+ }
+ ]
},
{
// Light (Q4_K) sibling — ~35% less memory, 16GB-friendly.
@@ -467,12 +696,20 @@ export const CATALOG: ModelEntry[] = [
kind: 'image',
tags: ['Stylized', 'Light'],
org: 'PurpleSmartAI',
- description: 'Dominant SDXL for stylized characters & illustration. Q4 quant: ~35% less memory, small quality trade-off. Runs on a 16GB Mac. Off Grid GGUF build of Pony Diffusion V6 XL.',
+ description:
+ 'Dominant SDXL for stylized characters & illustration. Q4 quant: ~35% less memory, small quality trade-off. Runs on a 16GB Mac. Off Grid GGUF build of Pony Diffusion V6 XL.',
minRamGb: 8,
quant: 'Q4_K',
releaseDate: '2024-05-25',
imageModes: ['txt2img', 'img2img'],
- files: [{ name: 'pony-diffusion-v6-xl-Q4_K.gguf', url: resolve('offgrid-ai/pony-diffusion-v6-xl-GGUF', 'pony-diffusion-v6-xl-Q4_K.gguf'), role: 'primary', sizeBytes: 2800000000 }],
+ files: [
+ {
+ name: 'pony-diffusion-v6-xl-Q4_K.gguf',
+ url: resolve('offgrid-ai/pony-diffusion-v6-xl-GGUF', 'pony-diffusion-v6-xl-Q4_K.gguf'),
+ role: 'primary',
+ sizeBytes: 2800000000
+ }
+ ]
},
// --- voice (TTS); open models, ONNX runtime (no Python) ---
{
@@ -487,9 +724,9 @@ export const CATALOG: ModelEntry[] = [
name: 'kokoro-82m-v1.0.onnx',
url: resolve('onnx-community/Kokoro-82M-v1.0-ONNX', 'onnx/model_quantized.onnx'),
role: 'primary',
- sizeBytes: 92361116,
- },
- ],
+ sizeBytes: 92361116
+ }
+ ]
},
{
id: 'rhasspy/piper-voices/en_US-lessac-medium',
@@ -503,14 +740,17 @@ export const CATALOG: ModelEntry[] = [
name: 'en_US-lessac-medium.onnx',
url: resolve('rhasspy/piper-voices', 'en/en_US/lessac/medium/en_US-lessac-medium.onnx'),
role: 'primary',
- sizeBytes: 63201294,
+ sizeBytes: 63201294
},
{
name: 'en_US-lessac-medium.onnx.json',
- url: resolve('rhasspy/piper-voices', 'en/en_US/lessac/medium/en_US-lessac-medium.onnx.json'),
- role: 'aux',
- },
- ],
+ url: resolve(
+ 'rhasspy/piper-voices',
+ 'en/en_US/lessac/medium/en_US-lessac-medium.onnx.json'
+ ),
+ role: 'aux'
+ }
+ ]
},
// --- transcription (STT / whisper); all from ggerganov/whisper.cpp (ggml .bin) ---
{
@@ -520,7 +760,14 @@ export const CATALOG: ModelEntry[] = [
org: 'ggerganov',
description: 'Fastest, smallest — lowest accuracy',
minRamGb: 2,
- files: [{ name: 'ggml-tiny.bin', url: resolve('ggerganov/whisper.cpp', 'ggml-tiny.bin'), role: 'primary', sizeBytes: 77700000 }],
+ files: [
+ {
+ name: 'ggml-tiny.bin',
+ url: resolve('ggerganov/whisper.cpp', 'ggml-tiny.bin'),
+ role: 'primary',
+ sizeBytes: 77700000
+ }
+ ]
},
{
id: 'ggerganov/whisper.cpp/base',
@@ -529,7 +776,14 @@ export const CATALOG: ModelEntry[] = [
org: 'ggerganov',
description: 'Offline speech-to-text (base) — good speed/quality default',
minRamGb: 3,
- files: [{ name: 'ggml-base.bin', url: resolve('ggerganov/whisper.cpp', 'ggml-base.bin'), role: 'primary', sizeBytes: 147951000 }],
+ files: [
+ {
+ name: 'ggml-base.bin',
+ url: resolve('ggerganov/whisper.cpp', 'ggml-base.bin'),
+ role: 'primary',
+ sizeBytes: 147951000
+ }
+ ]
},
{
id: 'ggerganov/whisper.cpp/small',
@@ -538,7 +792,14 @@ export const CATALOG: ModelEntry[] = [
org: 'ggerganov',
description: 'Offline speech-to-text (higher accuracy)',
minRamGb: 4,
- files: [{ name: 'ggml-small.bin', url: resolve('ggerganov/whisper.cpp', 'ggml-small.bin'), role: 'primary', sizeBytes: 487601000 }],
+ files: [
+ {
+ name: 'ggml-small.bin',
+ url: resolve('ggerganov/whisper.cpp', 'ggml-small.bin'),
+ role: 'primary',
+ sizeBytes: 487601000
+ }
+ ]
},
{
id: 'ggerganov/whisper.cpp/medium',
@@ -547,7 +808,14 @@ export const CATALOG: ModelEntry[] = [
org: 'ggerganov',
description: 'High accuracy; slower',
minRamGb: 6,
- files: [{ name: 'ggml-medium.bin', url: resolve('ggerganov/whisper.cpp', 'ggml-medium.bin'), role: 'primary', sizeBytes: 1533000000 }],
+ files: [
+ {
+ name: 'ggml-medium.bin',
+ url: resolve('ggerganov/whisper.cpp', 'ggml-medium.bin'),
+ role: 'primary',
+ sizeBytes: 1533000000
+ }
+ ]
},
{
id: 'ggerganov/whisper.cpp/large-v3-turbo',
@@ -556,7 +824,14 @@ export const CATALOG: ModelEntry[] = [
org: 'ggerganov',
description: 'Near-large accuracy, much faster — recommended',
minRamGb: 6,
- files: [{ name: 'ggml-large-v3-turbo.bin', url: resolve('ggerganov/whisper.cpp', 'ggml-large-v3-turbo.bin'), role: 'primary', sizeBytes: 1624000000 }],
+ files: [
+ {
+ name: 'ggml-large-v3-turbo.bin',
+ url: resolve('ggerganov/whisper.cpp', 'ggml-large-v3-turbo.bin'),
+ role: 'primary',
+ sizeBytes: 1624000000
+ }
+ ]
},
{
id: 'ggerganov/whisper.cpp/large-v3',
@@ -565,7 +840,14 @@ export const CATALOG: ModelEntry[] = [
org: 'ggerganov',
description: 'Highest accuracy (large); needs more RAM',
minRamGb: 8,
- files: [{ name: 'ggml-large-v3.bin', url: resolve('ggerganov/whisper.cpp', 'ggml-large-v3.bin'), role: 'primary', sizeBytes: 3095000000 }],
+ files: [
+ {
+ name: 'ggml-large-v3.bin',
+ url: resolve('ggerganov/whisper.cpp', 'ggml-large-v3.bin'),
+ role: 'primary',
+ sizeBytes: 3095000000
+ }
+ ]
},
// --- transcription (Parakeet, NVIDIA NeMo) — sherpa-onnx offline transducer (ONNX).
// A model is 4 files (encoder/decoder/joiner/tokens); on-disk names are slug-prefixed
@@ -581,11 +863,31 @@ export const CATALOG: ModelEntry[] = [
minRamGb: 4,
tags: ['Accurate', 'English'],
files: [
- { name: 'parakeet-v2.encoder.int8.onnx', url: resolve('csukuangfj/sherpa-onnx-nemo-parakeet-tdt-0.6b-v2-int8', 'encoder.int8.onnx'), role: 'primary', sizeBytes: 652000000 },
- { name: 'parakeet-v2.decoder.int8.onnx', url: resolve('csukuangfj/sherpa-onnx-nemo-parakeet-tdt-0.6b-v2-int8', 'decoder.int8.onnx'), role: 'aux', sizeBytes: 7260000 },
- { name: 'parakeet-v2.joiner.int8.onnx', url: resolve('csukuangfj/sherpa-onnx-nemo-parakeet-tdt-0.6b-v2-int8', 'joiner.int8.onnx'), role: 'aux', sizeBytes: 1740000 },
- { name: 'parakeet-v2.tokens.txt', url: resolve('csukuangfj/sherpa-onnx-nemo-parakeet-tdt-0.6b-v2-int8', 'tokens.txt'), role: 'tokenizer', sizeBytes: 9600 },
- ],
+ {
+ name: 'parakeet-v2.encoder.int8.onnx',
+ url: resolve('csukuangfj/sherpa-onnx-nemo-parakeet-tdt-0.6b-v2-int8', 'encoder.int8.onnx'),
+ role: 'primary',
+ sizeBytes: 652000000
+ },
+ {
+ name: 'parakeet-v2.decoder.int8.onnx',
+ url: resolve('csukuangfj/sherpa-onnx-nemo-parakeet-tdt-0.6b-v2-int8', 'decoder.int8.onnx'),
+ role: 'aux',
+ sizeBytes: 7260000
+ },
+ {
+ name: 'parakeet-v2.joiner.int8.onnx',
+ url: resolve('csukuangfj/sherpa-onnx-nemo-parakeet-tdt-0.6b-v2-int8', 'joiner.int8.onnx'),
+ role: 'aux',
+ sizeBytes: 1740000
+ },
+ {
+ name: 'parakeet-v2.tokens.txt',
+ url: resolve('csukuangfj/sherpa-onnx-nemo-parakeet-tdt-0.6b-v2-int8', 'tokens.txt'),
+ role: 'tokenizer',
+ sizeBytes: 9600
+ }
+ ]
},
{
id: 'csukuangfj/sherpa-onnx-nemo-parakeet-tdt-0.6b-v3-int8',
@@ -598,16 +900,36 @@ export const CATALOG: ModelEntry[] = [
isNew: true,
tags: ['Accurate', 'Multilingual'],
files: [
- { name: 'parakeet-v3.encoder.int8.onnx', url: resolve('csukuangfj/sherpa-onnx-nemo-parakeet-tdt-0.6b-v3-int8', 'encoder.int8.onnx'), role: 'primary', sizeBytes: 652000000 },
- { name: 'parakeet-v3.decoder.int8.onnx', url: resolve('csukuangfj/sherpa-onnx-nemo-parakeet-tdt-0.6b-v3-int8', 'decoder.int8.onnx'), role: 'aux', sizeBytes: 7260000 },
- { name: 'parakeet-v3.joiner.int8.onnx', url: resolve('csukuangfj/sherpa-onnx-nemo-parakeet-tdt-0.6b-v3-int8', 'joiner.int8.onnx'), role: 'aux', sizeBytes: 1740000 },
- { name: 'parakeet-v3.tokens.txt', url: resolve('csukuangfj/sherpa-onnx-nemo-parakeet-tdt-0.6b-v3-int8', 'tokens.txt'), role: 'tokenizer', sizeBytes: 9600 },
- ],
- },
-];
+ {
+ name: 'parakeet-v3.encoder.int8.onnx',
+ url: resolve('csukuangfj/sherpa-onnx-nemo-parakeet-tdt-0.6b-v3-int8', 'encoder.int8.onnx'),
+ role: 'primary',
+ sizeBytes: 652000000
+ },
+ {
+ name: 'parakeet-v3.decoder.int8.onnx',
+ url: resolve('csukuangfj/sherpa-onnx-nemo-parakeet-tdt-0.6b-v3-int8', 'decoder.int8.onnx'),
+ role: 'aux',
+ sizeBytes: 7260000
+ },
+ {
+ name: 'parakeet-v3.joiner.int8.onnx',
+ url: resolve('csukuangfj/sherpa-onnx-nemo-parakeet-tdt-0.6b-v3-int8', 'joiner.int8.onnx'),
+ role: 'aux',
+ sizeBytes: 1740000
+ },
+ {
+ name: 'parakeet-v3.tokens.txt',
+ url: resolve('csukuangfj/sherpa-onnx-nemo-parakeet-tdt-0.6b-v3-int8', 'tokens.txt'),
+ role: 'tokenizer',
+ sizeBytes: 9600
+ }
+ ]
+ }
+]
export function modelsByKind(kind: ModelKind): ModelEntry[] {
- return CATALOG.filter((m) => m.kind === kind);
+ return CATALOG.filter((m) => m.kind === kind)
}
-export const MODEL_KINDS: ModelKind[] = ['text', 'vision', 'image', 'voice', 'transcription'];
+export const MODEL_KINDS: ModelKind[] = ['text', 'vision', 'image', 'voice', 'transcription']
diff --git a/packages/models/src/credibility.ts b/packages/models/src/credibility.ts
index 36ec894d..abfdd757 100644
--- a/packages/models/src/credibility.ts
+++ b/packages/models/src/credibility.ts
@@ -2,10 +2,10 @@
// Verified quantizer / Community, with labels + colors for badges. Shared so
// desktop and mobile rank sources identically.
-export type Credibility = 'offgrid' | 'official' | 'verified-quantizer' | 'community';
+export type Credibility = 'offgrid' | 'official' | 'verified-quantizer' | 'community'
// HF authors published/curated by Off Grid (our own converted + verified models).
-export const OFFGRID_AUTHORS = ['offgrid-ai', 'offgrid'];
+export const OFFGRID_AUTHORS = ['offgrid-ai', 'offgrid']
export const OFFICIAL_MODEL_AUTHORS: Record = {
'meta-llama': 'Meta',
@@ -28,8 +28,8 @@ export const OFFICIAL_MODEL_AUTHORS: Record = {
CohereForAI: 'Cohere',
allenai: 'Allen AI',
nvidia: 'NVIDIA',
- apple: 'Apple',
-};
+ apple: 'Apple'
+}
export const VERIFIED_QUANTIZERS: Record = {
TheBloke: 'TheBloke',
@@ -44,20 +44,31 @@ export const VERIFIED_QUANTIZERS: Record = {
ggerganov: 'Georgi Gerganov',
// Strong community quantizers (formerly badged separately) — trusted GGUFs.
'lmstudio-community': 'Community GGUF',
- 'lmstudio-ai': 'Community GGUF',
-};
+ 'lmstudio-ai': 'Community GGUF'
+}
-export const CREDIBILITY_LABELS: Record = {
- offgrid: { label: 'Off Grid', description: 'Curated & converted by Off Grid — verified to run on-device', color: '#34D399' },
+export const CREDIBILITY_LABELS: Record<
+ Credibility,
+ { label: string; description: string; color: string }
+> = {
+ offgrid: {
+ label: 'Off Grid',
+ description: 'Curated & converted by Off Grid — verified to run on-device',
+ color: '#34D399'
+ },
official: { label: 'Official', description: 'From the original model creator', color: '#22C55E' },
- 'verified-quantizer': { label: 'Verified', description: 'From a trusted quantization provider', color: '#A78BFA' },
- community: { label: 'Community', description: 'Community contributed model', color: '#64748B' },
-};
+ 'verified-quantizer': {
+ label: 'Verified',
+ description: 'From a trusted quantization provider',
+ color: '#A78BFA'
+ },
+ community: { label: 'Community', description: 'Community contributed model', color: '#64748B' }
+}
/** Classify a HF author into a credibility tier. */
export function determineCredibility(author: string): Credibility {
- if (OFFGRID_AUTHORS.includes(author)) return 'offgrid';
- if (author in OFFICIAL_MODEL_AUTHORS) return 'official';
- if (author in VERIFIED_QUANTIZERS) return 'verified-quantizer';
- return 'community';
+ if (OFFGRID_AUTHORS.includes(author)) return 'offgrid'
+ if (author in OFFICIAL_MODEL_AUTHORS) return 'official'
+ if (author in VERIFIED_QUANTIZERS) return 'verified-quantizer'
+ return 'community'
}
diff --git a/packages/models/src/download.ts b/packages/models/src/download.ts
index 9e3e5f92..f7b10fc6 100644
--- a/packages/models/src/download.ts
+++ b/packages/models/src/download.ts
@@ -2,11 +2,11 @@
// DownloadBridge, with aggregate progress, cancel, and install tracking via a
// ModelStore. Platform-agnostic; the bridge does the actual file IO.
-import type { DownloadBridge, DownloadProgress, ModelEntry, ModelStore } from './types';
+import type { DownloadBridge, DownloadProgress, ModelEntry, ModelStore } from './types'
export class ModelDownloader {
- private aborts = new Map();
- private listeners = new Set<(p: DownloadProgress) => void>();
+ private aborts = new Map()
+ private listeners = new Set<(p: DownloadProgress) => void>()
constructor(
private readonly bridge: DownloadBridge,
@@ -14,75 +14,75 @@ export class ModelDownloader {
) {}
onProgress(cb: (p: DownloadProgress) => void): () => void {
- this.listeners.add(cb);
- return () => this.listeners.delete(cb);
+ this.listeners.add(cb)
+ return () => this.listeners.delete(cb)
}
isInstalled(modelId: string): boolean {
- return this.store.isInstalled(modelId);
+ return this.store.isInstalled(modelId)
}
cancel(modelId: string): void {
- this.aborts.get(modelId)?.abort();
+ this.aborts.get(modelId)?.abort()
}
private emit(p: DownloadProgress): void {
- for (const l of this.listeners) l(p);
+ for (const l of this.listeners) l(p)
}
async download(entry: ModelEntry): Promise {
- const controller = new AbortController();
- this.aborts.set(entry.id, controller);
- const totalKnown = entry.files.reduce((n, f) => n + (f.sizeBytes ?? 0), 0);
- let basePrev = 0;
+ const controller = new AbortController()
+ this.aborts.set(entry.id, controller)
+ const totalKnown = entry.files.reduce((n, f) => n + (f.sizeBytes ?? 0), 0)
+ let basePrev = 0
try {
for (const file of entry.files) {
- const dest = this.bridge.pathFor(file.name);
+ const dest = this.bridge.pathFor(file.name)
if (await this.bridge.exists(dest, file.sizeBytes)) {
- basePrev += file.sizeBytes ?? 0;
- continue;
+ basePrev += file.sizeBytes ?? 0
+ continue
}
await this.bridge.download(file.url, dest, {
signal: controller.signal,
onProgress: (written, total) => {
- const totalBytes = totalKnown || basePrev + total;
- const bytesDownloaded = basePrev + written;
+ const totalBytes = totalKnown || basePrev + total
+ const bytesDownloaded = basePrev + written
this.emit({
modelId: entry.id,
status: 'downloading',
bytesDownloaded,
totalBytes,
progress: totalBytes ? Math.min(1, bytesDownloaded / totalBytes) : 0,
- currentFile: file.name,
- });
- },
- });
- basePrev += file.sizeBytes ?? 0;
+ currentFile: file.name
+ })
+ }
+ })
+ basePrev += file.sizeBytes ?? 0
}
- this.store.markInstalled(entry);
+ this.store.markInstalled(entry)
this.emit({
modelId: entry.id,
status: 'completed',
progress: 1,
bytesDownloaded: totalKnown,
- totalBytes: totalKnown,
- });
- return true;
+ totalBytes: totalKnown
+ })
+ return true
} catch (err) {
- const aborted = controller.signal.aborted;
+ const aborted = controller.signal.aborted
this.emit({
modelId: entry.id,
status: aborted ? 'paused' : 'failed',
progress: 0,
bytesDownloaded: 0,
totalBytes: totalKnown,
- error: aborted ? undefined : err instanceof Error ? err.message : String(err),
- });
- return false;
+ error: aborted ? undefined : err instanceof Error ? err.message : String(err)
+ })
+ return false
} finally {
- this.aborts.delete(entry.id);
+ this.aborts.delete(entry.id)
}
}
}
diff --git a/packages/models/src/filters.ts b/packages/models/src/filters.ts
index 35f51047..0810a5d6 100644
--- a/packages/models/src/filters.ts
+++ b/packages/models/src/filters.ts
@@ -3,20 +3,20 @@
// / downloads / size / recency). Pure logic over a normalized FilterableModel;
// the UI renders the option lists and feeds the FilterState.
-import type { Credibility } from './credibility';
+import type { Credibility } from './credibility'
-export type ModelTypeFilter = 'all' | 'text' | 'vision' | 'code' | 'image-gen';
-export type CredibilityFilter = 'all' | Credibility;
-export type SizeFilter = 'all' | 'tiny' | 'small' | 'medium' | 'large';
-export type SortOption = 'recommended' | 'bestfit' | 'size' | 'downloads' | 'recency';
+export type ModelTypeFilter = 'all' | 'text' | 'vision' | 'code' | 'image-gen'
+export type CredibilityFilter = 'all' | Credibility
+export type SizeFilter = 'all' | 'tiny' | 'small' | 'medium' | 'large'
+export type SortOption = 'recommended' | 'bestfit' | 'size' | 'downloads' | 'recency'
export interface FilterState {
- orgs: string[];
- type: ModelTypeFilter;
- source: CredibilityFilter;
- size: SizeFilter;
- quant: string; // 'all' or a quant label
- sort: SortOption;
+ orgs: string[]
+ type: ModelTypeFilter
+ source: CredibilityFilter
+ size: SizeFilter
+ quant: string // 'all' or a quant label
+ sort: SortOption
}
export const initialFilterState: FilterState = {
@@ -25,135 +25,161 @@ export const initialFilterState: FilterState = {
source: 'all',
size: 'all',
quant: 'all',
- sort: 'recommended',
-};
+ sort: 'recommended'
+}
/** Normalized model the filters/sorts operate on (map HF results into this). */
export interface FilterableModel {
- id: string;
- name: string;
- org: string;
- credibility?: Credibility;
- params?: number | null;
- tags?: string[];
- downloads?: number;
- likes?: number;
- lastModified?: string;
- minRamGb?: number;
- files?: { sizeBytes?: number; quant?: string }[];
+ id: string
+ name: string
+ org: string
+ credibility?: Credibility
+ params?: number | null
+ tags?: string[]
+ downloads?: number
+ likes?: number
+ lastModified?: string
+ minRamGb?: number
+ files?: { sizeBytes?: number; quant?: string }[]
}
export const SIZE_OPTIONS = [
{ key: 'tiny', label: 'Tiny (<2B)', min: 0, max: 2 },
{ key: 'small', label: 'Small (2-5B)', min: 2, max: 5 },
{ key: 'medium', label: 'Medium (5-15B)', min: 5, max: 15 },
- { key: 'large', label: 'Large (15B+)', min: 15, max: Infinity },
-] as const;
+ { key: 'large', label: 'Large (15B+)', min: 15, max: Infinity }
+] as const
export const MODEL_TYPE_OPTIONS = [
{ key: 'text', label: 'Text' },
{ key: 'vision', label: 'Vision' },
{ key: 'code', label: 'Code' },
- { key: 'image-gen', label: 'Image' },
-] as const;
+ { key: 'image-gen', label: 'Image' }
+] as const
export const CREDIBILITY_OPTIONS = [
{ key: 'offgrid', label: 'Off Grid' },
{ key: 'official', label: 'Official' },
{ key: 'verified-quantizer', label: 'Verified' },
- { key: 'community', label: 'Community' },
-] as const;
+ { key: 'community', label: 'Community' }
+] as const
export const SORT_OPTIONS = [
{ key: 'recommended', label: 'Recommended' },
{ key: 'bestfit', label: 'Best fit' },
{ key: 'downloads', label: 'Downloads' },
{ key: 'size', label: 'Size' },
- { key: 'recency', label: 'Recent' },
-] as const;
+ { key: 'recency', label: 'Recent' }
+] as const
// Matches a parameter count with a B (billions) or M (millions) unit, e.g.
// "2.2B", "500M", "256m". M is normalized to billions (500M -> 0.5).
-const PARAM_RE = /\b(\d+(?:\.\d+)?)\s?([BbMm])\b/;
+const PARAM_RE = /\b(\d+(?:\.\d+)?)\s?([BbMm])\b/
/** Parse a billions-of-parameters count from a model name/id, in billions
* ("Qwen3.5-2B" -> 2, "SmolVLM2-500M" -> 0.5). Returns null if none found. */
export function parseParamCount(nameOrId: string): number | null {
- const m = PARAM_RE.exec(nameOrId);
- if (!m) return null;
- const n = Number.parseFloat(m[1]);
- return /[Mm]/.test(m[2]) ? n / 1000 : n;
+ const m = PARAM_RE.exec(nameOrId)
+ if (!m) return null
+ const n = Number.parseFloat(m[1])
+ return /[Mm]/.test(m[2]) ? n / 1000 : n
}
/** Detect a model's type from its name + tags. */
export function getModelType(name: string, tags: string[] = []): ModelTypeFilter {
- const n = name.toLowerCase();
- const t = tags.map((x) => x.toLowerCase());
+ const n = name.toLowerCase()
+ const t = tags.map((x) => x.toLowerCase())
if (
- t.some((x) => x.includes('diffusion') || x.includes('text-to-image') || x.includes('image-generation') || x.includes('diffusers')) ||
- n.includes('stable-diffusion') || n.includes('sd-') || n.includes('sdxl') || n.includes('flux')
+ t.some(
+ (x) =>
+ x.includes('diffusion') ||
+ x.includes('text-to-image') ||
+ x.includes('image-generation') ||
+ x.includes('diffusers')
+ ) ||
+ n.includes('stable-diffusion') ||
+ n.includes('sd-') ||
+ n.includes('sdxl') ||
+ n.includes('flux')
)
- return 'image-gen';
+ return 'image-gen'
if (
t.some((x) => x.includes('vision') || x.includes('multimodal') || x.includes('image-text')) ||
- n.includes('vision') || n.includes('vlm') || n.includes('-vl') || n.includes('llava')
+ n.includes('vision') ||
+ n.includes('vlm') ||
+ n.includes('-vl') ||
+ n.includes('llava')
)
- return 'vision';
- if (t.some((x) => x.includes('code')) || n.includes('code') || n.includes('coder')) return 'code';
- return 'text';
+ return 'vision'
+ if (t.some((x) => x.includes('code')) || n.includes('code') || n.includes('coder')) return 'code'
+ return 'text'
}
/** Lower is better. Ideal model uses ~40% of RAM; penalize >75% (too slow). */
export function bestFitScore(m: FilterableModel, ramGb: number): number {
- const params = m.params ?? parseParamCount(m.name) ?? parseParamCount(m.id) ?? 0;
- const minRam = m.minRamGb ?? params * 0.75;
- const ratio = ramGb ? minRam / ramGb : 0;
- const penalty = ratio > 0.75 ? (ratio - 0.75) * 4 : 0;
- return Math.abs(ratio - 0.4) + penalty;
+ const params = m.params ?? parseParamCount(m.name) ?? parseParamCount(m.id) ?? 0
+ const minRam = m.minRamGb ?? params * 0.75
+ const ratio = ramGb ? minRam / ramGb : 0
+ const penalty = ratio > 0.75 ? (ratio - 0.75) * 4 : 0
+ return Math.abs(ratio - 0.4) + penalty
}
export function hasActiveFilters(state: FilterState): boolean {
- return state.orgs.length > 0 || state.type !== 'all' || state.source !== 'all' || state.size !== 'all' || state.quant !== 'all';
+ return (
+ state.orgs.length > 0 ||
+ state.type !== 'all' ||
+ state.source !== 'all' ||
+ state.size !== 'all' ||
+ state.quant !== 'all'
+ )
}
export function applyFilters(models: T[], state: FilterState): T[] {
return models.filter((m) => {
- if (state.source !== 'all' && m.credibility !== state.source) return false;
- if (state.type !== 'all' && getModelType(m.name, m.tags) !== state.type) return false;
- if (state.orgs.length > 0 && !state.orgs.includes(m.org)) return false;
+ if (state.source !== 'all' && m.credibility !== state.source) return false
+ if (state.type !== 'all' && getModelType(m.name, m.tags) !== state.type) return false
+ if (state.orgs.length > 0 && !state.orgs.includes(m.org)) return false
if (state.size !== 'all') {
- const p = m.params ?? parseParamCount(m.name) ?? parseParamCount(m.id);
- const opt = SIZE_OPTIONS.find((s) => s.key === state.size);
+ const p = m.params ?? parseParamCount(m.name) ?? parseParamCount(m.id)
+ const opt = SIZE_OPTIONS.find((s) => s.key === state.size)
// Exclude when out of range — and when the size is unknowable, since the
// user explicitly asked for a size band (don't leak unsized models in).
- if (opt && (p == null || p < opt.min || p >= opt.max)) return false;
+ if (opt && (p == null || p < opt.min || p >= opt.max)) return false
}
if (state.quant !== 'all' && m.files && m.files.length > 0) {
- if (!m.files.some((f) => f.quant === state.quant)) return false;
+ if (!m.files.some((f) => f.quant === state.quant)) return false
}
- return true;
- });
+ return true
+ })
}
-export function applySort(models: T[], sort: SortOption, ramGb = 0): T[] {
- if (sort === 'recommended') return models;
- const arr = [...models];
- const p = (m: FilterableModel) => m.params ?? parseParamCount(m.name) ?? 0;
+export function applySort(
+ models: T[],
+ sort: SortOption,
+ ramGb = 0
+): T[] {
+ if (sort === 'recommended') return models
+ const arr = [...models]
+ const p = (m: FilterableModel) => m.params ?? parseParamCount(m.name) ?? 0
switch (sort) {
case 'bestfit':
- return arr.sort((a, b) => bestFitScore(a, ramGb) - bestFitScore(b, ramGb));
+ return arr.sort((a, b) => bestFitScore(a, ramGb) - bestFitScore(b, ramGb))
case 'size':
- return arr.sort((a, b) => p(a) - p(b));
+ return arr.sort((a, b) => p(a) - p(b))
case 'downloads':
- return arr.sort((a, b) => (b.downloads ?? 0) - (a.downloads ?? 0));
+ return arr.sort((a, b) => (b.downloads ?? 0) - (a.downloads ?? 0))
case 'recency':
- return arr.sort((a, b) => (b.lastModified ?? '').localeCompare(a.lastModified ?? ''));
+ return arr.sort((a, b) => (b.lastModified ?? '').localeCompare(a.lastModified ?? ''))
default:
- return arr;
+ return arr
}
}
/** Apply filters then sort in one pass. */
-export function filterAndSort(models: T[], state: FilterState, ramGb = 0): T[] {
- return applySort(applyFilters(models, state), state.sort, ramGb);
+export function filterAndSort(
+ models: T[],
+ state: FilterState,
+ ramGb = 0
+): T[] {
+ return applySort(applyFilters(models, state), state.sort, ramGb)
}
diff --git a/packages/models/src/hf.ts b/packages/models/src/hf.ts
index 36a642d3..8be73655 100644
--- a/packages/models/src/hf.ts
+++ b/packages/models/src/hf.ts
@@ -2,10 +2,10 @@
// downloadable ModelEntry (pick a Q4_K_M weight + matching mmproj for vision).
// fetch is injectable so this is unit-testable without the network.
-import type { ModelEntry, ModelFile, ModelKind } from './types';
-import { QUANTIZATION_INFO, extractQuantization, isMMProjFile } from './quant';
-import { determineCredibility, type Credibility } from './credibility';
-import { getModelType } from './filters';
+import type { ModelEntry, ModelFile, ModelKind } from './types'
+import { QUANTIZATION_INFO, extractQuantization, isMMProjFile } from './quant'
+import { determineCredibility, type Credibility } from './credibility'
+import { getModelType } from './filters'
// HF pipeline_tag per Off Grid modality — so a tab's search only returns models
// of that kind (text-gen, VLM, diffusion, ASR, TTS) instead of everything.
@@ -14,56 +14,59 @@ const KIND_PIPELINE: Record = {
vision: 'image-text-to-text',
image: 'text-to-image',
voice: 'text-to-speech',
- transcription: 'automatic-speech-recognition',
-};
+ transcription: 'automatic-speech-recognition'
+}
// Runtimes that consume GGUF (llama.cpp text/vision, sd.cpp image). Whisper (STT)
// is ggml .bin and Kokoro (TTS) is onnx, so we don't constrain those to gguf.
-const GGUF_KINDS: ReadonlySet = new Set(['text', 'vision', 'image']);
+const GGUF_KINDS: ReadonlySet = new Set(['text', 'vision', 'image'])
-const HF = 'https://huggingface.co';
-const HF_API = 'https://huggingface.co/api';
+const HF = 'https://huggingface.co'
+const HF_API = 'https://huggingface.co/api'
-type FetchLike = (url: string, init?: { headers?: Record }) => Promise<{
- ok: boolean;
- status: number;
- json: () => Promise;
-}>;
+type FetchLike = (
+ url: string,
+ init?: { headers?: Record }
+) => Promise<{
+ ok: boolean
+ status: number
+ json: () => Promise
+}>
-const defaultFetch: FetchLike = (url, init) => fetch(url, init) as unknown as ReturnType;
+const defaultFetch: FetchLike = (url, init) => fetch(url, init) as unknown as ReturnType
export interface HFSearchResult {
- id: string;
- name: string;
- org: string;
- downloads?: number;
- likes?: number;
- lastModified?: string;
- credibility: Credibility;
+ id: string
+ name: string
+ org: string
+ downloads?: number
+ likes?: number
+ lastModified?: string
+ credibility: Credibility
}
/** A selectable quantization variant within a HF repo (for the file picker). */
export interface ModelFileVariant {
- fileName: string;
- quant: string;
- quality: string;
- recommended: boolean;
- sizeBytes: number;
- downloadUrl: string;
+ fileName: string
+ quant: string
+ quality: string
+ recommended: boolean
+ sizeBytes: number
+ downloadUrl: string
/** Matched vision projector for this weight, when the repo is multimodal. */
- mmproj?: { fileName: string; url: string; sizeBytes?: number };
+ mmproj?: { fileName: string; url: string; sizeBytes?: number }
}
interface HFModel {
- id?: string;
- modelId?: string;
- downloads?: number;
- likes?: number;
- lastModified?: string;
- siblings?: { rfilename: string; size?: number }[];
+ id?: string
+ modelId?: string
+ downloads?: number
+ likes?: number
+ lastModified?: string
+ siblings?: { rfilename: string; size?: number }[]
}
-const isMmproj = (name: string): boolean => /mmproj|clip/i.test(name);
-const baseName = (p: string): string => p.split('/').pop() ?? p;
+const isMmproj = (name: string): boolean => /mmproj|clip/i.test(name)
+const baseName = (p: string): string => p.split('/').pop() ?? p
/** Search the HF hub for models, scoped to a modality (kind) when given so each
* tab only surfaces models it can actually use. */
@@ -71,35 +74,49 @@ export async function searchHuggingFace(
query: string,
opts: { limit?: number; sort?: string; kind?: ModelKind; fetchImpl?: FetchLike } = {}
): Promise {
- const fetchImpl = opts.fetchImpl ?? defaultFetch;
- const kind = opts.kind;
+ const fetchImpl = opts.fetchImpl ?? defaultFetch
+ const kind = opts.kind
const params = new URLSearchParams({
sort: opts.sort ?? 'downloads',
direction: '-1',
// Over-fetch so post-filtering by detected type still leaves a full page.
- limit: String((opts.limit ?? 30) * 2),
- });
+ limit: String((opts.limit ?? 30) * 2)
+ })
// GGUF kinds (text/vision/image): constrain to gguf and scope by the NAME
// heuristic below — HF's pipeline_tag is unreliable on gguf repos (it tags
// plain text models as image-text-to-text). Non-gguf kinds (ASR/TTS) have no
// name signal, so lean on HF's pipeline_tag, which is accurate for them.
- if (!kind || GGUF_KINDS.has(kind)) params.set('filter', 'gguf');
- else if (kind) params.set('pipeline_tag', KIND_PIPELINE[kind]);
- if (query) params.set('search', query);
- const res = await fetchImpl(`${HF_API}/models?${params.toString()}`, { headers: { Accept: 'application/json' } });
- if (!res.ok) throw new Error(`Hugging Face search failed: HTTP ${res.status}`);
- const data = (await res.json()) as HFModel[];
+ if (!kind || GGUF_KINDS.has(kind)) params.set('filter', 'gguf')
+ else if (kind) params.set('pipeline_tag', KIND_PIPELINE[kind])
+ if (query) params.set('search', query)
+ const res = await fetchImpl(`${HF_API}/models?${params.toString()}`, {
+ headers: { Accept: 'application/json' }
+ })
+ if (!res.ok) throw new Error(`Hugging Face search failed: HTTP ${res.status}`)
+ const data = (await res.json()) as HFModel[]
let out = data.map((m) => {
- const id = m.id ?? m.modelId ?? '';
- const org = id.split('/')[0] ?? '';
- return { id, name: baseName(id), org, downloads: m.downloads, likes: m.likes, lastModified: m.lastModified, credibility: determineCredibility(org) };
- });
+ const id = m.id ?? m.modelId ?? ''
+ const org = id.split('/')[0] ?? ''
+ return {
+ id,
+ name: baseName(id),
+ org,
+ downloads: m.downloads,
+ likes: m.likes,
+ lastModified: m.lastModified,
+ credibility: determineCredibility(org)
+ }
+ })
// HF's pipeline tags are inconsistent for gguf repos, so refine text/vision/image
// by the name heuristic too (e.g. keep VLMs off the Text tab and vice versa).
- if (kind === 'text') out = out.filter((m) => { const t = getModelType(m.name); return t === 'text' || t === 'code'; });
- else if (kind === 'vision') out = out.filter((m) => getModelType(m.name) === 'vision');
- else if (kind === 'image') out = out.filter((m) => getModelType(m.name) === 'image-gen');
- return out.slice(0, opts.limit ?? 30);
+ if (kind === 'text')
+ out = out.filter((m) => {
+ const t = getModelType(m.name)
+ return t === 'text' || t === 'code'
+ })
+ else if (kind === 'vision') out = out.filter((m) => getModelType(m.name) === 'vision')
+ else if (kind === 'image') out = out.filter((m) => getModelType(m.name) === 'image-gen')
+ return out.slice(0, opts.limit ?? 30)
}
/** List a repo's GGUF quantization variants (with matched mmproj), for a file
@@ -108,31 +125,36 @@ export async function getModelFiles(
repoId: string,
opts: { fetchImpl?: FetchLike } = {}
): Promise {
- const fetchImpl = opts.fetchImpl ?? defaultFetch;
- const res = await fetchImpl(`${HF_API}/models/${repoId}`, { headers: { Accept: 'application/json' } });
- if (!res.ok) return [];
- const data = (await res.json()) as HFModel;
- const gguf = (data.siblings ?? []).filter((f) => f.rfilename.endsWith('.gguf'));
- const mmprojFiles = gguf.filter((f) => isMMProjFile(f.rfilename));
- const weights = gguf.filter((f) => !isMMProjFile(f.rfilename));
- const url = (rf: string): string => `${HF}/${repoId}/resolve/main/${rf}`;
+ const fetchImpl = opts.fetchImpl ?? defaultFetch
+ const res = await fetchImpl(`${HF_API}/models/${repoId}`, {
+ headers: { Accept: 'application/json' }
+ })
+ if (!res.ok) return []
+ const data = (await res.json()) as HFModel
+ const gguf = (data.siblings ?? []).filter((f) => f.rfilename.endsWith('.gguf'))
+ const mmprojFiles = gguf.filter((f) => isMMProjFile(f.rfilename))
+ const weights = gguf.filter((f) => !isMMProjFile(f.rfilename))
+ const url = (rf: string): string => `${HF}/${repoId}/resolve/main/${rf}`
const matchMmproj = (weightName: string): ModelFileVariant['mmproj'] | undefined => {
- if (mmprojFiles.length === 0) return undefined;
- const wq = extractQuantization(weightName);
- const exact = wq !== 'Unknown' ? mmprojFiles.find((f) => extractQuantization(f.rfilename) === wq) : undefined;
+ if (mmprojFiles.length === 0) return undefined
+ const wq = extractQuantization(weightName)
+ const exact =
+ wq !== 'Unknown'
+ ? mmprojFiles.find((f) => extractQuantization(f.rfilename) === wq)
+ : undefined
const f16 = mmprojFiles.find((f) => {
- const l = f.rfilename.toLowerCase();
- return (l.includes('f16') || l.includes('fp16')) && !l.includes('bf16');
- });
- const pick = exact ?? f16 ?? mmprojFiles[0];
- return { fileName: baseName(pick.rfilename), url: url(pick.rfilename), sizeBytes: pick.size };
- };
+ const l = f.rfilename.toLowerCase()
+ return (l.includes('f16') || l.includes('fp16')) && !l.includes('bf16')
+ })
+ const pick = exact ?? f16 ?? mmprojFiles[0]
+ return { fileName: baseName(pick.rfilename), url: url(pick.rfilename), sizeBytes: pick.size }
+ }
return weights
.map((f) => {
- const quant = extractQuantization(f.rfilename);
- const info = QUANTIZATION_INFO[quant];
+ const quant = extractQuantization(f.rfilename)
+ const info = QUANTIZATION_INFO[quant]
return {
fileName: baseName(f.rfilename),
quant,
@@ -140,10 +162,10 @@ export async function getModelFiles(
recommended: info?.recommended ?? false,
sizeBytes: f.size ?? 0,
downloadUrl: url(f.rfilename),
- mmproj: matchMmproj(f.rfilename),
- };
+ mmproj: matchMmproj(f.rfilename)
+ }
})
- .sort((a, b) => Number(b.recommended) - Number(a.recommended) || a.sizeBytes - b.sizeBytes);
+ .sort((a, b) => Number(b.recommended) - Number(a.recommended) || a.sizeBytes - b.sizeBytes)
}
/**
@@ -155,50 +177,86 @@ export async function resolveHuggingFaceModel(
repoId: string,
opts: { kind?: ModelKind; fetchImpl?: FetchLike } = {}
): Promise {
- const fetchImpl = opts.fetchImpl ?? defaultFetch;
- const res = await fetchImpl(`${HF_API}/models/${repoId}`, { headers: { Accept: 'application/json' } });
- if (!res.ok) return null;
- const data = (await res.json()) as HFModel;
- const siblings = data.siblings ?? [];
- const url = (rf: string): string => `${HF}/${repoId}/resolve/main/${rf}`;
- const org = repoId.split('/')[0];
+ const fetchImpl = opts.fetchImpl ?? defaultFetch
+ const res = await fetchImpl(`${HF_API}/models/${repoId}`, {
+ headers: { Accept: 'application/json' }
+ })
+ if (!res.ok) return null
+ const data = (await res.json()) as HFModel
+ const siblings = data.siblings ?? []
+ const url = (rf: string): string => `${HF}/${repoId}/resolve/main/${rf}`
+ const org = repoId.split('/')[0]
// Non-GGUF runtimes our pipeline already supports: whisper transcription reads
// ggml `.bin`; Kokoro/Piper TTS read `.onnx`. Detect these first so a search →
// download works for them, not just llama.cpp GGUF models.
- const ggml = siblings.filter((f) => /ggml.*\.bin$/i.test(f.rfilename));
+ const ggml = siblings.filter((f) => /ggml.*\.bin$/i.test(f.rfilename))
if (ggml.length > 0) {
// Prefer the multilingual base (good speed/quality default), else smallest.
- const pick = ggml.find((f) => /ggml-base\.bin$/i.test(f.rfilename))
- ?? [...ggml].sort((a, b) => (a.size ?? 0) - (b.size ?? 0))[0];
+ const pick =
+ ggml.find((f) => /ggml-base\.bin$/i.test(f.rfilename)) ??
+ [...ggml].sort((a, b) => (a.size ?? 0) - (b.size ?? 0))[0]
return {
- id: repoId, name: baseName(repoId), kind: 'transcription', org,
- files: [{ name: baseName(pick.rfilename), url: url(pick.rfilename), sizeBytes: pick.size, role: 'primary' }],
- };
+ id: repoId,
+ name: baseName(repoId),
+ kind: 'transcription',
+ org,
+ files: [
+ {
+ name: baseName(pick.rfilename),
+ url: url(pick.rfilename),
+ sizeBytes: pick.size,
+ role: 'primary'
+ }
+ ]
+ }
}
- const onnx = siblings.filter((f) => /\.onnx$/i.test(f.rfilename));
+ const onnx = siblings.filter((f) => /\.onnx$/i.test(f.rfilename))
if (onnx.length > 0 && siblings.every((f) => !f.rfilename.endsWith('.gguf'))) {
- const pick = onnx.find((f) => /quant/i.test(f.rfilename)) ?? onnx[0];
- const files: ModelFile[] = [{ name: baseName(pick.rfilename), url: url(pick.rfilename), sizeBytes: pick.size, role: 'primary' }];
+ const pick = onnx.find((f) => /quant/i.test(f.rfilename)) ?? onnx[0]
+ const files: ModelFile[] = [
+ {
+ name: baseName(pick.rfilename),
+ url: url(pick.rfilename),
+ sizeBytes: pick.size,
+ role: 'primary'
+ }
+ ]
// Piper voices ship a sidecar .onnx.json the runtime needs.
- const cfg = siblings.find((f) => f.rfilename === `${pick.rfilename}.json`);
- if (cfg) files.push({ name: baseName(cfg.rfilename), url: url(cfg.rfilename), sizeBytes: cfg.size, role: 'aux' });
- return { id: repoId, name: baseName(repoId), kind: 'voice', org, files };
+ const cfg = siblings.find((f) => f.rfilename === `${pick.rfilename}.json`)
+ if (cfg)
+ files.push({
+ name: baseName(cfg.rfilename),
+ url: url(cfg.rfilename),
+ sizeBytes: cfg.size,
+ role: 'aux'
+ })
+ return { id: repoId, name: baseName(repoId), kind: 'voice', org, files }
}
- const gguf = siblings.filter((f) => f.rfilename.endsWith('.gguf'));
- if (gguf.length === 0) return null;
+ const gguf = siblings.filter((f) => f.rfilename.endsWith('.gguf'))
+ if (gguf.length === 0) return null
- const weights = gguf.filter((f) => !isMmproj(f.rfilename));
- const mmprojFiles = gguf.filter((f) => isMmproj(f.rfilename));
- const primary = weights.find((f) => /q4_k_m/i.test(f.rfilename)) ?? weights[0] ?? gguf[0];
- if (!primary) return null;
+ const weights = gguf.filter((f) => !isMmproj(f.rfilename))
+ const mmprojFiles = gguf.filter((f) => isMmproj(f.rfilename))
+ const primary = weights.find((f) => /q4_k_m/i.test(f.rfilename)) ?? weights[0] ?? gguf[0]
+ if (!primary) return null
const files: ModelFile[] = [
- { name: baseName(primary.rfilename), url: url(primary.rfilename), sizeBytes: primary.size, role: 'primary' },
- ];
+ {
+ name: baseName(primary.rfilename),
+ url: url(primary.rfilename),
+ sizeBytes: primary.size,
+ role: 'primary'
+ }
+ ]
if (mmprojFiles[0]) {
- files.push({ name: baseName(mmprojFiles[0].rfilename), url: url(mmprojFiles[0].rfilename), sizeBytes: mmprojFiles[0].size, role: 'mmproj' });
+ files.push({
+ name: baseName(mmprojFiles[0].rfilename),
+ url: url(mmprojFiles[0].rfilename),
+ sizeBytes: mmprojFiles[0].size,
+ role: 'mmproj'
+ })
}
return {
@@ -206,6 +264,6 @@ export async function resolveHuggingFaceModel(
name: baseName(repoId),
kind: mmprojFiles.length ? 'vision' : (opts.kind ?? 'text'),
org,
- files,
- };
+ files
+ }
}
diff --git a/packages/models/src/imagegen.ts b/packages/models/src/imagegen.ts
index 0e0b9511..71a13aba 100644
--- a/packages/models/src/imagegen.ts
+++ b/packages/models/src/imagegen.ts
@@ -4,45 +4,48 @@
// defines the shared request/result shape + capability so UI and orchestration
// are identical across platforms.
-export type ImageGenMode = 'txt2img' | 'img2img';
+export type ImageGenMode = 'txt2img' | 'img2img'
export interface ImageGenRequest {
- prompt: string;
- mode: ImageGenMode;
- negativePrompt?: string;
+ prompt: string
+ mode: ImageGenMode
+ negativePrompt?: string
/** Input image for img2img (base64 data URL or local path). */
- initImage?: string;
+ initImage?: string
/** img2img denoising strength, 0..1 (how much to change the input). */
- strength?: number;
- width?: number;
- height?: number;
- steps?: number;
- seed?: number;
- signal?: AbortSignal;
+ strength?: number
+ width?: number
+ height?: number
+ steps?: number
+ seed?: number
+ signal?: AbortSignal
}
export interface ImageGenResult {
/** Output image (base64 data URL or local path). */
- image: string;
- seed?: number;
+ image: string
+ seed?: number
}
/** A platform diffusion runtime. Implemented per-platform, used the same way. */
export interface ImageGenProvider {
- readonly id: string;
+ readonly id: string
/** Modes this provider/model supports (e.g. ['txt2img','img2img']). */
- readonly modes: ImageGenMode[];
- generate(req: ImageGenRequest): Promise;
+ readonly modes: ImageGenMode[]
+ generate(req: ImageGenRequest): Promise
}
export function supportsMode(provider: ImageGenProvider, mode: ImageGenMode): boolean {
- return provider.modes.includes(mode);
+ return provider.modes.includes(mode)
}
/** Validate a request against a provider's capabilities before running it. */
-export function validateImageGenRequest(provider: ImageGenProvider, req: ImageGenRequest): string | null {
- if (!supportsMode(provider, req.mode)) return `provider does not support ${req.mode}`;
- if (req.mode === 'img2img' && !req.initImage) return 'img2img requires an initImage';
- if (!req.prompt.trim()) return 'prompt is required';
- return null;
+export function validateImageGenRequest(
+ provider: ImageGenProvider,
+ req: ImageGenRequest
+): string | null {
+ if (!supportsMode(provider, req.mode)) return `provider does not support ${req.mode}`
+ if (req.mode === 'img2img' && !req.initImage) return 'img2img requires an initImage'
+ if (!req.prompt.trim()) return 'prompt is required'
+ return null
}
diff --git a/packages/models/src/index.ts b/packages/models/src/index.ts
index 9eb5f1d2..e9a00d82 100644
--- a/packages/models/src/index.ts
+++ b/packages/models/src/index.ts
@@ -3,29 +3,29 @@
// manager (text, vision, image, voice, transcription; more soon). The actual
// file IO is a platform DownloadBridge (see ./node for desktop/Electron).
-export * from './types';
+export * from './types'
export {
CATALOG,
MODEL_KINDS,
RECOMMENDATION_TIERS,
recommendForRam,
- modelsByKind,
-} from './catalog';
-export { ModelDownloader } from './download';
-export { searchHuggingFace, resolveHuggingFaceModel, getModelFiles } from './hf';
-export type { HFSearchResult, ModelFileVariant } from './hf';
-export { QUANTIZATION_INFO, extractQuantization, isMMProjFile, formatFileSize } from './quant';
-export type { QuantInfo } from './quant';
+ modelsByKind
+} from './catalog'
+export { ModelDownloader } from './download'
+export { searchHuggingFace, resolveHuggingFaceModel, getModelFiles } from './hf'
+export type { HFSearchResult, ModelFileVariant } from './hf'
+export { QUANTIZATION_INFO, extractQuantization, isMMProjFile, formatFileSize } from './quant'
+export type { QuantInfo } from './quant'
export {
determineCredibility,
CREDIBILITY_LABELS,
OFFICIAL_MODEL_AUTHORS,
- VERIFIED_QUANTIZERS,
-} from './credibility';
-export type { Credibility } from './credibility';
-export * from './providers';
-export * from './filters';
-export { supportsMode, validateImageGenRequest } from './imagegen';
-export type { ImageGenMode, ImageGenRequest, ImageGenResult, ImageGenProvider } from './imagegen';
-export { recommendedImageModelId, LIGHT_MODEL_RAM_CEILING_GB } from './recommend-image';
-export type { RecommendableModel } from './recommend-image';
+ VERIFIED_QUANTIZERS
+} from './credibility'
+export type { Credibility } from './credibility'
+export * from './providers'
+export * from './filters'
+export { supportsMode, validateImageGenRequest } from './imagegen'
+export type { ImageGenMode, ImageGenRequest, ImageGenResult, ImageGenProvider } from './imagegen'
+export { recommendedImageModelId, LIGHT_MODEL_RAM_CEILING_GB } from './recommend-image'
+export type { RecommendableModel } from './recommend-image'
diff --git a/packages/models/src/providers.ts b/packages/models/src/providers.ts
index 776b6c57..48be173d 100644
--- a/packages/models/src/providers.ts
+++ b/packages/models/src/providers.ts
@@ -5,91 +5,96 @@
// InferenceProvider interface directly. All shared; platforms inject nothing
// beyond an endpoint (or, for in-process, their own provider).
-export type ChatRole = 'system' | 'user' | 'assistant';
+export type ChatRole = 'system' | 'user' | 'assistant'
export interface ChatMessage {
- role: ChatRole;
- content: string;
+ role: ChatRole
+ content: string
}
export interface ChatOptions {
- model?: string;
- temperature?: number;
- maxTokens?: number;
- signal?: AbortSignal;
+ model?: string
+ temperature?: number
+ maxTokens?: number
+ signal?: AbortSignal
}
export interface ProviderModel {
- id: string;
- name: string;
+ id: string
+ name: string
}
/** Local or remote LLM. chat() streams text chunks. */
export interface InferenceProvider {
- readonly id: string;
- readonly name: string;
- listModels(): Promise;
- chat(messages: ChatMessage[], opts?: ChatOptions): AsyncIterable;
+ readonly id: string
+ readonly name: string
+ listModels(): Promise
+ chat(messages: ChatMessage[], opts?: ChatOptions): AsyncIterable
}
-export type RemoteServerKind = 'openai' | 'ollama';
+export type RemoteServerKind = 'openai' | 'ollama'
export interface RemoteServerConfig {
- id: string;
- name: string;
- kind: RemoteServerKind;
+ id: string
+ name: string
+ kind: RemoteServerKind
/** Base URL. OpenAI-compatible includes the /v1 suffix; Ollama is the host root. */
- endpoint: string;
- apiKey?: string;
+ endpoint: string
+ apiKey?: string
}
interface FetchResponse {
- ok: boolean;
- status: number;
- json(): Promise;
- body: ReadableStream | null;
+ ok: boolean
+ status: number
+ json(): Promise
+ body: ReadableStream | null
}
-export type FetchLike = (url: string, init?: {
- method?: string;
- headers?: Record;
- body?: string;
- signal?: AbortSignal;
-}) => Promise;
+export type FetchLike = (
+ url: string,
+ init?: {
+ method?: string
+ headers?: Record
+ body?: string
+ signal?: AbortSignal
+ }
+) => Promise
-const defaultFetch: FetchLike = (url, init) => fetch(url, init) as unknown as Promise;
+const defaultFetch: FetchLike = (url, init) => fetch(url, init) as unknown as Promise
function authHeaders(apiKey?: string): Record {
- return apiKey ? { Authorization: `Bearer ${apiKey}` } : {};
+ return apiKey ? { Authorization: `Bearer ${apiKey}` } : {}
}
async function* lines(body: ReadableStream): AsyncGenerator {
- const reader = body.getReader();
- const decoder = new TextDecoder();
- let buf = '';
+ const reader = body.getReader()
+ const decoder = new TextDecoder()
+ let buf = ''
for (;;) {
- const { done, value } = await reader.read();
- if (done) break;
- buf += decoder.decode(value, { stream: true });
- const parts = buf.split('\n');
- buf = parts.pop() ?? '';
- for (const line of parts) yield line;
+ const { done, value } = await reader.read()
+ if (done) break
+ buf += decoder.decode(value, { stream: true })
+ const parts = buf.split('\n')
+ buf = parts.pop() ?? ''
+ for (const line of parts) yield line
}
- if (buf.trim()) yield buf;
+ if (buf.trim()) yield buf
}
/** OpenAI-compatible provider: local llama-server, LM Studio, LocalAI, OpenAI. */
export function openAICompatibleProvider(cfg: {
- id: string;
- name: string;
- endpoint: string;
- apiKey?: string;
- fetchImpl?: FetchLike;
+ id: string
+ name: string
+ endpoint: string
+ apiKey?: string
+ fetchImpl?: FetchLike
}): InferenceProvider {
- const f = cfg.fetchImpl ?? defaultFetch;
+ const f = cfg.fetchImpl ?? defaultFetch
return {
id: cfg.id,
name: cfg.name,
async listModels() {
- const res = await f(`${cfg.endpoint}/models`, { headers: { Accept: 'application/json', ...authHeaders(cfg.apiKey) } });
- if (!res.ok) throw new Error(`listModels failed: HTTP ${res.status}`);
- const data = (await res.json()) as { data?: { id: string }[] };
- return (data.data ?? []).map((m) => ({ id: m.id, name: m.id }));
+ const res = await f(`${cfg.endpoint}/models`, {
+ headers: { Accept: 'application/json', ...authHeaders(cfg.apiKey) }
+ })
+ if (!res.ok) throw new Error(`listModels failed: HTTP ${res.status}`)
+ const data = (await res.json()) as { data?: { id: string }[] }
+ return (data.data ?? []).map((m) => ({ id: m.id, name: m.id }))
},
async *chat(messages, opts) {
const res = await f(`${cfg.endpoint}/chat/completions`, {
@@ -100,96 +105,110 @@ export function openAICompatibleProvider(cfg: {
messages,
stream: true,
temperature: opts?.temperature,
- max_tokens: opts?.maxTokens,
+ max_tokens: opts?.maxTokens
}),
- signal: opts?.signal,
- });
- if (!res.ok || !res.body) throw new Error(`chat failed: HTTP ${res.status}`);
+ signal: opts?.signal
+ })
+ if (!res.ok || !res.body) throw new Error(`chat failed: HTTP ${res.status}`)
for await (const line of lines(res.body)) {
- const t = line.trim();
- if (!t.startsWith('data:')) continue;
- const data = t.slice(5).trim();
- if (data === '[DONE]') return;
+ const t = line.trim()
+ if (!t.startsWith('data:')) continue
+ const data = t.slice(5).trim()
+ if (data === '[DONE]') return
try {
- const j = JSON.parse(data) as { choices?: { delta?: { content?: string } }[] };
- const c = j.choices?.[0]?.delta?.content;
- if (c) yield c;
+ const j = JSON.parse(data) as { choices?: { delta?: { content?: string } }[] }
+ const c = j.choices?.[0]?.delta?.content
+ if (c) yield c
} catch {
// ignore keep-alives / malformed lines
}
}
- },
- };
+ }
+ }
}
/** Ollama provider (/api/tags, /api/chat NDJSON). */
export function ollamaProvider(cfg: {
- id: string;
- name: string;
- endpoint: string;
- fetchImpl?: FetchLike;
+ id: string
+ name: string
+ endpoint: string
+ fetchImpl?: FetchLike
}): InferenceProvider {
- const f = cfg.fetchImpl ?? defaultFetch;
+ const f = cfg.fetchImpl ?? defaultFetch
return {
id: cfg.id,
name: cfg.name,
async listModels() {
- const res = await f(`${cfg.endpoint}/api/tags`, { headers: { Accept: 'application/json' } });
- if (!res.ok) throw new Error(`listModels failed: HTTP ${res.status}`);
- const data = (await res.json()) as { models?: { name: string }[] };
- return (data.models ?? []).map((m) => ({ id: m.name, name: m.name }));
+ const res = await f(`${cfg.endpoint}/api/tags`, { headers: { Accept: 'application/json' } })
+ if (!res.ok) throw new Error(`listModels failed: HTTP ${res.status}`)
+ const data = (await res.json()) as { models?: { name: string }[] }
+ return (data.models ?? []).map((m) => ({ id: m.name, name: m.name }))
},
async *chat(messages, opts) {
const res = await f(`${cfg.endpoint}/api/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ model: opts?.model, messages, stream: true }),
- signal: opts?.signal,
- });
- if (!res.ok || !res.body) throw new Error(`chat failed: HTTP ${res.status}`);
+ signal: opts?.signal
+ })
+ if (!res.ok || !res.body) throw new Error(`chat failed: HTTP ${res.status}`)
for await (const line of lines(res.body)) {
- const t = line.trim();
- if (!t) continue;
+ const t = line.trim()
+ if (!t) continue
try {
- const j = JSON.parse(t) as { message?: { content?: string }; done?: boolean };
- if (j.message?.content) yield j.message.content;
- if (j.done) return;
+ const j = JSON.parse(t) as { message?: { content?: string }; done?: boolean }
+ if (j.message?.content) yield j.message.content
+ if (j.done) return
} catch {
// ignore
}
}
- },
- };
+ }
+ }
}
/** Build a provider from a remote server config. */
-export function createProvider(server: RemoteServerConfig, fetchImpl?: FetchLike): InferenceProvider {
+export function createProvider(
+ server: RemoteServerConfig,
+ fetchImpl?: FetchLike
+): InferenceProvider {
if (server.kind === 'ollama') {
- return ollamaProvider({ id: server.id, name: server.name, endpoint: server.endpoint, fetchImpl });
+ return ollamaProvider({
+ id: server.id,
+ name: server.name,
+ endpoint: server.endpoint,
+ fetchImpl
+ })
}
- return openAICompatibleProvider({ id: server.id, name: server.name, endpoint: server.endpoint, apiKey: server.apiKey, fetchImpl });
+ return openAICompatibleProvider({
+ id: server.id,
+ name: server.name,
+ endpoint: server.endpoint,
+ apiKey: server.apiKey,
+ fetchImpl
+ })
}
/** Registry of available providers (local + remote) with an active selection. */
export class ProviderRegistry {
- private providers = new Map();
- private activeId: string | null = null;
+ private providers = new Map()
+ private activeId: string | null = null
register(provider: InferenceProvider): void {
- this.providers.set(provider.id, provider);
- if (!this.activeId) this.activeId = provider.id;
+ this.providers.set(provider.id, provider)
+ if (!this.activeId) this.activeId = provider.id
}
unregister(id: string): void {
- this.providers.delete(id);
- if (this.activeId === id) this.activeId = this.providers.keys().next().value ?? null;
+ this.providers.delete(id)
+ if (this.activeId === id) this.activeId = this.providers.keys().next().value ?? null
}
list(): InferenceProvider[] {
- return [...this.providers.values()];
+ return [...this.providers.values()]
}
setActive(id: string): void {
- if (this.providers.has(id)) this.activeId = id;
+ if (this.providers.has(id)) this.activeId = id
}
active(): InferenceProvider | null {
- return this.activeId ? this.providers.get(this.activeId) ?? null : null;
+ return this.activeId ? (this.providers.get(this.activeId) ?? null) : null
}
}
diff --git a/packages/models/src/quant.ts b/packages/models/src/quant.ts
index 23132e1b..a3fbfb1b 100644
--- a/packages/models/src/quant.ts
+++ b/packages/models/src/quant.ts
@@ -2,48 +2,102 @@
// and mobile share one definition of quant quality/size/recommendation.
export interface QuantInfo {
- bitsPerWeight: number;
- quality: string;
- description: string;
- recommended: boolean;
+ bitsPerWeight: number
+ quality: string
+ description: string
+ recommended: boolean
}
export const QUANTIZATION_INFO: Record = {
- Q2_K: { bitsPerWeight: 2.625, quality: 'Low', description: 'Extreme compression, noticeable quality loss', recommended: false },
- Q3_K_S: { bitsPerWeight: 3.4375, quality: 'Low-Medium', description: 'High compression, some quality loss', recommended: false },
- Q3_K_M: { bitsPerWeight: 3.4375, quality: 'Medium', description: 'Good compression with acceptable quality', recommended: false },
- Q4_0: { bitsPerWeight: 4, quality: 'Medium', description: 'Basic 4-bit quantization', recommended: false },
- Q4_K_S: { bitsPerWeight: 4.5, quality: 'Medium-Good', description: 'Good balance of size and quality', recommended: true },
- Q4_K_M: { bitsPerWeight: 4.5, quality: 'Good', description: 'Optimal balance - best for most devices', recommended: true },
- Q5_K_S: { bitsPerWeight: 5.5, quality: 'Good-High', description: 'Higher quality, larger size', recommended: false },
- Q5_K_M: { bitsPerWeight: 5.5, quality: 'High', description: 'Near original quality', recommended: false },
- Q6_K: { bitsPerWeight: 6.5, quality: 'Very High', description: 'Minimal quality loss', recommended: false },
- Q8_0: { bitsPerWeight: 8, quality: 'Excellent', description: 'Best quality, largest size', recommended: false },
-};
+ Q2_K: {
+ bitsPerWeight: 2.625,
+ quality: 'Low',
+ description: 'Extreme compression, noticeable quality loss',
+ recommended: false
+ },
+ Q3_K_S: {
+ bitsPerWeight: 3.4375,
+ quality: 'Low-Medium',
+ description: 'High compression, some quality loss',
+ recommended: false
+ },
+ Q3_K_M: {
+ bitsPerWeight: 3.4375,
+ quality: 'Medium',
+ description: 'Good compression with acceptable quality',
+ recommended: false
+ },
+ Q4_0: {
+ bitsPerWeight: 4,
+ quality: 'Medium',
+ description: 'Basic 4-bit quantization',
+ recommended: false
+ },
+ Q4_K_S: {
+ bitsPerWeight: 4.5,
+ quality: 'Medium-Good',
+ description: 'Good balance of size and quality',
+ recommended: true
+ },
+ Q4_K_M: {
+ bitsPerWeight: 4.5,
+ quality: 'Good',
+ description: 'Optimal balance - best for most devices',
+ recommended: true
+ },
+ Q5_K_S: {
+ bitsPerWeight: 5.5,
+ quality: 'Good-High',
+ description: 'Higher quality, larger size',
+ recommended: false
+ },
+ Q5_K_M: {
+ bitsPerWeight: 5.5,
+ quality: 'High',
+ description: 'Near original quality',
+ recommended: false
+ },
+ Q6_K: {
+ bitsPerWeight: 6.5,
+ quality: 'Very High',
+ description: 'Minimal quality loss',
+ recommended: false
+ },
+ Q8_0: {
+ bitsPerWeight: 8,
+ quality: 'Excellent',
+ description: 'Best quality, largest size',
+ recommended: false
+ }
+}
/** Extract a quantization label from a GGUF filename. */
export function extractQuantization(fileName: string): string {
- const upper = fileName.toUpperCase();
+ const upper = fileName.toUpperCase()
for (const quant of Object.keys(QUANTIZATION_INFO)) {
- if (upper.includes(quant.replace('_', '')) || upper.includes(quant)) return quant;
+ if (upper.includes(quant.replace('_', '')) || upper.includes(quant)) return quant
}
- const match = fileName.match(/[QqFf]\d+[_]?[KkMmSs]*/);
- return match ? match[0].toUpperCase() : 'Unknown';
+ const match = fileName.match(/[QqFf]\d+[_]?[KkMmSs]*/)
+ return match ? match[0].toUpperCase() : 'Unknown'
}
export function isMMProjFile(fileName: string): boolean {
- const lower = fileName.toLowerCase();
- return lower.includes('mmproj') || lower.includes('projector') || (lower.includes('clip') && lower.endsWith('.gguf'));
+ const lower = fileName.toLowerCase()
+ return (
+ lower.includes('mmproj') ||
+ lower.includes('projector') ||
+ (lower.includes('clip') && lower.endsWith('.gguf'))
+ )
}
export function formatFileSize(bytes: number): string {
- if (!bytes) return 'Unknown';
- const units = ['B', 'KB', 'MB', 'GB'];
- let n = bytes;
- let i = 0;
+ if (!bytes) return 'Unknown'
+ const units = ['B', 'KB', 'MB', 'GB']
+ let n = bytes
+ let i = 0
while (n >= 1024 && i < units.length - 1) {
- n /= 1024;
- i++;
+ n /= 1024
+ i++
}
- return `${n.toFixed(n >= 10 || i === 0 ? 0 : 1)} ${units[i]}`;
+ return `${n.toFixed(n >= 10 || i === 0 ? 0 : 1)} ${units[i]}`
}
diff --git a/packages/models/src/recommend-image.ts b/packages/models/src/recommend-image.ts
index 6b302f92..f1b88375 100644
--- a/packages/models/src/recommend-image.ts
+++ b/packages/models/src/recommend-image.ts
@@ -11,32 +11,32 @@
* callers can pass either the package `ModelEntry` or a renderer-local model type
* (whose `kind` is a plain string) without a cast. */
export interface RecommendableModel {
- id: string;
- kind: string;
- tags?: string[];
+ id: string
+ kind: string
+ tags?: string[]
}
/** RAM (GB) at or below which the lighter (Light-tagged) quant is recommended.
* 16GB is the ceiling: verified that the full Q8 DreamShaper pegs memory (~4.7GB
* peak) and can freeze a 16GB Mac, while the Q4 (~3.08GB peak) does not. */
-export const LIGHT_MODEL_RAM_CEILING_GB = 16;
+export const LIGHT_MODEL_RAM_CEILING_GB = 16
const hasLightTag = (m: RecommendableModel): boolean =>
- (m.tags ?? []).some((t) => /^light$/i.test(t));
+ (m.tags ?? []).some((t) => /^light$/i.test(t))
const isVersatile = (m: RecommendableModel): boolean =>
- (m.tags ?? []).some((t) => /^versatile$/i.test(t));
+ (m.tags ?? []).some((t) => /^versatile$/i.test(t))
/** Prefer the 'Versatile' all-rounder (DreamShaper) when several models qualify,
* so the badge is stable no matter how many Light variants the catalog lists /
* their order. Falls back to the first candidate. */
const pickVersatileFirst = (candidates: RecommendableModel[]): RecommendableModel | undefined =>
- candidates.find(isVersatile) ?? candidates[0];
+ candidates.find(isVersatile) ?? candidates[0]
/** Family key for pairing a full quant with its Light sibling: the id with any
* trailing quant suffix (e.g. "-Q4") stripped, so both DreamShaper entries map
* to the same family. */
-const familyKey = (m: RecommendableModel): string => m.id.replace(/-Q\d[\w]*$/i, '');
+const familyKey = (m: RecommendableModel): string => m.id.replace(/-Q\d[\w]*$/i, '')
/**
* The image model id best suited to a machine with `ramGb` RAM, or null when no
@@ -48,19 +48,23 @@ const familyKey = (m: RecommendableModel): string => m.id.replace(/-Q\d[\w]*$/i,
* family (DreamShaper) rather than an unrelated heavy model. Falls back to any
* Light model when only that exists (small machine) / the family's full entry.
*/
-export function recommendedImageModelId(models: RecommendableModel[], ramGb: number | null | undefined): string | null {
- if (!ramGb || !Number.isFinite(ramGb)) return null;
- const images = models.filter((m) => m.kind === 'image');
- if (!images.length) return null;
+export function recommendedImageModelId(
+ models: RecommendableModel[],
+ ramGb: number | null | undefined
+): string | null {
+ if (!ramGb || !Number.isFinite(ramGb)) return null
+ const images = models.filter((m) => m.kind === 'image')
+ if (!images.length) return null
- const light = images.filter(hasLightTag);
+ const light = images.filter(hasLightTag)
// Families that ship a Light variant — those are the ones we recommend within.
- const lightFamilies = new Set(light.map(familyKey));
- const fullOfLightFamily = images.filter((m) => !hasLightTag(m) && lightFamilies.has(familyKey(m)));
+ const lightFamilies = new Set(light.map(familyKey))
+ const fullOfLightFamily = images.filter((m) => !hasLightTag(m) && lightFamilies.has(familyKey(m)))
if (ramGb <= LIGHT_MODEL_RAM_CEILING_GB) {
- return (pickVersatileFirst(light) ?? images[0]).id;
+ return (pickVersatileFirst(light) ?? images[0]).id
}
// Above the ceiling: the full sibling of a Light family, else any non-Light image model.
- return (pickVersatileFirst(fullOfLightFamily) ?? images.find((m) => !hasLightTag(m)) ?? images[0]).id;
+ return (pickVersatileFirst(fullOfLightFamily) ?? images.find((m) => !hasLightTag(m)) ?? images[0])
+ .id
}
diff --git a/packages/models/src/types.ts b/packages/models/src/types.ts
index cd530e92..06adb713 100644
--- a/packages/models/src/types.ts
+++ b/packages/models/src/types.ts
@@ -5,73 +5,73 @@
// The catalog and downloader are kind-agnostic; the host runtime knows how to
// load each kind.
-import type { ImageGenMode } from './imagegen';
+import type { ImageGenMode } from './imagegen'
-export type ModelKind = 'text' | 'vision' | 'image' | 'voice' | 'transcription';
+export type ModelKind = 'text' | 'vision' | 'image' | 'voice' | 'transcription'
export interface ModelFile {
/** Filename on disk, e.g. "Qwen3.5-2B-Q4_K_M.gguf". */
- name: string;
+ name: string
/** Download URL (often a Hugging Face resolve URL). */
- url: string;
+ url: string
/** Size in bytes when known (for progress + RAM/disk checks). */
- sizeBytes?: number;
+ sizeBytes?: number
/** Marks an auxiliary file (e.g. a vision mmproj) vs the primary weights. */
- role?: 'primary' | 'mmproj' | 'tokenizer' | 'aux';
+ role?: 'primary' | 'mmproj' | 'tokenizer' | 'aux'
}
export interface ModelEntry {
/** Stable id, usually the HF repo id, e.g. "unsloth/Qwen3.5-2B-GGUF". */
- id: string;
- name: string;
- kind: ModelKind;
+ id: string
+ name: string
+ kind: ModelKind
/** Provider/org, e.g. "google", "Qwen", "openai-whisper". */
- org?: string;
- description?: string;
+ org?: string
+ description?: string
/** Billions of parameters (LLMs); omitted for non-LLM kinds. */
- params?: number;
+ params?: number
/** Minimum device RAM (GB) recommended. */
- minRamGb?: number;
+ minRamGb?: number
/** Quantization label, e.g. "Q4_K_M". */
- quant?: string;
+ quant?: string
/** Files to download for this model. */
- files: ModelFile[];
+ files: ModelFile[]
/** For image models: which generation modes it supports (txt2img/img2img). */
- imageModes?: ImageGenMode[];
- isNew?: boolean;
+ imageModes?: ImageGenMode[]
+ isNew?: boolean
/** Short capability labels shown as chips, e.g. ['Recommended','Fast','Photoreal']. */
- tags?: string[];
+ tags?: string[]
/** Which on-device runtime serves this model. Default 'sd-cli' (stable-diffusion.cpp).
* 'mflux' = the bundled MLX runtime (Apple-Silicon-only; fetches its own weights). */
- runtime?: 'sd-cli' | 'mflux';
+ runtime?: 'sd-cli' | 'mflux'
/** For transcription models: which STT engine serves it. Default 'whisper' when
* omitted. 'parakeet' models are multi-file ONNX sets run by the sherpa-onnx CLI. */
- engine?: 'whisper' | 'parakeet';
+ engine?: 'whisper' | 'parakeet'
/** Release date (ISO yyyy-mm-dd), from the source repo's createdAt. Shown on the
* card and used to surface only recent ("post-Jan-2026") small models. */
- releaseDate?: string;
+ releaseDate?: string
}
/** RAM tier -> max model size + quant, for recommending a default model. */
export interface ModelRecommendationTier {
- minRamGb: number;
- maxRamGb: number;
- maxParams: number;
- quantization: string;
+ minRamGb: number
+ maxRamGb: number
+ maxParams: number
+ quantization: string
}
-export type DownloadStatus = 'queued' | 'downloading' | 'paused' | 'completed' | 'failed';
+export type DownloadStatus = 'queued' | 'downloading' | 'paused' | 'completed' | 'failed'
export interface DownloadProgress {
- modelId: string;
- status: DownloadStatus;
+ modelId: string
+ status: DownloadStatus
/** 0..1 across all of the model's files. */
- progress: number;
- bytesDownloaded: number;
- totalBytes: number;
- currentFile?: string;
- speedBytesPerSec?: number;
- error?: string;
+ progress: number
+ bytesDownloaded: number
+ totalBytes: number
+ currentFile?: string
+ speedBytesPerSec?: number
+ error?: string
}
/** Platform file download (Node/Electron streams to disk; RN background downloader). */
@@ -82,17 +82,17 @@ export interface DownloadBridge {
url: string,
destPath: string,
opts: { onProgress?: (written: number, total: number) => void; signal?: AbortSignal }
- ): Promise;
+ ): Promise
/** Whether a fully-downloaded file already exists (size match). */
- exists(destPath: string, expectedBytes?: number): Promise;
+ exists(destPath: string, expectedBytes?: number): Promise
/** Join the models directory with a filename. */
- pathFor(fileName: string): string;
+ pathFor(fileName: string): string
}
/** Records which models are installed (a memory entity or local store). */
export interface ModelStore {
- markInstalled(entry: ModelEntry): void;
- isInstalled(modelId: string): boolean;
- installed(): ModelEntry[];
- remove(modelId: string): void;
+ markInstalled(entry: ModelEntry): void
+ isInstalled(modelId: string): boolean
+ installed(): ModelEntry[]
+ remove(modelId: string): void
}
diff --git a/packages/rag/dist/index.js b/packages/rag/dist/index.js
index db1b9653..938ee63a 100644
--- a/packages/rag/dist/index.js
+++ b/packages/rag/dist/index.js
@@ -157,7 +157,8 @@ async function extractContent(path, fileName, bridges, opts = {}) {
text = await bridges.extractPdf(path, maxChars);
break;
case "docx":
- if (!bridges.extractDocx) throw new Error("DOCX extraction is not available on this platform.");
+ if (!bridges.extractDocx)
+ throw new Error("DOCX extraction is not available on this platform.");
text = await bridges.extractDocx(path, maxChars);
break;
case "audio":
diff --git a/packages/rag/dist/index.mjs b/packages/rag/dist/index.mjs
index eac596d3..8f0cb261 100644
--- a/packages/rag/dist/index.mjs
+++ b/packages/rag/dist/index.mjs
@@ -118,7 +118,8 @@ async function extractContent(path, fileName, bridges, opts = {}) {
text = await bridges.extractPdf(path, maxChars);
break;
case "docx":
- if (!bridges.extractDocx) throw new Error("DOCX extraction is not available on this platform.");
+ if (!bridges.extractDocx)
+ throw new Error("DOCX extraction is not available on this platform.");
text = await bridges.extractDocx(path, maxChars);
break;
case "audio":
diff --git a/packages/rag/package.json b/packages/rag/package.json
index 8da085e1..32932245 100644
--- a/packages/rag/package.json
+++ b/packages/rag/package.json
@@ -15,7 +15,7 @@
}
},
"scripts": {
- "build": "tsup src/index.ts --format esm,cjs --dts",
+ "build": "tsup src/index.ts --format esm,cjs --dts --clean",
"dev": "tsup src/index.ts --format esm,cjs --dts --watch",
"typecheck": "tsc --noEmit",
"test": "npm run build && node --test test/*.test.mjs"
diff --git a/packages/rag/src/bridges.ts b/packages/rag/src/bridges.ts
index a6be2702..4e8317ed 100644
--- a/packages/rag/src/bridges.ts
+++ b/packages/rag/src/bridges.ts
@@ -3,24 +3,24 @@
// better-sqlite3 store + Node/native extractors; mobile wires llama.rn + op-sqlite
// + RNFS/PDFKit. This keeps the chunking/retrieval/orchestration logic shared.
-import type { MediaKind, RagDocument } from './types';
+import type { MediaKind, RagDocument } from './types'
/** Turns text into an embedding vector. Same model must be used for index + query. */
export interface EmbeddingProvider {
- embed(text: string): Promise;
+ embed(text: string): Promise
/** Optional batch path; the service falls back to mapped embed() if absent. */
- embedBatch?(texts: string[]): Promise;
+ embedBatch?(texts: string[]): Promise
/** Embedding dimension (e.g. 384 for all-MiniLM-L6-v2). */
- readonly dimension: number;
+ readonly dimension: number
}
/** A stored chunk with its embedding, returned as a retrieval candidate. */
export interface ChunkCandidate {
- docId: number;
- name: string;
- content: string;
- position: number;
- embedding: number[];
+ docId: number
+ name: string
+ content: string
+ position: number
+ embedding: number[]
}
/**
@@ -31,22 +31,22 @@ export interface ChunkCandidate {
*/
export interface VectorStore {
addDocument(doc: {
- projectId: string;
- name: string;
- path: string;
- size: number;
- kind: MediaKind;
- }): Promise;
+ projectId: string
+ name: string
+ path: string
+ size: number
+ kind: MediaKind
+ }): Promise
addChunks(
docId: number,
chunks: { content: string; position: number }[],
embeddings: number[][]
- ): Promise;
+ ): Promise
/** Enabled chunks (and any extra sources) eligible for retrieval in a project. */
- getChunkCandidates(projectId: string): Promise;
- listDocuments(projectId: string): Promise;
- setDocumentEnabled(docId: number, enabled: boolean): Promise;
- deleteDocument(docId: number): Promise;
+ getChunkCandidates(projectId: string): Promise
+ listDocuments(projectId: string): Promise
+ setDocumentEnabled(docId: number, enabled: boolean): Promise
+ deleteDocument(docId: number): Promise
}
/**
@@ -56,16 +56,16 @@ export interface VectorStore {
* the engine when a file needs an unavailable capability (e.g. no vision model).
*/
export interface ExtractionBridges {
- readText(path: string): Promise;
- extractPdf?(path: string, maxChars?: number): Promise;
- extractDocx?(path: string, maxChars?: number): Promise;
+ readText(path: string): Promise
+ extractPdf?(path: string, maxChars?: number): Promise
+ extractDocx?(path: string, maxChars?: number): Promise
/** Transcribe an audio file to text (uses a downloaded transcription model). */
- transcribeAudio?(path: string): Promise;
+ transcribeAudio?(path: string): Promise
/** Sample frames from a video; returns image paths (or data URIs) to caption. */
sampleVideoFrames?(
path: string,
opts: { everySeconds?: number; maxFrames?: number }
- ): Promise;
+ ): Promise
/** Describe/OCR a single image (uses a downloaded vision model). */
- captionImage?(imagePath: string): Promise;
+ captionImage?(imagePath: string): Promise
}
diff --git a/packages/rag/src/chunking.ts b/packages/rag/src/chunking.ts
index 1bcb43ea..b9a193e2 100644
--- a/packages/rag/src/chunking.ts
+++ b/packages/rag/src/chunking.ts
@@ -5,58 +5,58 @@
export interface ChunkOptions {
/** Target chunk size in characters (default 500). */
- chunkSize?: number;
+ chunkSize?: number
/** Sliding-window overlap for oversized paragraphs (default 100). */
- overlap?: number;
+ overlap?: number
/** Drop chunks shorter than this (default 20). */
- minChunkLength?: number;
+ minChunkLength?: number
}
export interface Chunk {
- content: string;
- position: number;
+ content: string
+ position: number
}
export function chunkText(text: string, opts: ChunkOptions = {}): Chunk[] {
- const chunkSize = opts.chunkSize ?? 500;
- const overlap = opts.overlap ?? 100;
- const minChunkLength = opts.minChunkLength ?? 20;
+ const chunkSize = opts.chunkSize ?? 500
+ const overlap = opts.overlap ?? 100
+ const minChunkLength = opts.minChunkLength ?? 20
- const clean = text.replace(/\r\n/g, '\n').trim();
- if (!clean) return [];
+ const clean = text.replace(/\r\n/g, '\n').trim()
+ if (!clean) return []
const paragraphs = clean
.split(/\n\n+/)
.map((p) => p.trim())
- .filter(Boolean);
+ .filter(Boolean)
- const chunks: string[] = [];
- let buffer = '';
+ const chunks: string[] = []
+ let buffer = ''
const flush = (): void => {
- const t = buffer.trim();
- if (t.length >= minChunkLength) chunks.push(t);
- buffer = '';
- };
+ const t = buffer.trim()
+ if (t.length >= minChunkLength) chunks.push(t)
+ buffer = ''
+ }
for (const para of paragraphs) {
if (para.length > chunkSize) {
// Oversized paragraph: emit the buffer, then sliding-window the paragraph.
- flush();
- const step = Math.max(1, chunkSize - overlap);
+ flush()
+ const step = Math.max(1, chunkSize - overlap)
for (let start = 0; start < para.length; start += step) {
- const slice = para.slice(start, start + chunkSize).trim();
- if (slice.length >= minChunkLength) chunks.push(slice);
- if (start + chunkSize >= para.length) break;
+ const slice = para.slice(start, start + chunkSize).trim()
+ if (slice.length >= minChunkLength) chunks.push(slice)
+ if (start + chunkSize >= para.length) break
}
- } else if (buffer && (buffer.length + 2 + para.length) > chunkSize) {
- flush();
- buffer = para;
+ } else if (buffer && buffer.length + 2 + para.length > chunkSize) {
+ flush()
+ buffer = para
} else {
- buffer = buffer ? `${buffer}\n\n${para}` : para;
+ buffer = buffer ? `${buffer}\n\n${para}` : para
}
}
- flush();
+ flush()
- return chunks.map((content, position) => ({ content, position }));
+ return chunks.map((content, position) => ({ content, position }))
}
diff --git a/packages/rag/src/extract.ts b/packages/rag/src/extract.ts
index a5a33372..d8866f00 100644
--- a/packages/rag/src/extract.ts
+++ b/packages/rag/src/extract.ts
@@ -11,45 +11,40 @@
// Audio and video "just work" off the capabilities we already ship: a
// transcription model for audio, a vision model for video frames / images.
-import type { MediaKind } from './types';
-import type { ExtractionBridges } from './bridges';
+import type { MediaKind } from './types'
+import type { ExtractionBridges } from './bridges'
-const TEXT_EXT = new Set([
- 'txt', 'md', 'markdown', 'csv', 'json', 'xml', 'html', 'htm', 'log', 'rtf',
- 'py', 'js', 'ts', 'tsx', 'jsx', 'java', 'c', 'cpp', 'h', 'hpp', 'cs', 'swift',
- 'kt', 'go', 'rs', 'rb', 'php', 'sql', 'sh', 'yaml', 'yml', 'toml', 'ini', 'cfg', 'conf',
-]);
-const AUDIO_EXT = new Set(['mp3', 'wav', 'm4a', 'aac', 'flac', 'ogg', 'opus', 'wma']);
-const VIDEO_EXT = new Set(['mp4', 'mov', 'mkv', 'avi', 'webm', 'm4v', 'mpg', 'mpeg']);
-const IMAGE_EXT = new Set(['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp', 'tiff', 'heic']);
+const AUDIO_EXT = new Set(['mp3', 'wav', 'm4a', 'aac', 'flac', 'ogg', 'opus', 'wma'])
+const VIDEO_EXT = new Set(['mp4', 'mov', 'mkv', 'avi', 'webm', 'm4v', 'mpg', 'mpeg'])
+const IMAGE_EXT = new Set(['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp', 'tiff', 'heic'])
export function extensionOf(fileName: string): string {
- return (fileName.split('.').pop() ?? '').toLowerCase();
+ return (fileName.split('.').pop() ?? '').toLowerCase()
}
/** Classify a file by extension into a MediaKind. */
export function detectKind(fileName: string): MediaKind {
- const ext = extensionOf(fileName);
- if (ext === 'pdf') return 'pdf';
- if (ext === 'docx' || ext === 'doc') return 'docx';
- if (AUDIO_EXT.has(ext)) return 'audio';
- if (VIDEO_EXT.has(ext)) return 'video';
- if (IMAGE_EXT.has(ext)) return 'image';
- return 'text';
+ const ext = extensionOf(fileName)
+ if (ext === 'pdf') return 'pdf'
+ if (ext === 'docx' || ext === 'doc') return 'docx'
+ if (AUDIO_EXT.has(ext)) return 'audio'
+ if (VIDEO_EXT.has(ext)) return 'video'
+ if (IMAGE_EXT.has(ext)) return 'image'
+ return 'text'
}
export interface ExtractOptions {
/** Hard cap on extracted characters (default 500_000). */
- maxChars?: number;
+ maxChars?: number
/** Video: sample one frame every N seconds (default 5). */
- videoEverySeconds?: number;
+ videoEverySeconds?: number
/** Video: cap total sampled frames (default 24). */
- videoMaxFrames?: number;
+ videoMaxFrames?: number
}
export interface ExtractedContent {
- text: string;
- kind: MediaKind;
+ text: string
+ kind: MediaKind
}
/** Extract plain text from a file, routing by kind through the given bridges. */
@@ -59,48 +54,49 @@ export async function extractContent(
bridges: ExtractionBridges,
opts: ExtractOptions = {}
): Promise {
- const kind = detectKind(fileName);
- const maxChars = opts.maxChars ?? 500_000;
- let text = '';
+ const kind = detectKind(fileName)
+ const maxChars = opts.maxChars ?? 500_000
+ let text = ''
switch (kind) {
case 'pdf':
- if (!bridges.extractPdf) throw new Error('PDF extraction is not available on this platform.');
- text = await bridges.extractPdf(path, maxChars);
- break;
+ if (!bridges.extractPdf) throw new Error('PDF extraction is not available on this platform.')
+ text = await bridges.extractPdf(path, maxChars)
+ break
case 'docx':
- if (!bridges.extractDocx) throw new Error('DOCX extraction is not available on this platform.');
- text = await bridges.extractDocx(path, maxChars);
- break;
+ if (!bridges.extractDocx)
+ throw new Error('DOCX extraction is not available on this platform.')
+ text = await bridges.extractDocx(path, maxChars)
+ break
case 'audio':
if (!bridges.transcribeAudio)
- throw new Error('Audio ingestion needs a transcription model — download one from Models.');
- text = await bridges.transcribeAudio(path);
- break;
+ throw new Error('Audio ingestion needs a transcription model — download one from Models.')
+ text = await bridges.transcribeAudio(path)
+ break
case 'video': {
if (!bridges.sampleVideoFrames || !bridges.captionImage)
- throw new Error('Video ingestion needs a vision model — download one from Models.');
+ throw new Error('Video ingestion needs a vision model — download one from Models.')
const frames = await bridges.sampleVideoFrames(path, {
everySeconds: opts.videoEverySeconds ?? 5,
- maxFrames: opts.videoMaxFrames ?? 24,
- });
- const captions: string[] = [];
+ maxFrames: opts.videoMaxFrames ?? 24
+ })
+ const captions: string[] = []
for (let i = 0; i < frames.length; i++) {
- const c = (await bridges.captionImage(frames[i]))?.trim();
- if (c) captions.push(`[frame ${i + 1}] ${c}`);
+ const c = (await bridges.captionImage(frames[i]))?.trim()
+ if (c) captions.push(`[frame ${i + 1}] ${c}`)
}
- text = captions.join('\n');
- break;
+ text = captions.join('\n')
+ break
}
case 'image':
if (!bridges.captionImage)
- throw new Error('Image ingestion needs a vision model — download one from Models.');
- text = await bridges.captionImage(path);
- break;
+ throw new Error('Image ingestion needs a vision model — download one from Models.')
+ text = await bridges.captionImage(path)
+ break
default:
- text = await bridges.readText(path);
+ text = await bridges.readText(path)
}
- if (text.length > maxChars) text = text.slice(0, maxChars);
- return { text, kind };
+ if (text.length > maxChars) text = text.slice(0, maxChars)
+ return { text, kind }
}
diff --git a/packages/rag/src/index.ts b/packages/rag/src/index.ts
index fc15350c..ec57626a 100644
--- a/packages/rag/src/index.ts
+++ b/packages/rag/src/index.ts
@@ -1,32 +1,22 @@
// @offgrid/rag — shared projects + RAG engine (pure TS over injectable bridges).
-export type { MediaKind, Project, RagDocument, RagSearchResult, SearchResult } from './types';
+export type { MediaKind, Project, RagDocument, RagSearchResult, SearchResult } from './types'
-export { chunkText, type Chunk, type ChunkOptions } from './chunking';
-export { dotProduct, cosineSimilarity, topKSimilar, type SimilarityResult } from './vectorMath';
+export { chunkText, type Chunk, type ChunkOptions } from './chunking'
+export { dotProduct, cosineSimilarity, topKSimilar, type SimilarityResult } from './vectorMath'
export {
rankBySimilarity,
estimateCharBudget,
selectWithinBudget,
- formatForPrompt,
-} from './retrieval';
+ formatForPrompt
+} from './retrieval'
export {
detectKind,
extensionOf,
extractContent,
type ExtractOptions,
- type ExtractedContent,
-} from './extract';
-export {
- RagService,
- type RagServiceDeps,
- type IndexResult,
- type IndexStage,
-} from './service';
-export { SEARCH_KB_TOOL, makeSearchKnowledgeBaseHandler } from './tools';
-export type {
- EmbeddingProvider,
- VectorStore,
- ChunkCandidate,
- ExtractionBridges,
-} from './bridges';
+ type ExtractedContent
+} from './extract'
+export { RagService, type RagServiceDeps, type IndexResult, type IndexStage } from './service'
+export { SEARCH_KB_TOOL, makeSearchKnowledgeBaseHandler } from './tools'
+export type { EmbeddingProvider, VectorStore, ChunkCandidate, ExtractionBridges } from './bridges'
diff --git a/packages/rag/src/retrieval.ts b/packages/rag/src/retrieval.ts
index 5de7456e..f22677da 100644
--- a/packages/rag/src/retrieval.ts
+++ b/packages/rag/src/retrieval.ts
@@ -3,9 +3,9 @@
// from Off Grid Mobile (rag/retrieval.ts). Pure: scoring only — fetching is the
// VectorStore's job.
-import { cosineSimilarity } from './vectorMath';
-import type { ChunkCandidate } from './bridges';
-import type { RagSearchResult } from './types';
+import { cosineSimilarity } from './vectorMath'
+import type { ChunkCandidate } from './bridges'
+import type { RagSearchResult } from './types'
/** Score every candidate by cosine similarity and return the top-k, desc. */
export function rankBySimilarity(
@@ -19,41 +19,44 @@ export function rankBySimilarity(
name: c.name,
content: c.content,
position: c.position,
- score: cosineSimilarity(queryVec, c.embedding),
+ score: cosineSimilarity(queryVec, c.embedding)
}))
.sort((a, b) => b.score - a.score)
- .slice(0, topK);
+ .slice(0, topK)
}
/** Characters of context to spend on retrieved KB excerpts for a given window.
* ~4 chars/token, reserve ~40% of the window for the knowledge base. */
export function estimateCharBudget(contextLengthTokens: number): number {
- return Math.max(1000, Math.floor(contextLengthTokens * 4 * 0.4));
+ return Math.max(1000, Math.floor(contextLengthTokens * 4 * 0.4))
}
/** Greedily keep top chunks until the character budget is exhausted. */
-export function selectWithinBudget(chunks: RagSearchResult[], charBudget: number): RagSearchResult[] {
- const out: RagSearchResult[] = [];
- let total = 0;
+export function selectWithinBudget(
+ chunks: RagSearchResult[],
+ charBudget: number
+): RagSearchResult[] {
+ const out: RagSearchResult[] = []
+ let total = 0
for (const c of chunks) {
- if (out.length > 0 && total + c.content.length > charBudget) break;
- out.push(c);
- total += c.content.length;
+ if (out.length > 0 && total + c.content.length > charBudget) break
+ out.push(c)
+ total += c.content.length
}
- return out;
+ return out
}
/** Wrap retrieved excerpts in a tagged block for the system/user prompt. */
export function formatForPrompt(result: { chunks: RagSearchResult[] }): string {
- if (!result.chunks.length) return '';
+ if (!result.chunks.length) return ''
const body = result.chunks
.map((c) => `[Source: ${c.name} (part ${c.position + 1})]\n${c.content}`)
- .join('\n---\n');
+ .join('\n---\n')
return (
'\n' +
"The following excerpts are from the user's project knowledge base. " +
'Use them to answer and cite the source filename when you do.\n' +
`${body}\n` +
' '
- );
+ )
}
diff --git a/packages/rag/src/service.ts b/packages/rag/src/service.ts
index f739d287..8276aa69 100644
--- a/packages/rag/src/service.ts
+++ b/packages/rag/src/service.ts
@@ -2,30 +2,30 @@
// embeds -> stores; searchProject embeds the query and ranks stored chunks.
// Mirrors Off Grid Mobile's RagService surface so the apps wire it the same way.
-import { chunkText, type ChunkOptions } from './chunking';
-import { extractContent, type ExtractOptions } from './extract';
+import { chunkText, type ChunkOptions } from './chunking'
+import { extractContent, type ExtractOptions } from './extract'
import {
rankBySimilarity,
selectWithinBudget,
estimateCharBudget,
- formatForPrompt,
-} from './retrieval';
-import type { EmbeddingProvider, VectorStore, ExtractionBridges } from './bridges';
-import type { RagDocument, SearchResult } from './types';
+ formatForPrompt
+} from './retrieval'
+import type { EmbeddingProvider, VectorStore, ExtractionBridges } from './bridges'
+import type { RagDocument, SearchResult } from './types'
-export type IndexStage = 'extracting' | 'chunking' | 'embedding' | 'indexing' | 'done';
+export type IndexStage = 'extracting' | 'chunking' | 'embedding' | 'indexing' | 'done'
export interface RagServiceDeps {
- store: VectorStore;
- embeddings: EmbeddingProvider;
- extraction: ExtractionBridges;
- chunkOptions?: ChunkOptions;
+ store: VectorStore
+ embeddings: EmbeddingProvider
+ extraction: ExtractionBridges
+ chunkOptions?: ChunkOptions
}
export interface IndexResult {
- docId: number;
- chunkCount: number;
- kind: RagDocument['kind'];
+ docId: number
+ chunkCount: number
+ kind: RagDocument['kind']
}
export class RagService {
@@ -34,49 +34,49 @@ export class RagService {
/** Ingest a file into a project's knowledge base. */
async indexDocument(
params: {
- projectId: string;
- path: string;
- fileName: string;
- size: number;
- extract?: ExtractOptions;
+ projectId: string
+ path: string
+ fileName: string
+ size: number
+ extract?: ExtractOptions
},
onProgress?: (stage: IndexStage) => void
): Promise {
- onProgress?.('extracting');
+ onProgress?.('extracting')
const { text, kind } = await extractContent(
params.path,
params.fileName,
this.deps.extraction,
params.extract
- );
+ )
- onProgress?.('chunking');
- const chunks = chunkText(text, this.deps.chunkOptions);
+ onProgress?.('chunking')
+ const chunks = chunkText(text, this.deps.chunkOptions)
const docId = await this.deps.store.addDocument({
projectId: params.projectId,
name: params.fileName,
path: params.path,
size: params.size,
- kind,
- });
+ kind
+ })
if (chunks.length === 0) {
- onProgress?.('done');
- return { docId, chunkCount: 0, kind };
+ onProgress?.('done')
+ return { docId, chunkCount: 0, kind }
}
- onProgress?.('embedding');
- const texts = chunks.map((c) => c.content);
+ onProgress?.('embedding')
+ const texts = chunks.map((c) => c.content)
const embeddings = this.deps.embeddings.embedBatch
? await this.deps.embeddings.embedBatch(texts)
- : await Promise.all(texts.map((t) => this.deps.embeddings.embed(t)));
+ : await Promise.all(texts.map((t) => this.deps.embeddings.embed(t)))
- onProgress?.('indexing');
- await this.deps.store.addChunks(docId, chunks, embeddings);
+ onProgress?.('indexing')
+ await this.deps.store.addChunks(docId, chunks, embeddings)
- onProgress?.('done');
- return { docId, chunkCount: chunks.length, kind };
+ onProgress?.('done')
+ return { docId, chunkCount: chunks.length, kind }
}
/** Retrieve the most relevant excerpts for a query within a project. */
@@ -85,31 +85,31 @@ export class RagService {
query: string,
opts: { topK?: number; contextLength?: number } = {}
): Promise {
- const candidates = await this.deps.store.getChunkCandidates(projectId);
- if (candidates.length === 0) return { chunks: [], query };
+ const candidates = await this.deps.store.getChunkCandidates(projectId)
+ if (candidates.length === 0) return { chunks: [], query }
- const queryVec = await this.deps.embeddings.embed(query);
- let ranked = rankBySimilarity(queryVec, candidates, opts.topK ?? 5);
+ const queryVec = await this.deps.embeddings.embed(query)
+ let ranked = rankBySimilarity(queryVec, candidates, opts.topK ?? 5)
if (opts.contextLength) {
- ranked = selectWithinBudget(ranked, estimateCharBudget(opts.contextLength));
+ ranked = selectWithinBudget(ranked, estimateCharBudget(opts.contextLength))
}
- return { chunks: ranked, query };
+ return { chunks: ranked, query }
}
/** Build a prompt-ready block from a search result. */
formatForPrompt(result: SearchResult): string {
- return formatForPrompt(result);
+ return formatForPrompt(result)
}
listDocuments(projectId: string): Promise {
- return this.deps.store.listDocuments(projectId);
+ return this.deps.store.listDocuments(projectId)
}
toggleDocument(docId: number, enabled: boolean): Promise {
- return this.deps.store.setDocumentEnabled(docId, enabled);
+ return this.deps.store.setDocumentEnabled(docId, enabled)
}
deleteDocument(docId: number): Promise {
- return this.deps.store.deleteDocument(docId);
+ return this.deps.store.deleteDocument(docId)
}
}
diff --git a/packages/rag/src/tools.ts b/packages/rag/src/tools.ts
index 7bd42361..ccf25e26 100644
--- a/packages/rag/src/tools.ts
+++ b/packages/rag/src/tools.ts
@@ -3,7 +3,7 @@
// to the always-on retrieval that injects context up front). The OpenAI-style
// schema works with our local llama-server tool calling and remote providers.
-import type { SearchResult } from './types';
+import type { SearchResult } from './types'
export const SEARCH_KB_TOOL = {
type: 'function' as const,
@@ -14,23 +14,23 @@ export const SEARCH_KB_TOOL = {
parameters: {
type: 'object',
properties: {
- query: { type: 'string', description: 'What to look up in the knowledge base.' },
+ query: { type: 'string', description: 'What to look up in the knowledge base.' }
},
- required: ['query'],
- },
- },
-};
+ required: ['query']
+ }
+ }
+}
/** Build a tool handler bound to a searcher. Returns a model-ready string. */
export function makeSearchKnowledgeBaseHandler(searcher: {
- searchProject(projectId: string, query: string): Promise;
+ searchProject(projectId: string, query: string): Promise
}) {
return async (args: { query: string }, projectId?: string): Promise => {
- if (!projectId) return 'No active project. The knowledge base requires an open project.';
- const result = await searcher.searchProject(projectId, args.query);
- if (!result.chunks.length) return `No knowledge-base results found for "${args.query}".`;
+ if (!projectId) return 'No active project. The knowledge base requires an open project.'
+ const result = await searcher.searchProject(projectId, args.query)
+ if (!result.chunks.length) return `No knowledge-base results found for "${args.query}".`
return result.chunks
.map((c, i) => `[${i + 1}] ${c.name} (part ${c.position + 1}):\n${c.content}`)
- .join('\n\n---\n\n');
- };
+ .join('\n\n---\n\n')
+ }
}
diff --git a/packages/rag/src/types.ts b/packages/rag/src/types.ts
index 09d9a41b..0ffda324 100644
--- a/packages/rag/src/types.ts
+++ b/packages/rag/src/types.ts
@@ -3,42 +3,42 @@
// platform's VectorStore implementation; these are the engine-facing types.
/** What a knowledge-base file is, which decides how its text is extracted. */
-export type MediaKind = 'text' | 'pdf' | 'docx' | 'audio' | 'video' | 'image';
+export type MediaKind = 'text' | 'pdf' | 'docx' | 'audio' | 'video' | 'image'
/** A workspace: a named container scoping chat threads + a knowledge base. */
export interface Project {
- id: string;
- name: string;
- description: string;
- systemPrompt: string;
- icon?: string;
- createdAt: string;
- updatedAt: string;
+ id: string
+ name: string
+ description: string
+ systemPrompt: string
+ icon?: string
+ createdAt: string
+ updatedAt: string
}
/** A file added to a project's knowledge base. */
export interface RagDocument {
- id: number;
- projectId: string;
- name: string;
- path: string;
- size: number;
- kind: MediaKind;
- createdAt: string;
- enabled: boolean;
+ id: number
+ projectId: string
+ name: string
+ path: string
+ size: number
+ kind: MediaKind
+ createdAt: string
+ enabled: boolean
}
/** One ranked excerpt returned from retrieval. */
export interface RagSearchResult {
- docId: number;
- name: string;
- content: string;
- position: number;
- score: number;
+ docId: number
+ name: string
+ content: string
+ position: number
+ score: number
}
/** The result of a knowledge-base query: ranked excerpts for `query`. */
export interface SearchResult {
- chunks: RagSearchResult[];
- query: string;
+ chunks: RagSearchResult[]
+ query: string
}
diff --git a/packages/rag/src/vectorMath.ts b/packages/rag/src/vectorMath.ts
index 8d32f6b7..ee99e73f 100644
--- a/packages/rag/src/vectorMath.ts
+++ b/packages/rag/src/vectorMath.ts
@@ -3,35 +3,39 @@
// for the brute-force search the RAG store does over a project's chunks.
export function dotProduct(a: number[], b: number[]): number {
- let dot = 0;
- const n = Math.min(a.length, b.length);
- for (let i = 0; i < n; i++) dot += a[i] * b[i];
- return dot;
+ let dot = 0
+ const n = Math.min(a.length, b.length)
+ for (let i = 0; i < n; i++) dot += a[i] * b[i]
+ return dot
}
export function cosineSimilarity(a: number[], b: number[]): number {
- let dot = 0;
- let normA = 0;
- let normB = 0;
- const n = Math.min(a.length, b.length);
+ let dot = 0
+ let normA = 0
+ let normB = 0
+ const n = Math.min(a.length, b.length)
for (let i = 0; i < n; i++) {
- dot += a[i] * b[i];
- normA += a[i] * a[i];
- normB += b[i] * b[i];
+ dot += a[i] * b[i]
+ normA += a[i] * a[i]
+ normB += b[i] * b[i]
}
- const denom = Math.sqrt(normA) * Math.sqrt(normB);
- return denom === 0 ? 0 : dot / denom;
+ const denom = Math.sqrt(normA) * Math.sqrt(normB)
+ return denom === 0 ? 0 : dot / denom
}
export interface SimilarityResult {
- index: number;
- score: number;
+ index: number
+ score: number
}
/** Top-k most similar candidate vectors to `query`, score-descending. */
-export function topKSimilar(query: number[], candidates: number[][], k: number): SimilarityResult[] {
+export function topKSimilar(
+ query: number[],
+ candidates: number[][],
+ k: number
+): SimilarityResult[] {
return candidates
.map((c, index) => ({ index, score: cosineSimilarity(query, c) }))
.sort((a, b) => b.score - a.score)
- .slice(0, k);
+ .slice(0, k)
}
diff --git a/playwright.config.ts b/playwright.config.ts
index fd2315ee..0680b43b 100644
--- a/playwright.config.ts
+++ b/playwright.config.ts
@@ -1,4 +1,4 @@
-import { defineConfig } from '@playwright/test';
+import { defineConfig } from '@playwright/test'
// Electron E2E. We drive the real app via Playwright's Electron support and read
// the renderer DOM directly (no OCR needed — it's a Chromium page). Single worker:
@@ -9,5 +9,5 @@ export default defineConfig({
expect: { timeout: 15_000 },
fullyParallel: false,
workers: 1,
- reporter: 'list',
-});
+ reporter: 'list'
+})
diff --git a/pr-targets.local.md b/pr-targets.local.md
index cf4cf425..4ec7f6b1 100644
--- a/pr-targets.local.md
+++ b/pr-targets.local.md
@@ -108,3 +108,33 @@ altstackHQ/altstack-data, AmineDjeghri/awesome-os-setup, Axorax/awesome-free-app
open-saas-directory/awesome-native-macosx-apps, momenbasel/awesome-mac-mini-homeserver, linsa-io/macos-apps,
EndoTheDev/Awesome-Ollama, BrethofAI/awesome-local-ai, jaywcjlove/awesome-swift-macos-apps,
Danielskry/Awesome-RAG, hasura/awesome-RAG-tools, coree/awesome-rag
+
+## RAISED (batch 5, 2026-07-10)
+- [x] AlexMili/Awesome-MCP — https://github.com/AlexMili/Awesome-MCP/pull/150
+- [x] raullenchai/awesome-mlx — https://github.com/raullenchai/awesome-mlx/pull/17
+- [x] Axorax/awesome-free-apps — https://github.com/Axorax/awesome-free-apps/pull/189
+- [x] e2b-dev/awesome-ai-agents — https://github.com/e2b-dev/awesome-ai-agents/pull/1226
+- [x] linsa-io/macos-apps — https://github.com/linsa-io/macos-apps/pull/62
+
+## RAISED (batch 6, 2026-07-15)
+- [x] serhii-londar/open-source-mac-os-apps — https://github.com/serhii-londar/open-source-mac-os-apps/pull/1199
+- [x] phmullins/awesome-macos — https://github.com/phmullins/awesome-macos/pull/217
+- [x] tehtbl/awesome-note-taking — https://github.com/tehtbl/awesome-note-taking/pull/109
+- [x] vince-lam/awesome-local-llms — https://github.com/vince-lam/awesome-local-llms/issues/55 (issue-first)
+- [x] abordage/awesome-mcp — https://github.com/abordage/awesome-mcp/pull/73
+- [x] IrtezaAsadRizvi/ai-megalist — https://github.com/IrtezaAsadRizvi/ai-megalist/pull/19
+- [x] altstackHQ/altstack-data — https://github.com/altstackHQ/altstack-data/pull/28
+
+## REVIEW REPLIES HANDLED
+- aristoapp/awesome-second-brain#32 — CHANGES_REQUESTED (rokpiy): rewrote row to make AGPL open-core vs paid Pro split explicit; replied. (2026-07-15)
+
+## SKIPPED / BLOCKED (batch 5-6)
+- appcypher/awesome-mcp-servers — BLOCKED: GitHub refuses PR from alichherawalla (interaction/permission). Branch staged: alichherawalla:add-off-grid-ai-desktop. Manual-file later.
+- punkpeye/awesome-mcp-devtools — server-devtools/SDK list, no client section (points clients to awesome-mcp-clients, already raised #245).
+- open-saas-directory/awesome-native-macosx-apps — excludes Electron apps.
+- sindresorhus/awesome-whisper — requires 100+ stars (repo has 38). Recheck when repo passes 100.
+- slimhk45/awesome-obsidian-alternatives, arkydon/Awesome-desktop-notes — relevance <3 (not an Obsidian/notes app).
+- electron/apps — DEFERRED: needs a STABLE release + 20-day wait; we only ship betas (0.0.39-beta.x) today.
+
+## GATING SIGNAL
+Repo off-grid-ai/off-grid-ai-desktop is at ~38 stars. Star-gated lists (awesome-whisper 100+, likely others) are blocked until the star count rises. Factor this before targeting large curated lists.
diff --git a/resources/tts-worker.mjs b/resources/tts-worker.mjs
index c1289fb0..ce0b0825 100644
--- a/resources/tts-worker.mjs
+++ b/resources/tts-worker.mjs
@@ -11,68 +11,68 @@
// tts-worker.mjs voices -> prints JSON array of voice ids to stdout
// tts-worker.mjs speak -> reads text from stdin, writes WAV to
-import fs from 'node:fs';
+import fs from 'node:fs'
-const MODEL_ID = 'onnx-community/Kokoro-82M-v1.0-ONNX';
-const DEFAULT_VOICE = 'af_heart';
+const MODEL_ID = 'onnx-community/Kokoro-82M-v1.0-ONNX'
+const DEFAULT_VOICE = 'af_heart'
// kokoro-js' RawAudio.toWav() emits 32-bit IEEE-float WAV (format 3), which
// Chromium's /new Audio() refuses to decode — so playback is silent.
// Re-encode the float samples to 16-bit PCM, which plays everywhere.
function encodeWavPcm16(float32, sampleRate) {
- const n = float32.length;
- const buf = Buffer.alloc(44 + n * 2);
- buf.write('RIFF', 0);
- buf.writeUInt32LE(36 + n * 2, 4);
- buf.write('WAVE', 8);
- buf.write('fmt ', 12);
- buf.writeUInt32LE(16, 16);
- buf.writeUInt16LE(1, 20); // PCM
- buf.writeUInt16LE(1, 22); // mono
- buf.writeUInt32LE(sampleRate, 24);
- buf.writeUInt32LE(sampleRate * 2, 28); // byte rate
- buf.writeUInt16LE(2, 32); // block align
- buf.writeUInt16LE(16, 34); // bits per sample
- buf.write('data', 36);
- buf.writeUInt32LE(n * 2, 40);
- let off = 44;
+ const n = float32.length
+ const buf = Buffer.alloc(44 + n * 2)
+ buf.write('RIFF', 0)
+ buf.writeUInt32LE(36 + n * 2, 4)
+ buf.write('WAVE', 8)
+ buf.write('fmt ', 12)
+ buf.writeUInt32LE(16, 16)
+ buf.writeUInt16LE(1, 20) // PCM
+ buf.writeUInt16LE(1, 22) // mono
+ buf.writeUInt32LE(sampleRate, 24)
+ buf.writeUInt32LE(sampleRate * 2, 28) // byte rate
+ buf.writeUInt16LE(2, 32) // block align
+ buf.writeUInt16LE(16, 34) // bits per sample
+ buf.write('data', 36)
+ buf.writeUInt32LE(n * 2, 40)
+ let off = 44
for (let i = 0; i < n; i++) {
- const s = Math.max(-1, Math.min(1, float32[i]));
- buf.writeInt16LE((s < 0 ? s * 0x8000 : s * 0x7fff) | 0, off);
- off += 2;
+ const s = Math.max(-1, Math.min(1, float32[i]))
+ buf.writeInt16LE((s < 0 ? s * 0x8000 : s * 0x7fff) | 0, off)
+ off += 2
}
- return buf;
+ return buf
}
async function synthToFile(tts, text, voice, outPath) {
- const clean = (text || '').trim().slice(0, 2000);
- if (!clean) throw new Error('no text');
- const audio = await tts.generate(clean, { voice: voice || DEFAULT_VOICE });
- const samples = audio.audio || audio.data;
- const sr = audio.sampling_rate || audio.sampleRate || 24000;
- fs.writeFileSync(outPath, encodeWavPcm16(samples, sr));
+ const clean = (text || '').trim().slice(0, 2000)
+ if (!clean) throw new Error('no text')
+ const audio = await tts.generate(clean, { voice: voice || DEFAULT_VOICE })
+ const samples = audio.audio || audio.data
+ const sr = audio.sampling_rate || audio.sampleRate || 24000
+ fs.writeFileSync(outPath, encodeWavPcm16(samples, sr))
}
async function main() {
- const mode = process.argv[2];
- const { KokoroTTS } = await import('kokoro-js');
- const tts = await KokoroTTS.from_pretrained(MODEL_ID, { dtype: 'q8', device: 'cpu' });
+ const mode = process.argv[2]
+ const { KokoroTTS } = await import('kokoro-js')
+ const tts = await KokoroTTS.from_pretrained(MODEL_ID, { dtype: 'q8', device: 'cpu' })
if (mode === 'voices') {
- const voices = Object.keys(tts.voices || {});
- process.stdout.write(JSON.stringify(voices));
- return { persist: false };
+ const voices = Object.keys(tts.voices || {})
+ process.stdout.write(JSON.stringify(voices))
+ return { persist: false }
}
if (mode === 'speak') {
- const outPath = process.argv[3];
- const voice = process.argv[4] || DEFAULT_VOICE;
- if (!outPath) throw new Error('speak mode requires an output path');
- let text = '';
- process.stdin.setEncoding('utf8');
- for await (const chunk of process.stdin) text += chunk;
- await synthToFile(tts, text, voice, outPath);
- return { persist: false };
+ const outPath = process.argv[3]
+ const voice = process.argv[4] || DEFAULT_VOICE
+ if (!outPath) throw new Error('speak mode requires an output path')
+ let text = ''
+ process.stdin.setEncoding('utf8')
+ for await (const chunk of process.stdin) text += chunk
+ await synthToFile(tts, text, voice, outPath)
+ return { persist: false }
}
if (mode === 'serve') {
@@ -80,30 +80,36 @@ async function main() {
// request per line on stdin ({ id, text, voice, out }) and we reply with one
// JSON line per request ({ id, ok } or { id, error }). Stays alive until the
// parent kills us (the queue's evict), so the ~330MB model is warm across calls.
- process.stdout.write(JSON.stringify({ ready: true }) + '\n');
- let buf = '';
- process.stdin.setEncoding('utf8');
+ process.stdout.write(JSON.stringify({ ready: true }) + '\n')
+ let buf = ''
+ process.stdin.setEncoding('utf8')
for await (const chunk of process.stdin) {
- buf += chunk;
- let nl;
+ buf += chunk
+ let nl
while ((nl = buf.indexOf('\n')) >= 0) {
- const line = buf.slice(0, nl).trim();
- buf = buf.slice(nl + 1);
- if (!line) continue;
- let req;
- try { req = JSON.parse(line); } catch { continue; }
+ const line = buf.slice(0, nl).trim()
+ buf = buf.slice(nl + 1)
+ if (!line) continue
+ let req
try {
- await synthToFile(tts, req.text, req.voice, req.out);
- process.stdout.write(JSON.stringify({ id: req.id, ok: true }) + '\n');
+ req = JSON.parse(line)
+ } catch {
+ continue
+ }
+ try {
+ await synthToFile(tts, req.text, req.voice, req.out)
+ process.stdout.write(JSON.stringify({ id: req.id, ok: true }) + '\n')
} catch (e) {
- process.stdout.write(JSON.stringify({ id: req.id, error: String(e && e.message ? e.message : e) }) + '\n');
+ process.stdout.write(
+ JSON.stringify({ id: req.id, error: String(e && e.message ? e.message : e) }) + '\n'
+ )
}
}
}
- return { persist: true }; // stdin closed -> parent is done with us
+ return { persist: true } // stdin closed -> parent is done with us
}
- throw new Error(`unknown mode: ${String(mode)}`);
+ throw new Error(`unknown mode: ${String(mode)}`)
}
// onnxruntime-node crashes (SIGABRT, "mutex lock failed") inside its static
@@ -114,13 +120,17 @@ async function main() {
// tick to flush, then kill.
function hardExit() {
setTimeout(() => {
- try { process.kill(process.pid, 'SIGKILL'); } catch { process.exit(0); }
- }, 40);
+ try {
+ process.kill(process.pid, 'SIGKILL')
+ } catch {
+ process.exit(0)
+ }
+ }, 40)
}
main()
.then(() => hardExit())
.catch((e) => {
- process.stderr.write(String(e && e.stack ? e.stack : e));
- hardExit();
- });
+ process.stderr.write(String(e && e.stack ? e.stack : e))
+ hardExit()
+ })
diff --git a/scripts/blur-pro-shots.mjs b/scripts/blur-pro-shots.mjs
index 53318f9b..136b0f07 100644
--- a/scripts/blur-pro-shots.mjs
+++ b/scripts/blur-pro-shots.mjs
@@ -1,19 +1,21 @@
// Blurs the content body of the Pro screenshots (keeps sidebar nav + header strip
// sharp) so they show the Pro UI/feature set without exposing any real CRM data.
// node scripts/blur-pro-shots.mjs
-import sharp from 'sharp';
-import { readdirSync } from 'fs';
+import sharp from 'sharp'
+import { readdirSync } from 'fs'
-const DIR = 'pro/docs/screenshots';
-const SB = 256; // sidebar width (2x) — kept sharp (pro nav)
-const HDR = 120; // header strip height (2x) — kept sharp (screen title)
+const DIR = 'pro/docs/screenshots'
+const SB = 256 // sidebar width (2x) — kept sharp (pro nav)
+const HDR = 120 // header strip height (2x) — kept sharp (screen title)
for (const f of readdirSync(DIR).filter((f) => f.endsWith('.png'))) {
- const p = `${DIR}/${f}`;
- const m = await sharp(p).metadata();
- const region = { left: SB, top: HDR, width: m.width - SB, height: m.height - HDR };
- const blurred = await sharp(p).extract(region).blur(25).toBuffer();
- await sharp(p).composite([{ input: blurred, left: SB, top: HDR }]).toFile(`/tmp/blur-${f}`);
- console.log('✓', f);
+ const p = `${DIR}/${f}`
+ const m = await sharp(p).metadata()
+ const region = { left: SB, top: HDR, width: m.width - SB, height: m.height - HDR }
+ const blurred = await sharp(p).extract(region).blur(25).toBuffer()
+ await sharp(p)
+ .composite([{ input: blurred, left: SB, top: HDR }])
+ .toFile(`/tmp/blur-${f}`)
+ console.log('✓', f)
}
-console.log('done → /tmp/blur-*.png (review before overwriting originals)');
+console.log('done → /tmp/blur-*.png (review before overwriting originals)')
diff --git a/scripts/build-llama.sh b/scripts/build-llama.sh
index 4b2fb1a8..46e2e7f2 100755
--- a/scripts/build-llama.sh
+++ b/scripts/build-llama.sh
@@ -15,7 +15,7 @@ set -euo pipefail
LLAMA_REF="${LLAMA_REF:-b9838}" # gemma4/qwen35-capable build
TARGET="${MACOS_DEPLOYMENT_TARGET:-13.0}" # runs on macOS 13+
-ROOT="$(cd "$(dirname "$0")/.." && pwd)"
+ROOT="${OFFGRID_BUILD_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
DEST="$ROOT/resources/bin/llama"
WORK="$(mktemp -d)"
trap 'rm -rf "$WORK"' EXIT
@@ -42,7 +42,24 @@ BIN="$(find build -name llama-server -type f -perm -111 | head -1)"
# Gate: fail the build if the binary targets a newer macOS than we asked for.
MINOS="$(otool -l "$BIN" | awk '/LC_BUILD_VERSION/{f=1} f&&/minos/{print $2; exit}')"
echo "[build-llama] built llama-server minos=$MINOS (want <= $TARGET)"
-if [ "${MINOS%%.*}" -gt "${TARGET%%.*}" ]; then
+version_exceeds() {
+ awk -v built="$1" -v target="$2" 'BEGIN {
+ built_n = split(built, built_parts, ".")
+ target_n = split(target, target_parts, ".")
+ n = built_n > target_n ? built_n : target_n
+ for (i = 1; i <= n; i++) {
+ built_part = (i <= built_n ? built_parts[i] : 0) + 0
+ target_part = (i <= target_n ? target_parts[i] : 0) + 0
+ if (built_part > target_part) exit 0
+ if (built_part < target_part) exit 1
+ }
+ exit 1
+ }'
+}
+if [ -z "$MINOS" ]; then
+ echo "[build-llama] FATAL: built llama-server has no LC_BUILD_VERSION minos"; exit 1
+fi
+if version_exceeds "$MINOS" "$TARGET"; then
echo "[build-llama] FATAL: minos $MINOS exceeds target $TARGET — would break older macOS"; exit 1
fi
@@ -79,12 +96,14 @@ MISSING=""
for f in "$DEST"/llama-server "$DEST"/*.dylib; do
while IFS= read -r dep; do
name="${dep#@rpath/}"
- [ -e "$DEST/$name" ] || MISSING="$MISSING $name"
+ if [ ! -f "$DEST/$name" ] || [ -L "$DEST/$name" ]; then
+ MISSING="$MISSING $name"
+ fi
done < <(otool -L "$f" 2>/dev/null | awk '/@rpath\//{print $1}')
done
MISSING="$(echo "$MISSING" | tr ' ' '\n' | sort -u | tr '\n' ' ' | sed 's/^ *//')"
if [ -n "$MISSING" ]; then
- echo "[build-llama] FATAL: engine references @rpath libs NOT bundled: $MISSING"; exit 1
+ echo "[build-llama] FATAL: engine references @rpath libs missing or not staged as real files: $MISSING"; exit 1
fi
echo "[build-llama] done — single engine, minos=$MINOS, no foreign deps, all @rpath libs present"
diff --git a/scripts/build-mac-local.sh b/scripts/build-mac-local.sh
index ae622d3a..1b912a93 100755
--- a/scripts/build-mac-local.sh
+++ b/scripts/build-mac-local.sh
@@ -23,7 +23,8 @@ cd "$(dirname "$0")/.."
VERSION=$(node -p "require('./package.json').version")
TARGET="${1:-both}"
-export CSC_IDENTITY_AUTO_DISCOVERY=false # unsigned local builds
+export CSC_IDENTITY_AUTO_DISCOVERY=false # unsigned local builds
+export OFFGRID_ALLOW_UNSIGNED_ARTIFACT=1 # integrity is enforced; codesign is release-only
# Guard against the bug that broke the last release: the runtime binaries in
# resources/bin/ are stored in Git LFS. If they're still 131-byte pointer stubs,
@@ -48,7 +49,7 @@ build_core() {
OFFGRID_FORCE_CORE=1 npx electron-vite build
npx electron-builder --mac \
-c.mac.notarize=false \
- -c.productName="Off Grid AI" \
+ -c.productName="Off Grid AI Desktop" \
-c.appId="co.getoffgridai.desktop" \
-c.dmg.artifactName="OffGrid-core-\${version}.dmg" \
--publish never
@@ -59,7 +60,7 @@ build_pro() {
npx electron-vite build
npx electron-builder --mac \
-c.mac.notarize=false \
- -c.productName="Off Grid AI Pro" \
+ -c.productName="Off Grid AI Desktop" \
-c.appId="co.getoffgridai.desktop.pro" \
-c.dmg.artifactName="OffGrid-pro-\${version}.dmg" \
--publish never
diff --git a/scripts/cap-orbit.mjs b/scripts/cap-orbit.mjs
index e72817a4..47ab3979 100644
--- a/scripts/cap-orbit.mjs
+++ b/scripts/cap-orbit.mjs
@@ -1,28 +1,46 @@
-import { _electron as electron } from 'playwright';
-import { mkdtempSync } from 'fs';
-import { tmpdir } from 'os';
-import { join } from 'path';
-const wait = (ms) => new Promise((r) => setTimeout(r, ms));
-const profile = mkdtempSync(join(tmpdir(), 'oborbit-'));
-const app = await electron.launch({ args: ['.'], env: { ...process.env, OFFGRID_USER_DATA: profile, OFFGRID_PRO: '0' } });
-const win = await app.firstWindow();
-await win.waitForLoadState('domcontentloaded');
-await app.evaluate(({ BrowserWindow }) => { const w = BrowserWindow.getAllWindows()[0]; if (w) { w.setSize(1480, 940); w.center(); } });
-await wait(3500);
+import { _electron as electron } from '@playwright/test'
+import { mkdtempSync } from 'fs'
+import { tmpdir } from 'os'
+import { join, resolve } from 'path'
+const wait = (ms) => new Promise((r) => setTimeout(r, ms))
+const profile = mkdtempSync(join(tmpdir(), 'oborbit-'))
+const app = await electron.launch({
+ args: ['.'],
+ env: { ...process.env, OFFGRID_USER_DATA: profile, OFFGRID_PRO: '0' }
+})
+const win = await app.firstWindow()
+await win.waitForLoadState('domcontentloaded')
+await app.evaluate(({ BrowserWindow }) => {
+ const w = BrowserWindow.getAllWindows()[0]
+ if (w) {
+ w.setSize(1480, 940)
+ w.center()
+ }
+})
+await wait(3500)
// step 1 -> click Continue to reach the orbit (step 2)
-await win.getByRole('button', { name: /Continue/i }).first().click().catch(() => {});
-await wait(2500);
-await win.screenshot({ path: '/tmp/orbit-step2.png' });
+await win
+ .getByRole('button', { name: /Continue/i })
+ .first()
+ .click()
+ .catch(() => {})
+await wait(2500)
+await win.screenshot({ path: resolve('e2e/screenshots/orbit-step2.png') })
// measure orbit card spread
-const labels = ['Image', 'Vision', 'Chat', 'Projects', 'Speech', 'Voice'];
-const boxes = [];
+const labels = ['Image', 'Vision', 'Chat', 'Projects', 'Speech', 'Voice']
+const boxes = []
for (const l of labels) {
- const b = await win.getByText(l, { exact: true }).first().boundingBox().catch(() => null);
- if (b) boxes.push({ l, x: Math.round(b.x + b.width / 2), y: Math.round(b.y + b.height / 2) });
+ const b = await win
+ .getByText(l, { exact: true })
+ .first()
+ .boundingBox()
+ .catch(() => null)
+ if (b) boxes.push({ l, x: Math.round(b.x + b.width / 2), y: Math.round(b.y + b.height / 2) })
}
-let minD = Infinity;
-for (let i = 0; i < boxes.length; i++) for (let j = i + 1; j < boxes.length; j++)
- minD = Math.min(minD, Math.hypot(boxes[i].x - boxes[j].x, boxes[i].y - boxes[j].y));
-console.log('CARDS:', JSON.stringify(boxes));
-console.log('MIN GAP:', Math.round(minD), 'px');
-await app.close();
+let minD = Infinity
+for (let i = 0; i < boxes.length; i++)
+ for (let j = i + 1; j < boxes.length; j++)
+ minD = Math.min(minD, Math.hypot(boxes[i].x - boxes[j].x, boxes[i].y - boxes[j].y))
+console.log('CARDS:', JSON.stringify(boxes))
+console.log('MIN GAP:', Math.round(minD), 'px')
+await app.close()
diff --git a/scripts/download-mmproj.mjs b/scripts/download-mmproj.mjs
index d5555874..c926af03 100644
--- a/scripts/download-mmproj.mjs
+++ b/scripts/download-mmproj.mjs
@@ -1,67 +1,71 @@
-import fs from 'fs';
-import path from 'path';
-import { fileURLToPath } from 'url';
-import https from 'https';
+import fs from 'fs'
+import path from 'path'
+import { fileURLToPath } from 'url'
+import https from 'https'
-const __dirname = path.dirname(fileURLToPath(import.meta.url));
-const RESOURCES_DIR = path.join(__dirname, '../resources/models');
-const MODEL_URL = "https://huggingface.co/ggml-org/Qwen2.5-VL-3B-Instruct-GGUF/resolve/main/mmproj-Qwen2.5-VL-3B-Instruct-f16.gguf";
-const MODEL_FILENAME = "qwen2.5-vl-3b-instruct-mmproj-f16.gguf";
-const DEST_PATH = path.join(RESOURCES_DIR, MODEL_FILENAME);
+const __dirname = path.dirname(fileURLToPath(import.meta.url))
+const RESOURCES_DIR = path.join(__dirname, '../resources/models')
+const MODEL_URL =
+ 'https://huggingface.co/ggml-org/Qwen2.5-VL-3B-Instruct-GGUF/resolve/main/mmproj-Qwen2.5-VL-3B-Instruct-f16.gguf'
+const MODEL_FILENAME = 'qwen2.5-vl-3b-instruct-mmproj-f16.gguf'
+const DEST_PATH = path.join(RESOURCES_DIR, MODEL_FILENAME)
if (!fs.existsSync(RESOURCES_DIR)) {
- fs.mkdirSync(RESOURCES_DIR, { recursive: true });
+ fs.mkdirSync(RESOURCES_DIR, { recursive: true })
}
if (fs.existsSync(DEST_PATH)) {
- console.log(`Model already exists at ${DEST_PATH}`);
- process.exit(0);
+ console.log('Pinned vision projector is already installed.')
+ process.exit(0)
}
-console.log(`Downloading mmproj from ${MODEL_URL} to ${DEST_PATH}...`);
+console.log('Downloading pinned vision projector...')
-const file = fs.createWriteStream(DEST_PATH);
+const file = fs.createWriteStream(DEST_PATH)
-https.get(MODEL_URL, (response) => {
+https
+ .get(MODEL_URL, (response) => {
if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
- console.log(`Redirecting to ${response.headers.location}...`);
- https.get(response.headers.location, (redirectResponse) => {
- downloadStream(redirectResponse, file);
- });
- return;
+ console.log('Following the verified model host redirect...')
+ https.get(response.headers.location, (redirectResponse) => {
+ downloadStream(redirectResponse, file)
+ })
+ return
}
-
- downloadStream(response, file);
-}).on('error', (err) => {
- fs.unlink(DEST_PATH, () => {});
- console.error("Download failed:", err.message);
- process.exit(1);
-});
+ downloadStream(response, file)
+ })
+ .on('error', (err) => {
+ fs.unlink(DEST_PATH, () => {})
+ console.error('Download failed:', err.message)
+ process.exit(1)
+ })
function downloadStream(response, fileStream) {
- if (response.statusCode !== 200) {
- console.error(`Failed to download: HTTP Status ${response.statusCode}`);
- process.exit(1);
- }
+ if (response.statusCode !== 200) {
+ console.error(`Failed to download: HTTP Status ${response.statusCode}`)
+ process.exit(1)
+ }
- const totalSize = parseInt(response.headers['content-length'], 10);
- let downloaded = 0;
+ const totalSize = parseInt(response.headers['content-length'], 10)
+ let downloaded = 0
- response.pipe(fileStream);
+ response.pipe(fileStream)
- response.on('data', (chunk) => {
- downloaded += chunk.length;
- if (totalSize) {
- const percent = ((downloaded / totalSize) * 100).toFixed(2);
- process.stdout.write(`\rDownloading: ${percent}% (${(downloaded / 1024 / 1024).toFixed(2)} MB)`);
- } else {
- process.stdout.write(`\rDownloading: ${(downloaded / 1024 / 1024).toFixed(2)} MB`);
- }
- });
+ response.on('data', (chunk) => {
+ downloaded += chunk.length
+ if (totalSize) {
+ const percent = ((downloaded / totalSize) * 100).toFixed(2)
+ process.stdout.write(
+ `\rDownloading: ${percent}% (${(downloaded / 1024 / 1024).toFixed(2)} MB)`
+ )
+ } else {
+ process.stdout.write(`\rDownloading: ${(downloaded / 1024 / 1024).toFixed(2)} MB`)
+ }
+ })
- fileStream.on('finish', () => {
- fileStream.close();
- console.log("\nDownload complete!");
- });
+ fileStream.on('finish', () => {
+ fileStream.close()
+ console.log('\nDownload complete!')
+ })
}
diff --git a/scripts/download-model.mjs b/scripts/download-model.mjs
index 04e4d98c..9c990ee7 100644
--- a/scripts/download-model.mjs
+++ b/scripts/download-model.mjs
@@ -1,116 +1,119 @@
-import fs from 'fs';
-import path from 'path';
-import { fileURLToPath } from 'url';
-import https from 'https';
-import os from 'os';
-
-const __dirname = path.dirname(fileURLToPath(import.meta.url));
+import fs from 'fs'
+import path from 'path'
+import https from 'https'
+import os from 'os'
// Model storage location - user's app support directory
function getModelsDir() {
- const platform = process.platform;
- let appDataDir;
-
- if (platform === 'darwin') {
- appDataDir = path.join(os.homedir(), 'Library', 'Application Support', 'My Memories');
- } else if (platform === 'win32') {
- appDataDir = path.join(process.env.APPDATA || os.homedir(), 'My Memories');
- } else {
- appDataDir = path.join(os.homedir(), '.my-memories');
- }
-
- return path.join(appDataDir, 'models');
+ const platform = process.platform
+ let appDataDir
+
+ if (platform === 'darwin') {
+ appDataDir = path.join(os.homedir(), 'Library', 'Application Support', 'My Memories')
+ } else if (platform === 'win32') {
+ appDataDir = path.join(process.env.APPDATA || os.homedir(), 'My Memories')
+ } else {
+ appDataDir = path.join(os.homedir(), '.my-memories')
+ }
+
+ return path.join(appDataDir, 'models')
}
-const MODELS_DIR = getModelsDir();
+const MODELS_DIR = getModelsDir()
// Qwen3-VL-4B model files
const MODELS = [
- {
- name: 'Qwen3-VL-4B-Instruct-Q4_K_M.gguf',
- url: 'https://huggingface.co/bartowski/Qwen_Qwen3-VL-4B-Instruct-GGUF/resolve/main/Qwen_Qwen3-VL-4B-Instruct-Q4_K_M.gguf'
- },
- {
- name: 'mmproj-Qwen3VL-4B-Instruct-F16.gguf',
- url: 'https://huggingface.co/Qwen/Qwen3-VL-4B-Instruct-GGUF/resolve/main/mmproj-Qwen3VL-4B-Instruct-F16.gguf'
- }
-];
+ {
+ name: 'Qwen3-VL-4B-Instruct-Q4_K_M.gguf',
+ url: 'https://huggingface.co/bartowski/Qwen_Qwen3-VL-4B-Instruct-GGUF/resolve/main/Qwen_Qwen3-VL-4B-Instruct-Q4_K_M.gguf'
+ },
+ {
+ name: 'mmproj-Qwen3VL-4B-Instruct-F16.gguf',
+ url: 'https://huggingface.co/Qwen/Qwen3-VL-4B-Instruct-GGUF/resolve/main/mmproj-Qwen3VL-4B-Instruct-F16.gguf'
+ }
+]
async function downloadFile(url, destPath) {
- return new Promise((resolve, reject) => {
- console.log(`Downloading from ${url}...`);
-
- const file = fs.createWriteStream(destPath);
-
- const request = (redirectUrl) => {
- https.get(redirectUrl, (response) => {
- if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
- console.log(`Redirecting...`);
- request(response.headers.location);
- return;
- }
-
- if (response.statusCode !== 200) {
- fs.unlink(destPath, () => {});
- reject(new Error(`HTTP ${response.statusCode}`));
- return;
- }
-
- const totalSize = parseInt(response.headers['content-length'], 10);
- let downloaded = 0;
-
- response.pipe(file);
-
- response.on('data', (chunk) => {
- downloaded += chunk.length;
- if (totalSize) {
- const percent = ((downloaded / totalSize) * 100).toFixed(1);
- const mb = (downloaded / 1024 / 1024).toFixed(1);
- process.stdout.write(`\rProgress: ${percent}% (${mb} MB)`);
- }
- });
-
- file.on('finish', () => {
- file.close();
- console.log('\nDownload complete!');
- resolve();
- });
- }).on('error', (err) => {
- fs.unlink(destPath, () => {});
- reject(err);
- });
- };
-
- request(url);
- });
+ return new Promise((resolve, reject) => {
+ console.log(`Downloading from ${url}...`)
+
+ const file = fs.createWriteStream(destPath)
+
+ const request = (redirectUrl) => {
+ https
+ .get(redirectUrl, (response) => {
+ if (
+ response.statusCode >= 300 &&
+ response.statusCode < 400 &&
+ response.headers.location
+ ) {
+ console.log(`Redirecting...`)
+ request(response.headers.location)
+ return
+ }
+
+ if (response.statusCode !== 200) {
+ fs.unlink(destPath, () => {})
+ reject(new Error(`HTTP ${response.statusCode}`))
+ return
+ }
+
+ const totalSize = parseInt(response.headers['content-length'], 10)
+ let downloaded = 0
+
+ response.pipe(file)
+
+ response.on('data', (chunk) => {
+ downloaded += chunk.length
+ if (totalSize) {
+ const percent = ((downloaded / totalSize) * 100).toFixed(1)
+ const mb = (downloaded / 1024 / 1024).toFixed(1)
+ process.stdout.write(`\rProgress: ${percent}% (${mb} MB)`)
+ }
+ })
+
+ file.on('finish', () => {
+ file.close()
+ console.log('\nDownload complete!')
+ resolve()
+ })
+ })
+ .on('error', (err) => {
+ fs.unlink(destPath, () => {})
+ reject(err)
+ })
+ }
+
+ request(url)
+ })
}
async function main() {
- console.log(`Models directory: ${MODELS_DIR}`);
-
- if (!fs.existsSync(MODELS_DIR)) {
- fs.mkdirSync(MODELS_DIR, { recursive: true });
- console.log('Created models directory');
+ console.log(`Models directory: ${MODELS_DIR}`)
+
+ if (!fs.existsSync(MODELS_DIR)) {
+ fs.mkdirSync(MODELS_DIR, { recursive: true })
+ console.log('Created models directory')
+ }
+
+ for (const model of MODELS) {
+ const destPath = path.join(MODELS_DIR, model.name)
+
+ if (fs.existsSync(destPath)) {
+ console.log(`${model.name} already exists, skipping.`)
+ continue
}
-
- for (const model of MODELS) {
- const destPath = path.join(MODELS_DIR, model.name);
-
- if (fs.existsSync(destPath)) {
- console.log(`${model.name} already exists, skipping.`);
- continue;
- }
-
- console.log(`\nDownloading ${model.name}...`);
- try {
- await downloadFile(model.url, destPath);
- } catch (err) {
- console.error(`Failed to download ${model.name}:`, err.message);
- process.exit(1);
- }
+
+ console.log(`\nDownloading ${model.name}...`)
+ try {
+ await downloadFile(model.url, destPath)
+ } catch (err) {
+ console.error(`Failed to download ${model.name}:`, err.message)
+ process.exit(1)
}
-
- console.log('\nAll models ready!');
+ }
+
+ console.log('\nAll models ready!')
}
-main();
+main()
diff --git a/scripts/hooks/pre-push b/scripts/hooks/pre-push
index a62b9a7a..e8567822 100755
--- a/scripts/hooks/pre-push
+++ b/scripts/hooks/pre-push
@@ -17,10 +17,8 @@ if ! npm run --silent test:coverage; then
fi
echo "[pre-push] coverage gate passed."
-# Standing reminder (see CLAUDE.md "Pending hygiene adoption"). Non-blocking - just surfaces
-# the deferred gold-standard prettier + tight-eslint adoption before each push so it is not
-# forgotten. Remove this echo (and the CLAUDE.md section) once the adoption PR has landed.
-echo ""
-echo "[pre-push] TODO before merge: adopt the gold-standard prettier config (repo-wide reformat)"
-echo " and tighten eslint (complexity / max-lines / max-params / no-shadow). Deferred"
-echo " to its own PR - see CLAUDE.md 'Pending hygiene adoption'."
+# The gold-standard hygiene adoption has LANDED on this branch: prettier is applied repo-wide
+# (enforced by eslint's prettier/prettier rule) and the structural rules (curly / complexity /
+# max-lines / max-params / no-shadow …) are a warn ratchet in eslint.config.mjs. No standing
+# TODO here anymore — see CLAUDE.md "Code hygiene (adopted)". Tighten the ratchet to error as
+# the god-files decompose; never loosen it to pass.
diff --git a/scripts/lib/macos-artifact-integrity.d.mts b/scripts/lib/macos-artifact-integrity.d.mts
new file mode 100644
index 00000000..4dcd2848
--- /dev/null
+++ b/scripts/lib/macos-artifact-integrity.d.mts
@@ -0,0 +1,9 @@
+export const REQUIRED_MAC_BUNDLE_FILES: readonly string[]
+export const REQUIRED_EXECUTABLE_FILES: ReadonlySet
+
+export function verifyBundlePair(referenceBundle: string, candidateBundle: string): void
+export function verifyDmgArtifact(
+ dmgPath: string,
+ referenceBundle: string,
+ options?: { requireCodeSignature?: boolean }
+): Promise
diff --git a/scripts/lib/macos-artifact-integrity.mjs b/scripts/lib/macos-artifact-integrity.mjs
new file mode 100644
index 00000000..815043fd
--- /dev/null
+++ b/scripts/lib/macos-artifact-integrity.mjs
@@ -0,0 +1,218 @@
+import { execFile } from 'node:child_process'
+import { createHash } from 'node:crypto'
+import fs from 'node:fs'
+import os from 'node:os'
+import path from 'node:path'
+import { promisify } from 'node:util'
+
+const execFileAsync = promisify(execFile)
+
+export const REQUIRED_MAC_BUNDLE_FILES = Object.freeze([
+ 'Contents/Info.plist',
+ 'Contents/MacOS/Off Grid AI Desktop',
+ 'Contents/Frameworks/Electron Framework.framework/Versions/A/Electron Framework',
+ 'Contents/Resources/app.asar',
+ 'Contents/Resources/bin/llama/llama-server',
+ 'Contents/Resources/bin/meeting-recorder',
+ 'Contents/Resources/bin/dictation-hotkey'
+])
+
+export const REQUIRED_EXECUTABLE_FILES = new Set([
+ 'Contents/MacOS/Off Grid AI Desktop',
+ 'Contents/Frameworks/Electron Framework.framework/Versions/A/Electron Framework',
+ 'Contents/Resources/bin/llama/llama-server',
+ 'Contents/Resources/bin/meeting-recorder',
+ 'Contents/Resources/bin/dictation-hotkey'
+])
+
+const FORBIDDEN_PRIVATE_SEGMENTS = new Set(['.demo-profile', '.offgrid', '.claude', '.Codex'])
+
+function normalizeRelative(relative) {
+ return relative.split(path.sep).join('/')
+}
+
+function fileDigest(file) {
+ const hash = createHash('sha256')
+ const descriptor = fs.openSync(file, 'r')
+ const buffer = Buffer.allocUnsafe(1024 * 1024)
+
+ try {
+ let bytesRead
+ do {
+ bytesRead = fs.readSync(descriptor, buffer, 0, buffer.length, null)
+ if (bytesRead > 0) {
+ hash.update(buffer.subarray(0, bytesRead))
+ }
+ } while (bytesRead > 0)
+ } finally {
+ fs.closeSync(descriptor)
+ }
+
+ return hash.digest('hex')
+}
+
+function entryKind(stat) {
+ if (stat.isFile()) return 'file'
+ if (stat.isDirectory()) return 'directory'
+ if (stat.isSymbolicLink()) return 'symlink'
+ return 'other'
+}
+
+function bundleManifest(bundle) {
+ const manifest = new Map()
+
+ function visit(directory, relativeDirectory = '') {
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
+ const relative = normalizeRelative(path.join(relativeDirectory, entry.name))
+ const absolute = path.join(directory, entry.name)
+ const stat = fs.lstatSync(absolute)
+ const kind = entryKind(stat)
+
+ manifest.set(relative, {
+ kind,
+ size: kind === 'file' ? stat.size : undefined,
+ target: kind === 'symlink' ? fs.readlinkSync(absolute) : undefined
+ })
+
+ if (kind === 'directory') {
+ visit(absolute, relative)
+ }
+ }
+ }
+
+ visit(bundle)
+ return manifest
+}
+
+function assertBundleRoot(bundle, label) {
+ if (!fs.existsSync(bundle) || !fs.statSync(bundle).isDirectory()) {
+ throw new Error(`${label} bundle is not a directory: ${bundle}`)
+ }
+}
+
+function assertRequiredFiles(bundle, label) {
+ for (const relative of REQUIRED_MAC_BUNDLE_FILES) {
+ const file = path.join(bundle, relative)
+ if (!fs.existsSync(file) || !fs.statSync(file).isFile()) {
+ throw new Error(`${label} bundle is missing required file: ${relative}`)
+ }
+ if (REQUIRED_EXECUTABLE_FILES.has(relative) && (fs.statSync(file).mode & 0o111) === 0) {
+ throw new Error(`${label} bundle required file is not executable: ${relative}`)
+ }
+ }
+}
+
+function assertNoPrivateState(manifest, label) {
+ for (const relative of manifest.keys()) {
+ const privateSegment = relative
+ .split('/')
+ .find((segment) => FORBIDDEN_PRIVATE_SEGMENTS.has(segment))
+ if (privateSegment) {
+ throw new Error(`${label} bundle contains forbidden private state: ${relative}`)
+ }
+ }
+}
+
+function compareManifests(reference, candidate) {
+ const missing = [...reference.keys()].filter((relative) => !candidate.has(relative))
+ const extra = [...candidate.keys()].filter((relative) => !reference.has(relative))
+ const changed = [...reference.entries()]
+ .filter(([relative, expected]) => {
+ const actual = candidate.get(relative)
+ return (
+ actual &&
+ (actual.kind !== expected.kind ||
+ actual.size !== expected.size ||
+ actual.target !== expected.target)
+ )
+ })
+ .map(([relative]) => relative)
+
+ if (missing.length || extra.length || changed.length) {
+ const details = [
+ ...missing.map((relative) => `missing: ${relative}`),
+ ...extra.map((relative) => `extra: ${relative}`),
+ ...changed.map((relative) => `changed: ${relative}`)
+ ]
+ throw new Error(`candidate bundle differs from packaged bundle:\n${details.join('\n')}`)
+ }
+}
+
+function compareRequiredDigests(referenceBundle, candidateBundle) {
+ for (const relative of REQUIRED_MAC_BUNDLE_FILES) {
+ const referenceDigest = fileDigest(path.join(referenceBundle, relative))
+ const candidateDigest = fileDigest(path.join(candidateBundle, relative))
+ if (referenceDigest !== candidateDigest) {
+ throw new Error(`candidate bundle required file content differs: ${relative}`)
+ }
+ }
+}
+
+export function verifyBundlePair(referenceBundle, candidateBundle) {
+ assertBundleRoot(referenceBundle, 'packaged')
+ assertBundleRoot(candidateBundle, 'candidate')
+ assertRequiredFiles(referenceBundle, 'packaged')
+ assertRequiredFiles(candidateBundle, 'candidate')
+
+ const referenceManifest = bundleManifest(referenceBundle)
+ const candidateManifest = bundleManifest(candidateBundle)
+ assertNoPrivateState(referenceManifest, 'packaged')
+ assertNoPrivateState(candidateManifest, 'candidate')
+ compareManifests(referenceManifest, candidateManifest)
+ compareRequiredDigests(referenceBundle, candidateBundle)
+}
+
+function findSingleApp(mountPoint) {
+ const apps = fs
+ .readdirSync(mountPoint, { withFileTypes: true })
+ .filter((entry) => entry.isDirectory() && entry.name.endsWith('.app'))
+ .map((entry) => path.join(mountPoint, entry.name))
+
+ if (apps.length !== 1) {
+ throw new Error(`expected exactly one app bundle in DMG, found ${apps.length}`)
+ }
+ return apps[0]
+}
+
+export async function verifyDmgArtifact(
+ dmgPath,
+ referenceBundle,
+ { requireCodeSignature = true } = {}
+) {
+ if (process.platform !== 'darwin') {
+ throw new Error('DMG integrity verification requires macOS')
+ }
+ if (!fs.existsSync(dmgPath) || !fs.statSync(dmgPath).isFile()) {
+ throw new Error(`DMG artifact does not exist: ${dmgPath}`)
+ }
+
+ const workRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'offgrid-dmg-integrity-'))
+ const mountPoint = path.join(workRoot, 'mount')
+ fs.mkdirSync(mountPoint)
+ let attached = false
+
+ try {
+ await execFileAsync('/usr/bin/hdiutil', [
+ 'attach',
+ dmgPath,
+ '-readonly',
+ '-nobrowse',
+ '-mountpoint',
+ mountPoint
+ ])
+ attached = true
+ const candidateBundle = findSingleApp(mountPoint)
+ verifyBundlePair(referenceBundle, candidateBundle)
+ if (requireCodeSignature) {
+ await execFileAsync('/usr/bin/codesign', ['--verify', '--deep', '--strict', referenceBundle])
+ await execFileAsync('/usr/bin/codesign', ['--verify', '--deep', '--strict', candidateBundle])
+ }
+ } finally {
+ if (attached) {
+ await execFileAsync('/usr/bin/hdiutil', ['detach', mountPoint, '-force']).catch(
+ () => undefined
+ )
+ }
+ fs.rmSync(workRoot, { recursive: true, force: true })
+ }
+}
diff --git a/scripts/notify-slack-release.mjs b/scripts/notify-slack-release.mjs
index 9838f3ee..ca5e85b8 100644
--- a/scripts/notify-slack-release.mjs
+++ b/scripts/notify-slack-release.mjs
@@ -17,46 +17,58 @@
// CHANNEL_LABEL "beta" | "stable" (optional, shown as a tag)
// NOTES_FILE (default release-notes.md)
// RELEASE_URL (optional; default derived from GITHUB_SERVER_URL/GITHUB_REPOSITORY + tag)
-import { readFileSync } from 'node:fs';
+import { readFileSync } from 'node:fs'
-const warn = (m) => console.warn(`[slack-release] ${m}`);
+const warn = (m) => console.warn(`[slack-release] ${m}`)
-const webhook = process.env.SLACK_WEBHOOK_URL;
-const token = process.env.SLACK_BOT_TOKEN;
-if (!webhook && !token) { warn('no SLACK_WEBHOOK_URL or SLACK_BOT_TOKEN set — skipping announcement (no-op).'); process.exit(0); }
+const webhook = process.env.SLACK_WEBHOOK_URL
+const token = process.env.SLACK_BOT_TOKEN
+if (!webhook && !token) {
+ warn('no SLACK_WEBHOOK_URL or SLACK_BOT_TOKEN set — skipping announcement (no-op).')
+ process.exit(0)
+}
-const channel = process.env.SLACK_CHANNEL || 'C0AFARY80HJ';
-const product = process.env.PRODUCT || 'Off Grid AI';
-const version = process.env.VERSION || '';
-const label = (process.env.CHANNEL_LABEL || '').trim();
-const notesFile = process.env.NOTES_FILE || 'release-notes.md';
+const channel = process.env.SLACK_CHANNEL || 'C0AFARY80HJ'
+const product = process.env.PRODUCT || 'Off Grid AI'
+const version = process.env.VERSION || ''
+const label = (process.env.CHANNEL_LABEL || '').trim()
+const notesFile = process.env.NOTES_FILE || 'release-notes.md'
-const server = process.env.GITHUB_SERVER_URL || 'https://github.com';
-const repo = process.env.GITHUB_REPOSITORY || '';
-const releaseUrl = process.env.RELEASE_URL || (repo && version ? `${server}/${repo}/releases/tag/v${version}` : '');
+const server = process.env.GITHUB_SERVER_URL || 'https://github.com'
+const repo = process.env.GITHUB_REPOSITORY || ''
+const releaseUrl =
+ process.env.RELEASE_URL || (repo && version ? `${server}/${repo}/releases/tag/v${version}` : '')
// Slack mrkdwn reserves & < > — a raw commit subject containing them (release notes are raw
// commit subjects) would misrender or be read as a . Escape the note body only;
// NOT the intentional link line below.
-const esc = (s) => s.replace(/&/g, '&').replace(//g, '>');
+const esc = (s) => s.replace(/&/g, '&').replace(//g, '>')
-let notes = '';
-try { notes = readFileSync(notesFile, 'utf8').trim(); } catch { /* notes optional */ }
+let notes = ''
+try {
+ notes = readFileSync(notesFile, 'utf8').trim()
+} catch {
+ /* notes optional */
+}
// Slack section text caps at 3000 chars; keep well under and never dump a wall.
-if (notes.length > 2600) { notes = `${notes.slice(0, 2600)}\n…`; }
+if (notes.length > 2600) {
+ notes = `${notes.slice(0, 2600)}\n…`
+}
-const tag = label ? ` \`${label}\`` : '';
-const header = `:package: *${esc(product)}* \`${version || 'release'}\`${tag}`;
-const linkLine = releaseUrl ? `<${releaseUrl}|Download / release page>` : '';
-const body = notes ? esc(notes) : '_No release notes generated for this build._';
+const tag = label ? ` \`${label}\`` : ''
+const header = `:package: *${esc(product)}* \`${version || 'release'}\`${tag}`
+const linkLine = releaseUrl ? `<${releaseUrl}|Download / release page>` : ''
+const body = notes ? esc(notes) : '_No release notes generated for this build._'
const blocks = [
{ type: 'section', text: { type: 'mrkdwn', text: header } },
- { type: 'section', text: { type: 'mrkdwn', text: body } },
-];
-if (linkLine) { blocks.push({ type: 'context', elements: [{ type: 'mrkdwn', text: linkLine }] }); }
+ { type: 'section', text: { type: 'mrkdwn', text: body } }
+]
+if (linkLine) {
+ blocks.push({ type: 'context', elements: [{ type: 'mrkdwn', text: linkLine }] })
+}
-const fallbackText = `${product} ${version} released`;
+const fallbackText = `${product} ${version} released`
try {
if (webhook) {
// Incoming webhook: channel is fixed by the hook; POST the blocks, expect body "ok".
@@ -64,23 +76,32 @@ try {
method: 'POST',
headers: { 'Content-Type': 'application/json; charset=utf-8' },
body: JSON.stringify({ text: fallbackText, blocks, unfurl_links: false }),
- signal: AbortSignal.timeout(10000),
- });
- const t = await res.text().catch(() => '');
- if (!res.ok) { warn(`webhook not ok: HTTP ${res.status} ${t}`); process.exit(0); }
- console.log(`[slack-release] announced ${product} ${version} via webhook`);
+ signal: AbortSignal.timeout(10000)
+ })
+ await res.text().catch(() => '')
+ if (!res.ok) {
+ warn(`webhook not ok: HTTP ${res.status}`)
+ process.exit(0)
+ }
+ console.log(`[slack-release] announced ${product} ${version} via webhook`)
} else {
const res = await fetch('https://slack.com/api/chat.postMessage', {
method: 'POST',
- headers: { 'Content-Type': 'application/json; charset=utf-8', Authorization: `Bearer ${token}` },
+ headers: {
+ 'Content-Type': 'application/json; charset=utf-8',
+ Authorization: `Bearer ${token}`
+ },
body: JSON.stringify({ channel, text: fallbackText, blocks, unfurl_links: false }),
- signal: AbortSignal.timeout(10000),
- });
- const j = await res.json().catch(() => ({}));
- if (!res.ok || !j.ok) { warn(`chat.postMessage not ok: HTTP ${res.status} ${j.error || ''}`); process.exit(0); }
- console.log(`[slack-release] announced ${product} ${version} to ${channel} (ts=${j.ts})`);
+ signal: AbortSignal.timeout(10000)
+ })
+ const j = await res.json().catch(() => ({}))
+ if (!res.ok || !j.ok) {
+ warn(`chat.postMessage not ok: HTTP ${res.status}`)
+ process.exit(0)
+ }
+ console.log(`[slack-release] announced ${product} ${version} via Slack API`)
}
-} catch (e) {
- warn(`post failed: ${e?.message || e}`);
+} catch {
+ warn('post failed')
}
-process.exit(0);
+process.exit(0)
diff --git a/scripts/release-evidence-profile.mjs b/scripts/release-evidence-profile.mjs
new file mode 100644
index 00000000..b2a5675c
--- /dev/null
+++ b/scripts/release-evidence-profile.mjs
@@ -0,0 +1,51 @@
+/* eslint-disable @typescript-eslint/explicit-function-return-type -- JavaScript harness module */
+import { mkdtempSync, realpathSync, rmSync } from 'node:fs'
+import { tmpdir } from 'node:os'
+import { join, relative } from 'node:path'
+
+const PROTECTED_ENVIRONMENT_KEYS = [
+ 'OFFGRID_PRO',
+ 'OFFGRID_SEED',
+ 'OFFGRID_SEED_PRO',
+ 'OFFGRID_USER_DATA'
+]
+
+export function createEvidenceProfile(label) {
+ const safeLabel = label.replace(/[^a-z0-9-]/gi, '-').toLowerCase()
+ return mkdtempSync(join(realpathSync(tmpdir()), `offgrid-evidence-${safeLabel}-`))
+}
+
+function assertIsolatedProfile(profile) {
+ const temporaryRoot = realpathSync(tmpdir())
+ const resolvedProfile = realpathSync(profile)
+ const pathFromTemporaryRoot = relative(temporaryRoot, resolvedProfile)
+ if (
+ pathFromTemporaryRoot === '' ||
+ pathFromTemporaryRoot.startsWith('..') ||
+ pathFromTemporaryRoot.includes('/../')
+ ) {
+ throw new Error('Release evidence must use an isolated temporary profile')
+ }
+ return resolvedProfile
+}
+
+export function evidenceEnvironment({
+ profile,
+ pro = false,
+ seedCore = false,
+ seedPro = false,
+ extra = {}
+}) {
+ const environment = { ...process.env, ...extra }
+ for (const key of PROTECTED_ENVIRONMENT_KEYS) delete environment[key]
+
+ environment.OFFGRID_PRO = pro ? '1' : '0'
+ environment.OFFGRID_USER_DATA = assertIsolatedProfile(profile)
+ if (seedCore) environment.OFFGRID_SEED = '1'
+ if (seedPro) environment.OFFGRID_SEED_PRO = '1'
+ return environment
+}
+
+export function removeEvidenceProfile(profile) {
+ rmSync(assertIsolatedProfile(profile), { recursive: true, force: true })
+}
diff --git a/scripts/release-notes.mjs b/scripts/release-notes.mjs
index e2f03c76..7676c4a2 100644
--- a/scripts/release-notes.mjs
+++ b/scripts/release-notes.mjs
@@ -34,22 +34,24 @@ const range = prevTag ? `${prevTag}..${toRef}` : toRef
const raw = execFileSync(
'git',
['log', range, '--no-merges', '--invert-grep', '--grep=\\[skip ci\\]', '--pretty=format:%s\t%h'],
- { encoding: 'utf8' },
+ { encoding: 'utf8' }
).trim()
-const commits = raw ? raw.split('\n').map((l) => {
- const [subject, hash] = l.split('\t')
- return { subject, hash }
-}) : []
+const commits = raw
+ ? raw.split('\n').map((l) => {
+ const [subject, hash] = l.split('\t')
+ return { subject, hash }
+ })
+ : []
// Turn a conventional-commit subject into a plain sentence a user can read.
function humanize(subject) {
const m = subject.match(/^(\w+)(?:\(([^)]+)\))?!?:\s*(.+)$/)
let text = m ? m[3] : subject
text = text
- .replace(/\s*\(#\d+\)\s*$/, '') // drop trailing PR refs: "(#11)"
- .replace(/[—–]/g, ' - ') // em/en dash -> " - " (brand voice)
- .replace(/[‘’]/g, "'") // curly -> straight
+ .replace(/\s*\(#\d+\)\s*$/, '') // drop trailing PR refs: "(#11)"
+ .replace(/[—–]/g, ' - ') // em/en dash -> " - " (brand voice)
+ .replace(/[‘’]/g, "'") // curly -> straight
.replace(/[“”]/g, '"')
.replace(/\s+/g, ' ')
.trim()
@@ -91,7 +93,7 @@ if (hasHighlights) {
out.push('Maintenance release. No user-facing changes this build.', '')
}
-out.push('## What\'s Changed', '')
+out.push("## What's Changed", '')
if (commits.length) {
for (const c of commits) out.push(`- ${c.subject} (${c.hash})`)
} else {
diff --git a/scripts/repro-chat.mjs b/scripts/repro-chat.mjs
index fe0adcb6..195753d9 100644
--- a/scripts/repro-chat.mjs
+++ b/scripts/repro-chat.mjs
@@ -1,30 +1,46 @@
-import { _electron as electron } from 'playwright';
-const wait = (ms) => new Promise((r) => setTimeout(r, ms));
-const APP_BIN = process.env.APP_BIN;
+import { _electron as electron } from '@playwright/test'
+const wait = (ms) => new Promise((r) => setTimeout(r, ms))
+const APP_BIN = process.env.APP_BIN
const app = await electron.launch({
...(APP_BIN ? { executablePath: APP_BIN, args: [] } : { args: ['.'] }),
- env: { ...process.env, OFFGRID_PRO: '0', OFFGRID_USER_DATA: '/tmp/og-chatdiag' },
-});
-app.process().stdout.on('data', (d) => process.stdout.write('[main] ' + d));
-app.process().stderr.on('data', (d) => process.stdout.write('[err] ' + d));
-const win = await app.firstWindow();
-await win.waitForLoadState('domcontentloaded');
-await app.evaluate(({ BrowserWindow }) => { const w = BrowserWindow.getAllWindows()[0]; if (w) w.setSize(1480, 940); });
-await wait(3000);
+ env: { ...process.env, OFFGRID_PRO: '0', OFFGRID_USER_DATA: '/tmp/og-chatdiag' }
+})
+app.process().stdout.on('data', (d) => process.stdout.write('[main] ' + d))
+app.process().stderr.on('data', (d) => process.stdout.write('[err] ' + d))
+const win = await app.firstWindow()
+await win.waitForLoadState('domcontentloaded')
+await app.evaluate(({ BrowserWindow }) => {
+ const w = BrowserWindow.getAllWindows()[0]
+ if (w) w.setSize(1480, 940)
+})
+await wait(3000)
// skip onboarding if present
-for (let i = 0; i < 4; i++) { const b = win.getByRole('button', { name: /Continue|Start using Off Grid/i }).first(); if (await b.isVisible().catch(() => false)) { await b.click().catch(() => {}); await wait(1000); } else break; }
-await wait(1000);
+for (let i = 0; i < 4; i++) {
+ const b = win.getByRole('button', { name: /Continue|Start using Off Grid/i }).first()
+ if (await b.isVisible().catch(() => false)) {
+ await b.click().catch(() => {})
+ await wait(1000)
+ } else break
+}
+await wait(1000)
// go to Chat
-await win.getByRole('button', { name: 'Chat', exact: false }).first().click().catch(() => {});
-await wait(1500);
+await win
+ .getByRole('button', { name: 'Chat', exact: false })
+ .first()
+ .click()
+ .catch(() => {})
+await wait(1500)
// type + send
-const box = win.getByPlaceholder(/Ask anything/i).first();
-await box.click().catch(() => {});
-await box.fill('hey');
-await win.keyboard.press('Enter');
-console.log('>>> sent "hey", waiting for response...');
-await wait(45000);
-const txt = await win.locator('body').innerText().catch(() => '');
-const m = txt.match(/(Sorry, something went wrong[^\n]*|No response returned[^\n]*)/);
-console.log('>>> RESULT:', m ? m[1] : '(got a normal response)');
-await app.close();
+const box = win.getByPlaceholder(/Ask anything/i).first()
+await box.click().catch(() => {})
+await box.fill('hey')
+await win.keyboard.press('Enter')
+console.log('>>> sent "hey", waiting for response...')
+await wait(45000)
+const txt = await win
+ .locator('body')
+ .innerText()
+ .catch(() => '')
+const m = txt.match(/(Sorry, something went wrong[^\n]*|No response returned[^\n]*)/)
+console.log('>>> RESULT:', m ? m[1] : '(got a normal response)')
+await app.close()
diff --git a/scripts/resign.js b/scripts/resign.js
index 55139364..ad6d1d25 100644
--- a/scripts/resign.js
+++ b/scripts/resign.js
@@ -13,86 +13,110 @@
* version always signed ad-hoc, which silently clobbered the Developer ID
* signature and made every release Gatekeeper-blocked.
*/
-const { execSync } = require('child_process');
-const path = require('path');
-const fs = require('fs');
+const { execSync } = require('child_process')
+const path = require('path')
+const fs = require('fs')
function findDeveloperId() {
- try {
- const out = execSync('security find-identity -v -p codesigning', { encoding: 'utf8' });
- const m = out.match(/"(Developer ID Application:[^"]+)"/);
- return m ? m[1] : null;
- } catch {
- return null;
- }
+ try {
+ const out = execSync('security find-identity -v -p codesigning', { encoding: 'utf8' })
+ const m = out.match(/"(Developer ID Application:[^"]+)"/)
+ return m ? m[1] : null
+ } catch {
+ return null
+ }
}
exports.default = async function (context) {
- const { appOutDir, packager } = context;
- if (packager.platform.name !== 'mac') return;
+ const { appOutDir, packager } = context
+ if (packager.platform.name !== 'mac') return
- const appName = packager.appInfo.productFilename;
- const appPath = path.join(appOutDir, `${appName}.app`);
- const resourcesPath = path.join(appPath, 'Contents', 'Resources');
- const entitlements = path.join(__dirname, '..', 'build', 'entitlements.mac.plist');
- const hasEnt = fs.existsSync(entitlements);
+ const appName = packager.appInfo.productFilename
+ const appPath = path.join(appOutDir, `${appName}.app`)
+ const resourcesPath = path.join(appPath, 'Contents', 'Resources')
+ const entitlements = path.join(__dirname, '..', 'build', 'entitlements.mac.plist')
+ const hasEnt = fs.existsSync(entitlements)
- const devId = findDeveloperId();
- const identity = devId || '-'; // '-' = ad-hoc
- const runtime = devId ? '--options runtime' : '';
- console.log(devId
- ? `[resign] Developer ID found — signing + hardened runtime for notarization: ${devId}`
- : '[resign] No Developer ID — ad-hoc signing (local dev; not notarizable)');
+ const devId = findDeveloperId()
+ const identity = devId || '-' // '-' = ad-hoc
+ const runtime = devId ? '--options runtime' : ''
+ console.log(
+ devId
+ ? `[resign] Developer ID found — signing + hardened runtime for notarization: ${devId}`
+ : '[resign] No Developer ID — ad-hoc signing (local dev; not notarizable)'
+ )
- const sign = (filePath, withEntitlements = false) => {
- const ent = withEntitlements && hasEnt ? `--entitlements "${entitlements}"` : '';
- execSync(`codesign --force ${runtime} ${ent} --sign "${identity}" "${filePath}"`, { stdio: 'inherit' });
- };
+ const sign = (filePath, withEntitlements = false) => {
+ const ent = withEntitlements && hasEnt ? `--entitlements "${entitlements}"` : ''
+ execSync(`codesign --force ${runtime} ${ent} --sign "${identity}" "${filePath}"`, {
+ stdio: 'inherit'
+ })
+ }
- try {
- const binDir = path.join(resourcesPath, 'bin');
- if (fs.existsSync(binDir)) {
- const walk = (dir) => {
- for (const name of fs.readdirSync(dir)) {
- if (name.startsWith('.')) continue;
- const p = path.join(dir, name);
- const st = fs.statSync(p);
- if (st.isDirectory()) { walk(p); continue; }
- if (name.endsWith('.dylib')) { console.log(`[resign] dylib: ${name}`); sign(p); }
- }
- };
- walk(binDir); // dylibs first (dependencies before dependents)
- const walkExec = (dir) => {
- for (const name of fs.readdirSync(dir)) {
- if (name.startsWith('.')) continue;
- const p = path.join(dir, name);
- const st = fs.statSync(p);
- if (st.isDirectory()) { walkExec(p); continue; }
- if (name.endsWith('.dylib')) continue;
- try { fs.accessSync(p, fs.constants.X_OK); console.log(`[resign] bin: ${name}`); sign(p, true); } catch { /* not exec */ }
- }
- };
- walkExec(binDir);
+ try {
+ const binDir = path.join(resourcesPath, 'bin')
+ if (fs.existsSync(binDir)) {
+ const walk = (dir) => {
+ for (const name of fs.readdirSync(dir)) {
+ if (name.startsWith('.')) continue
+ const p = path.join(dir, name)
+ const st = fs.statSync(p)
+ if (st.isDirectory()) {
+ walk(p)
+ continue
+ }
+ if (name.endsWith('.dylib')) {
+ console.log(`[resign] dylib: ${name}`)
+ sign(p)
+ }
+ }
+ }
+ walk(binDir) // dylibs first (dependencies before dependents)
+ const walkExec = (dir) => {
+ for (const name of fs.readdirSync(dir)) {
+ if (name.startsWith('.')) continue
+ const p = path.join(dir, name)
+ const st = fs.statSync(p)
+ if (st.isDirectory()) {
+ walkExec(p)
+ continue
+ }
+ if (name.endsWith('.dylib')) continue
+ try {
+ fs.accessSync(p, fs.constants.X_OK)
+ console.log(`[resign] bin: ${name}`)
+ sign(p, true)
+ } catch {
+ /* not exec */
+ }
}
+ }
+ walkExec(binDir)
+ }
- const watcherPath = path.join(resourcesPath, 'watcher');
- if (fs.existsSync(watcherPath)) { console.log('[resign] watcher'); sign(watcherPath, true); }
+ const watcherPath = path.join(resourcesPath, 'watcher')
+ if (fs.existsSync(watcherPath)) {
+ console.log('[resign] watcher')
+ sign(watcherPath, true)
+ }
- // Re-seal the outer bundle. With a real identity we DON'T use --deep (that
- // would overwrite the framework/helper entitlements electron-builder set
- // correctly); a top-level re-sign re-establishes the seal over our changed
- // nested binaries. Ad-hoc dev keeps --deep for simplicity.
- console.log('[resign] re-sealing app bundle');
- if (devId) {
- const ent = hasEnt ? `--entitlements "${entitlements}"` : '';
- execSync(`codesign --force ${runtime} ${ent} --sign "${identity}" "${appPath}"`, { stdio: 'inherit' });
- } else {
- const ent = hasEnt ? `--entitlements "${entitlements}"` : '';
- execSync(`codesign --deep --force --sign - ${ent} "${appPath}"`, { stdio: 'inherit' });
- }
- console.log('[resign] done');
- } catch (error) {
- console.error('[resign] failed:', error);
- throw error;
+ // Re-seal the outer bundle. With a real identity we DON'T use --deep (that
+ // would overwrite the framework/helper entitlements electron-builder set
+ // correctly); a top-level re-sign re-establishes the seal over our changed
+ // nested binaries. Ad-hoc dev keeps --deep for simplicity.
+ console.log('[resign] re-sealing app bundle')
+ if (devId) {
+ const ent = hasEnt ? `--entitlements "${entitlements}"` : ''
+ execSync(`codesign --force ${runtime} ${ent} --sign "${identity}" "${appPath}"`, {
+ stdio: 'inherit'
+ })
+ } else {
+ const ent = hasEnt ? `--entitlements "${entitlements}"` : ''
+ execSync(`codesign --deep --force --sign - ${ent} "${appPath}"`, { stdio: 'inherit' })
}
-};
+ console.log('[resign] done')
+ } catch (error) {
+ console.error('[resign] failed:', error)
+ throw error
+ }
+}
diff --git a/scripts/screenshots-pro.mjs b/scripts/screenshots-pro.mjs
index 99351f0b..c9a19e31 100644
--- a/scripts/screenshots-pro.mjs
+++ b/scripts/screenshots-pro.mjs
@@ -1,98 +1,200 @@
+/* eslint-disable @typescript-eslint/explicit-function-return-type -- Playwright JavaScript harness */
// PRO screenshot harness — launches the built app with Pro ACTIVE and a SYNTHETIC
// demo seed in an ISOLATED profile (never the real DB), then captures each pro
// screen. Output: pro/docs/screenshots/ (private repo). No blur — data is fake.
// node scripts/screenshots-pro.mjs
-import { _electron as electron } from 'playwright';
-import { mkdirSync, rmSync } from 'fs';
-import { tmpdir } from 'os';
-import { join } from 'path';
+import { _electron as electron } from '@playwright/test'
+import { mkdirSync } from 'fs'
+import {
+ createEvidenceProfile,
+ evidenceEnvironment,
+ removeEvidenceProfile
+} from './release-evidence-profile.mjs'
-const OUT = 'pro/docs/screenshots';
-mkdirSync(OUT, { recursive: true });
-const PROFILE = join(tmpdir(), 'offgrid-pro-demo');
-try { rmSync(PROFILE, { recursive: true, force: true }); } catch { /* fresh */ }
+const OUT = 'pro/docs/screenshots'
+mkdirSync(OUT, { recursive: true })
+const PROFILE = createEvidenceProfile('pro-tour')
-const wait = (ms) => new Promise((r) => setTimeout(r, ms));
-const baseEnv = { ...process.env, OFFGRID_PRO: '1', OFFGRID_USER_DATA: PROFILE, OFFGRID_NO_SERVICES: '1' };
+const wait = (ms) => new Promise((r) => setTimeout(r, ms))
// Remove demo-only overlays before each shot: the setup nudge and the meeting
// recording indicator (auto-triggered; not meaningful in a static demo).
-const killNudge = (win) => win.evaluate(() => {
- for (const el of document.querySelectorAll('div')) {
- const t = el.textContent || '';
- if (el.className?.includes?.('fixed') && (t.includes('Finish setting up Off Grid Pro') || t.includes('click to stop'))) el.remove();
- }
-}).catch(() => {});
+const killNudge = (win) =>
+ win
+ .evaluate(() => {
+ for (const el of document.querySelectorAll('div')) {
+ const t = el.textContent || ''
+ if (
+ el.className?.includes?.('fixed') &&
+ (t.includes('Finish setting up Off Grid Pro') || t.includes('click to stop'))
+ )
+ el.remove()
+ }
+ })
+ .catch(() => {})
-async function launch(extraEnv = {}) {
- const app = await electron.launch({ args: ['.'], env: { ...baseEnv, ...extraEnv } });
- const win = await app.firstWindow();
- await win.waitForLoadState('domcontentloaded');
+async function launch({ seedCore = false, seedPro = false } = {}) {
+ const app = await electron.launch({
+ args: ['.'],
+ env: evidenceEnvironment({
+ profile: PROFILE,
+ pro: true,
+ seedCore,
+ seedPro,
+ extra: { OFFGRID_NO_SERVICES: '1' }
+ })
+ })
+ const win = await app.firstWindow()
+ await win.waitForLoadState('domcontentloaded')
await app.evaluate(({ BrowserWindow, screen }) => {
- const w = BrowserWindow.getAllWindows()[0];
- if (!w) return;
+ const w = BrowserWindow.getAllWindows()[0]
+ if (!w) return
// Capture at the largest available display for a roomy view of the bento.
- const displays = screen.getAllDisplays();
- const big = displays.reduce((a, b) => (b.workAreaSize.width > a.workAreaSize.width ? b : a), displays[0]);
- const { x, y, width, height } = big.workArea;
- w.setBounds({ x, y, width, height });
- });
- return { app, win };
+ const displays = screen.getAllDisplays()
+ const big = displays.reduce(
+ (a, b) => (b.workAreaSize.width > a.workAreaSize.width ? b : a),
+ displays[0]
+ )
+ const { x, y, width, height } = big.workArea
+ w.setBounds({ x, y, width, height })
+ })
+ return { app, win }
}
// Launch 1: seed the isolated DB (main process) + mark onboarding done, then quit.
-let { app, win } = await launch({ OFFGRID_SEED_PRO: '1' });
-await wait(6000); // let activateMain + seedProDemo finish
+let { app, win } = await launch({ seedCore: true, seedPro: true })
+await wait(6000) // let activateMain + seedProDemo finish
await win.evaluate(async () => {
- localStorage.setItem('onboarding_completed', 'true');
- localStorage.setItem('offgrid:disable-capture', '1'); // no live auto-record in the demo
- localStorage.setItem('og-theme', 'light'); // capture in light mode (uniform gallery)
+ localStorage.setItem('onboarding_completed', 'true')
+ localStorage.setItem('offgrid:disable-capture', '1') // no live auto-record in the demo
+ localStorage.setItem('og-theme', 'light') // capture in light mode (uniform gallery)
// The Notifications inbox loads from localStorage (useNotifications), not the DB,
// so seed synthetic approvals + to-dos here or the screen captures empty. Same
// fictional people/projects as the rest of the demo seed.
- const min = (m) => new Date(Date.now() - m * 60000).toISOString();
- localStorage.setItem('my-memories-notifications', JSON.stringify([
- { id: 'n1', type: 'approval', title: 'Reply to Sam Shafer re: pilot rollout', message: 'Off Grid drafted a reply about the v0.8.0 timeline. Approve to send.', timestamp: min(4), read: false, approvalId: 1 },
- { id: 'n2', type: 'todo', title: "Review Tom's fix for the llama.cpp UI regression", message: 'Pulled from your screen activity in the llama.cpp repo.', timestamp: min(38), read: false, actionId: 1 },
- { id: 'n3', type: 'approval', title: 'Create Linear issue: /v1/images edits endpoint', message: 'Add to the Gateway v1 milestone with the streaming acceptance criteria.', timestamp: min(72), read: false, approvalId: 2 },
- { id: 'n4', type: 'todo', title: 'Send Priya the BFSI compliance deck before Friday', message: 'And loop in Daniel on the Northwind metrics before the kickoff.', timestamp: min(96), read: true, actionId: 2 },
- { id: 'n5', type: 'todo', title: 'Have the privacy one-pager ready for Helio Labs', message: 'Extracted from your dictation about the pilot kickoff prep.', timestamp: min(140), read: false, actionId: 3 },
- { id: 'n6', type: 'info', title: 'Synced with Priya on the v0.8.0 cut', message: 'Day view updated with the latest timeline.', timestamp: min(220), read: true },
- ]));
+ const min = (m) => new Date(Date.now() - m * 60000).toISOString()
+ localStorage.setItem(
+ 'my-memories-notifications',
+ JSON.stringify([
+ {
+ id: 'n1',
+ type: 'approval',
+ title: 'Reply to Sam Shafer re: pilot rollout',
+ message: 'Off Grid drafted a reply about the v0.8.0 timeline. Approve to send.',
+ timestamp: min(4),
+ read: false,
+ approvalId: 1
+ },
+ {
+ id: 'n2',
+ type: 'todo',
+ title: "Review Tom's fix for the llama.cpp UI regression",
+ message: 'Pulled from your screen activity in the llama.cpp repo.',
+ timestamp: min(38),
+ read: false,
+ actionId: 1
+ },
+ {
+ id: 'n3',
+ type: 'approval',
+ title: 'Create Linear issue: /v1/images edits endpoint',
+ message: 'Add to the Gateway v1 milestone with the streaming acceptance criteria.',
+ timestamp: min(72),
+ read: false,
+ approvalId: 2
+ },
+ {
+ id: 'n4',
+ type: 'todo',
+ title: 'Send Priya the BFSI compliance deck before Friday',
+ message: 'And loop in Daniel on the Northwind metrics before the kickoff.',
+ timestamp: min(96),
+ read: true,
+ actionId: 2
+ },
+ {
+ id: 'n5',
+ type: 'todo',
+ title: 'Have the privacy one-pager ready for Helio Labs',
+ message: 'Extracted from your dictation about the pilot kickoff prep.',
+ timestamp: min(140),
+ read: false,
+ actionId: 3
+ },
+ {
+ id: 'n6',
+ type: 'info',
+ title: 'Synced with Priya on the v0.8.0 cut',
+ message: 'Day view updated with the latest timeline.',
+ timestamp: min(220),
+ read: true
+ }
+ ])
+ )
// Build the keyword search index over the seeded observations so Search has results.
- try { await window.api?.proInvoke?.('search:reindex'); } catch { /* model-less FTS still builds */ }
-});
-await wait(4000); // let the reindex finish
-await app.close();
+ try {
+ await window.api?.proInvoke?.('search:reindex')
+ } catch {
+ /* model-less FTS still builds */
+ }
+})
+await wait(4000) // let the reindex finish
+await app.close()
// Launch 2: into the shell with seeded data (seed flag set → no re-seed).
-({ app, win } = await launch());
-await wait(3500);
-const shot = async (name) => { await wait(1100); await killNudge(win); await wait(150); await win.screenshot({ path: `${OUT}/${name}.png` }); console.log('✓', name); };
+;({ app, win } = await launch())
+await wait(3500)
+const shot = async (name) => {
+ await wait(1100)
+ await killNudge(win)
+ await wait(150)
+ await win.screenshot({ path: `${OUT}/${name}.png` })
+ console.log('✓', name)
+}
const nav = async (label) => {
- try { await win.getByRole('button', { name: label, exact: false }).first().click({ timeout: 6000 }); await wait(1800); }
- catch (e) { console.error('nav fail:', label, e.message); }
-};
+ try {
+ await win.getByRole('button', { name: label, exact: false }).first().click({ timeout: 6000 })
+ await wait(1800)
+ } catch (e) {
+ console.error('nav fail:', label, e.message)
+ }
+}
-try { await win.getByRole('button', { name: 'Expand sidebar' }).click({ timeout: 4000 }); } catch { /* open */ }
-try { await win.getByRole('button', { name: 'Dismiss' }).click({ timeout: 3000 }); } catch { /* no nudge */ }
-await wait(600);
-await shot('00-launch');
+try {
+ await win.getByRole('button', { name: 'Expand sidebar' }).click({ timeout: 4000 })
+} catch {
+ /* open */
+}
+try {
+ await win.getByRole('button', { name: 'Dismiss' }).click({ timeout: 3000 })
+} catch {
+ /* no nudge */
+}
+await wait(600)
+await shot('00-launch')
for (const [label, file] of [
- ['Day', 'pro-day'], ['Reflect', 'pro-reflect'], ['Replay', 'pro-replay'],
- ['Meetings', 'pro-meetings'], ['Actions', 'pro-actions'], ['Entities', 'pro-entities'],
- ['Search', 'pro-search'], ['Notifications', 'pro-notifications'],
- ['Voice', 'pro-voice'], ['Clipboard', 'pro-clipboard'],
+ ['Day', 'pro-day'],
+ ['Reflect', 'pro-reflect'],
+ ['Replay', 'pro-replay'],
+ ['Meetings', 'pro-meetings'],
+ ['Actions', 'pro-actions'],
+ ['Entities', 'pro-entities'],
+ ['Search', 'pro-search'],
+ ['Notifications', 'pro-notifications'],
+ ['Voice', 'pro-voice'],
+ ['Clipboard', 'pro-clipboard']
]) {
- await nav(label);
+ await nav(label)
if (file === 'pro-search') {
try {
- const box = win.getByPlaceholder(/Search everything/i);
- await box.click({ timeout: 4000 });
- await box.fill('release notes');
- await wait(2500); // let FTS + semantic results land
- } catch (e) { console.error('search type', e.message); }
+ const box = win.getByPlaceholder(/Search everything/i)
+ await box.click({ timeout: 4000 })
+ await box.fill('release notes')
+ await wait(2500) // let FTS + semantic results land
+ } catch (e) {
+ console.error('search type', e.message)
+ }
}
- await shot(file);
+ await shot(file)
}
-await app.close();
-console.log('done →', OUT);
+await app.close()
+removeEvidenceProfile(PROFILE)
+console.log('done →', OUT)
diff --git a/scripts/screenshots.mjs b/scripts/screenshots.mjs
index 766537a2..f0ea25cf 100644
--- a/scripts/screenshots.mjs
+++ b/scripts/screenshots.mjs
@@ -1,46 +1,88 @@
+/* eslint-disable @typescript-eslint/explicit-function-return-type -- Playwright JavaScript harness */
// Screenshot harness: launches the built app (free/core), navigates each core
// screen, and saves PNGs to docs/screenshots/. Uses the seeded demo data.
// Re-launches between states (no reload — the SPA rewrites the URL).
// node scripts/screenshots.mjs
-import { _electron as electron } from 'playwright';
-import { mkdirSync } from 'fs';
-import { tmpdir } from 'os';
-import { join } from 'path';
+import { _electron as electron } from '@playwright/test'
+import { mkdirSync } from 'fs'
+import {
+ createEvidenceProfile,
+ evidenceEnvironment,
+ removeEvidenceProfile
+} from './release-evidence-profile.mjs'
-const OUT = 'docs/screenshots';
-mkdirSync(OUT, { recursive: true });
-const wait = (ms) => new Promise((r) => setTimeout(r, ms));
+const OUT = 'docs/screenshots'
+mkdirSync(OUT, { recursive: true })
+const wait = (ms) => new Promise((r) => setTimeout(r, ms))
-async function launch(extraEnv = {}) {
- const app = await electron.launch({ args: ['.'], env: { ...process.env, OFFGRID_PRO: '0', ...extraEnv } });
- const win = await app.firstWindow();
- await win.waitForLoadState('domcontentloaded');
- await app.evaluate(({ BrowserWindow }) => { const w = BrowserWindow.getAllWindows()[0]; if (w) { w.setSize(1480, 940); w.center(); } });
- await wait(2500);
- return { app, win };
+async function launch(profile, { seedCore = false } = {}) {
+ const app = await electron.launch({
+ args: ['.'],
+ env: evidenceEnvironment({ profile, seedCore })
+ })
+ const win = await app.firstWindow()
+ await win.waitForLoadState('domcontentloaded')
+ await app.evaluate(({ BrowserWindow }) => {
+ const w = BrowserWindow.getAllWindows()[0]
+ if (w) {
+ w.setSize(1480, 940)
+ w.center()
+ }
+ })
+ await wait(2500)
+ return { app, win }
+}
+const shot = async (win, name) => {
+ await wait(900)
+ await win.screenshot({ path: `${OUT}/${name}.png` })
+ console.log('✓', name)
}
-const shot = async (win, name) => { await wait(900); await win.screenshot({ path: `${OUT}/${name}.png` }); console.log('✓', name); };
const nav = async (win, label) => {
- try { await win.getByRole('button', { name: label, exact: false }).first().click({ timeout: 6000 }); await wait(1600); }
- catch (e) { console.error('nav fail:', label, e.message); }
-};
+ try {
+ await win.getByRole('button', { name: label, exact: false }).first().click({ timeout: 6000 })
+ await wait(1600)
+ } catch (e) {
+ console.error('nav fail:', label, e.message)
+ }
+}
// --- Onboarding (fresh temp profile → no persisted flag → onboarding shows) ---
-const freshProfile = join(tmpdir(), `offgrid-shots-${process.pid}`);
-let { app, win } = await launch({ OFFGRID_USER_DATA: freshProfile });
-await shot(win, '08-onboarding');
-await app.close();
+const onboardingProfile = createEvidenceProfile('core-onboarding')
+const tourProfile = createEvidenceProfile('core-tour')
+let { app, win } = await launch(onboardingProfile)
+await shot(win, '08-onboarding')
+await app.close()
-// --- Tour (default profile: already onboarded + seeded demo data) ---
-({ app, win } = await launch());
-try { await win.getByRole('button', { name: 'Expand sidebar' }).click({ timeout: 4000 }); } catch { /* open */ }
-await wait(800);
-await shot(win, '01-models'); // free default landing
-await nav(win, 'Chat'); await shot(win, '02-chat');
-await nav(win, 'Projects'); await shot(win, '03-projects');
-try { await win.getByRole('button', { name: 'Artifacts', exact: true }).first().click({ timeout: 4000 }); await wait(1500); await shot(win, '09-artifacts'); } catch (e) { console.error('artifacts tab', e.message); }
-await nav(win, 'Integrations'); await shot(win, '05-integrations');
-await nav(win, 'Gateway'); await shot(win, '06-gateway');
-await nav(win, 'Day'); await shot(win, '07-pro-upgrade'); // locked Pro tab → upgrade
-await app.close();
-console.log('done → docs/screenshots/');
+// --- Tour (isolated profile with synthetic demo data) ---
+;({ app, win } = await launch(tourProfile, { seedCore: true }))
+await win.evaluate(() => localStorage.setItem('onboarding_completed', 'true'))
+await app.close()
+;({ app, win } = await launch(tourProfile))
+try {
+ await win.getByRole('button', { name: 'Expand sidebar' }).click({ timeout: 4000 })
+} catch {
+ /* open */
+}
+await wait(800)
+await shot(win, '01-models') // free default landing
+await nav(win, 'Chat')
+await shot(win, '02-chat')
+await nav(win, 'Projects')
+await shot(win, '03-projects')
+try {
+ await win.getByRole('button', { name: 'Artifacts', exact: true }).first().click({ timeout: 4000 })
+ await wait(1500)
+ await shot(win, '09-artifacts')
+} catch (e) {
+ console.error('artifacts tab', e.message)
+}
+await nav(win, 'Integrations')
+await shot(win, '05-integrations')
+await nav(win, 'Gateway')
+await shot(win, '06-gateway')
+await nav(win, 'Day')
+await shot(win, '07-pro-upgrade') // locked Pro tab → upgrade
+await app.close()
+removeEvidenceProfile(onboardingProfile)
+removeEvidenceProfile(tourProfile)
+console.log('done → docs/screenshots/')
diff --git a/scripts/smoke-api.sh b/scripts/smoke-api.sh
index 699db1e5..5a3b4af7 100755
--- a/scripts/smoke-api.sh
+++ b/scripts/smoke-api.sh
@@ -15,17 +15,20 @@ GW="${OFFGRID_GATEWAY_URL:-http://127.0.0.1:7878}"
# predictable name and lets concurrent runs coexist. Cleaned up on any exit.
MODELS_JSON="$(mktemp -t smoke_models.XXXXXX)"
STREAM_TXT="$(mktemp -t smoke_stream.XXXXXX)"
-trap 'rm -f "$MODELS_JSON" "$STREAM_TXT"' EXIT
+TTS_WAV="$(mktemp -t smoke_tts.XXXXXX)"
+trap 'rm -f "$MODELS_JSON" "$STREAM_TXT" "$TTS_WAV"' EXIT
fail() { echo " FAIL: $1"; exit 1; }
ok() { echo " ok: $1"; }
echo "[smoke] gateway = $GW"
-# 0. Gateway reachable + model catalog (models-manager path)
+# 0. Gateway reachable + model catalog (models-manager path). Parse the body via
+# JSON.parse (NOT require() - require on a temp file with no .json extension parses it as
+# JS and throws on a JSON object, which silently broke this check).
curl -s -m 5 "$GW/v1/models" >"$MODELS_JSON" 2>&1 || fail "gateway not reachable - start the app first (npm run dev)"
-MODELS_JSON="$MODELS_JSON" node -e 'const j=require(process.env.MODELS_JSON);if(!j.data||!j.data.length)process.exit(1);console.log(" models:",j.data.map(m=>m.id).join(", "))' \
- < /dev/null 2>/dev/null || fail "/v1/models returned no models"
+MODELS_JSON="$MODELS_JSON" node -e 'const fs=require("fs");const j=JSON.parse(fs.readFileSync(process.env.MODELS_JSON,"utf8"));if(!j.data||!j.data.length)process.exit(1);console.log(" models:",j.data.map(m=>m.id).join(", "))' \
+ || fail "/v1/models returned no models"
ok "/v1/models"
# 1. Chat non-stream (model-server proxy + llm payload path)
@@ -45,6 +48,21 @@ curl -s -m 60 "$GW/v1/embeddings" -H 'Content-Type: application/json' -d '{"inpu
| node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{const v=JSON.parse(s).data?.[0]?.embedding;if(!Array.isArray(v)||!v.length)process.exit(1);console.log(" dims:",v.length)})' || fail "embeddings returned no vector"
ok "embeddings"
+# 3b. TTS (kokoro /v1/audio/speech). THE endpoint that shipped broken - a `spawn ENOTDIR`
+# from an asar-file cwd - while chat/stream/embeddings were all green. Assert a real WAV.
+curl -s -m 90 "$GW/v1/audio/speech" -H 'Content-Type: application/json' \
+ -d '{"input":"off grid","voice":"af_heart"}' -o "$TTS_WAV"
+{ [ "$(head -c 4 "$TTS_WAV" 2>/dev/null)" = "RIFF" ] && [ "$(wc -c < "$TTS_WAV")" -gt 1000 ]; } \
+ || fail "TTS (voice) did not return a real WAV - $(head -c 200 "$TTS_WAV")"
+ok "TTS (voice) -> WAV ($(wc -c < "$TTS_WAV" | tr -d ' ') bytes)"
+
+# 3c. STT (whisper /v1/audio/transcriptions) - round-trip the TTS WAV back to text, so one
+# step proves BOTH audio engines end-to-end (synth -> transcribe).
+curl -s -m 120 "$GW/v1/audio/transcriptions" -F "file=@$TTS_WAV;type=audio/wav" -F "model=whisper" \
+ | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{const t=JSON.parse(s).text;if(typeof t!=="string")process.exit(1);console.log(" transcript:",JSON.stringify(t).slice(0,60))})' \
+ || fail "STT returned no transcript"
+ok "STT (transcribe)"
+
# 4. Image generation (imagegen args/memory-guard/progress) - opt-in
if [ "${SMOKE_IMAGE:-0}" = "1" ]; then
curl -s -m 240 "$GW/v1/images/generations" -H 'Content-Type: application/json' \
diff --git a/scripts/smoke-dmg-install.sh b/scripts/smoke-dmg-install.sh
new file mode 100755
index 00000000..1ea9449c
--- /dev/null
+++ b/scripts/smoke-dmg-install.sh
@@ -0,0 +1,116 @@
+#!/usr/bin/env bash
+# Verify the install path users actually receive from a macOS DMG without writing to
+# /Applications. The image is mounted read-only, its single canonical app bundle is
+# copied with ditto into a throwaway install directory, the source image is detached,
+# and the existing packaged UI smoke runs against the copied bundle on a fresh
+# temporary profile.
+#
+# Usage:
+# npm run smoke:dmg -- dist/OffGrid-0.0.38.dmg
+#
+# Env:
+# DMG_REFERENCE_APP= Compare the DMG and installed copy against the
+# signed app bundle electron-builder packaged.
+# DMG_SMOKE_RUNNER= Override only the final launch/smoke boundary.
+# The runner receives APP=.
+# Set scripts/smoke-packaged.sh for the deeper
+# model/runtime smoke when its prerequisites exist.
+
+set -euo pipefail
+
+REPO_ROOT=$(cd "$(dirname "$0")/.." && pwd)
+DMG_PATH="${1:-${DMG:-}}"
+EXPECTED_APP_NAME="${EXPECTED_APP_NAME:-Off Grid AI Desktop.app}"
+SMOKE_RUNNER="${DMG_SMOKE_RUNNER:-}"
+REFERENCE_APP="${DMG_REFERENCE_APP:-}"
+
+if [ "$(uname -s)" != "Darwin" ]; then
+ echo "[dmg-smoke] macOS is required because DMG mounting uses hdiutil" >&2
+ exit 2
+fi
+if [ -z "$DMG_PATH" ] || [ ! -f "$DMG_PATH" ]; then
+ echo "[dmg-smoke] pass an existing DMG path as the first argument or DMG=" >&2
+ exit 2
+fi
+if [ -n "$SMOKE_RUNNER" ] && [ ! -f "$SMOKE_RUNNER" ]; then
+ echo "[dmg-smoke] smoke runner not found: $SMOKE_RUNNER" >&2
+ exit 2
+fi
+if [ -n "$REFERENCE_APP" ] && [ ! -d "$REFERENCE_APP" ]; then
+ echo "[dmg-smoke] reference app bundle not found: $REFERENCE_APP" >&2
+ exit 2
+fi
+
+WORK_ROOT=$(mktemp -d "${TMPDIR:-/tmp}/offgrid-dmg-install.XXXXXX")
+MOUNT_POINT="$WORK_ROOT/mount"
+INSTALL_ROOT="$WORK_ROOT/install"
+ATTACHED=0
+mkdir -p "$MOUNT_POINT" "$INSTALL_ROOT"
+
+cleanup() {
+ if [ "$ATTACHED" = 1 ]; then
+ hdiutil detach "$MOUNT_POINT" -force >/dev/null 2>&1 || true
+ fi
+ rm -rf "$WORK_ROOT"
+}
+trap cleanup EXIT
+
+echo "[dmg-smoke] mounting read-only: $DMG_PATH"
+hdiutil attach "$DMG_PATH" -readonly -nobrowse -mountpoint "$MOUNT_POINT" >/dev/null
+ATTACHED=1
+
+APPS=()
+while IFS= read -r app; do
+ APPS+=("$app")
+done < <(find "$MOUNT_POINT" -type d -name '*.app' -prune -print)
+
+if [ "${#APPS[@]}" -ne 1 ]; then
+ echo "[dmg-smoke] expected exactly one .app in the DMG, found ${#APPS[@]}" >&2
+ exit 1
+fi
+
+SOURCE_APP="${APPS[0]}"
+if [ "$(basename "$SOURCE_APP")" != "$EXPECTED_APP_NAME" ]; then
+ echo "[dmg-smoke] expected $EXPECTED_APP_NAME, found $(basename "$SOURCE_APP")" >&2
+ exit 1
+fi
+
+if [ -n "$REFERENCE_APP" ]; then
+ echo "[dmg-smoke] comparing mounted bundle with packaged reference"
+ node "$REPO_ROOT/scripts/verify-macos-bundle.mjs" "$REFERENCE_APP" "$SOURCE_APP"
+fi
+
+INSTALLED_APP="$INSTALL_ROOT/$EXPECTED_APP_NAME"
+echo "[dmg-smoke] copying with ditto to temporary install root"
+/usr/bin/ditto "$SOURCE_APP" "$INSTALLED_APP"
+
+if [ -n "$REFERENCE_APP" ]; then
+ echo "[dmg-smoke] comparing installed bundle with packaged reference"
+ node "$REPO_ROOT/scripts/verify-macos-bundle.mjs" "$REFERENCE_APP" "$INSTALLED_APP"
+fi
+
+INFO_PLIST="$INSTALLED_APP/Contents/Info.plist"
+if [ ! -f "$INFO_PLIST" ]; then
+ echo "[dmg-smoke] copied bundle is missing Contents/Info.plist" >&2
+ exit 1
+fi
+BUNDLE_EXECUTABLE=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleExecutable' "$INFO_PLIST" 2>/dev/null || true)
+if [ -z "$BUNDLE_EXECUTABLE" ] || [ ! -x "$INSTALLED_APP/Contents/MacOS/$BUNDLE_EXECUTABLE" ]; then
+ echo "[dmg-smoke] copied bundle has no executable CFBundleExecutable" >&2
+ exit 1
+fi
+
+# Detach before launch so success cannot depend on files remaining available from
+# the mounted image. This is the important difference from launching in-place.
+hdiutil detach "$MOUNT_POINT" >/dev/null
+ATTACHED=0
+
+echo "[dmg-smoke] detached source; running packaged UI smoke against copied app"
+if [ -n "$SMOKE_RUNNER" ]; then
+ APP="$INSTALLED_APP" OFFGRID_DMG_MOUNT_POINT="$MOUNT_POINT" bash "$SMOKE_RUNNER"
+else
+ APP_BIN="$INSTALLED_APP/Contents/MacOS/$BUNDLE_EXECUTABLE" \
+ OFFGRID_DMG_MOUNT_POINT="$MOUNT_POINT" \
+ node "$REPO_ROOT/scripts/smoke-test.mjs"
+fi
+echo "[dmg-smoke] installed-copy smoke passed"
diff --git a/scripts/smoke-packaged.sh b/scripts/smoke-packaged.sh
new file mode 100755
index 00000000..a870c1e7
--- /dev/null
+++ b/scripts/smoke-packaged.sh
@@ -0,0 +1,96 @@
+#!/usr/bin/env bash
+# PACKAGED smoke gate - the missing link that lets packaged-only bugs ship to users.
+#
+# tsc + unit tests + `npm run dev` ALL pass while the ASSEMBLED .app is broken: an
+# app.getAppPath()/asar lookup, a spawn cwd pointing inside the asar, a bundled asset the
+# code can't find at its packaged path. Two shipped to every user undetected - TTS
+# `spawn ENOTDIR` (asar-file cwd) and a blank menu-bar (icon.png resolved into the asar) -
+# on top of 3 prior engine failures. None of the pre-existing gates run the REAL bundle.
+#
+# This launches the built .app on a throwaway profile and asserts (a) the bundled assets
+# are where the code looks and (b) every engine actually works end-to-end over the gateway.
+# The TTS runtime probe also transitively proves resourceFile() resolves the packaged
+# Resources dir - the exact mechanism the tray icon depends on. Exits non-zero on any
+# failure, so it drops straight into release.yml before signing.
+#
+# npm run build:unpack # or point at a signed build with APP=...
+# npm run smoke:packaged
+#
+# Env: APP= (default: dist/mac-arm64/*.app)
+# OFFGRID_MODELS= (default: the real userData models dir, if present)
+# SMOKE_IMAGE=1 to include image generation (slow; needs an image model)
+set -uo pipefail
+cd "$(dirname "$0")/.."
+
+GW="http://127.0.0.1:7878"
+FAILED=0
+ok() { echo " ok: $1"; }
+bad() { echo " FAIL: $1"; FAILED=1; }
+
+# --- locate the built app -----------------------------------------------------
+APP="${APP:-$(ls -d dist/mac-arm64/*.app 2>/dev/null | head -1)}"
+if [ -z "${APP:-}" ] || [ ! -d "$APP" ]; then
+ echo "[packaged-smoke] no .app found - build one first (npm run build:unpack) or pass APP="; exit 2
+fi
+RES="$APP/Contents/Resources"
+BIN="$APP/Contents/MacOS/$(basename "$APP" .app)"
+echo "[packaged-smoke] app = $APP"
+
+# --- the port must be free (a running instance would make the bundle exit on the ------
+# single-instance lock and we'd test nothing) --------------------------------------
+if [ "$(curl -s -o /dev/null -w '%{http_code}' -m 2 "$GW/v1/models" 2>/dev/null)" = "200" ]; then
+ echo "[packaged-smoke] FAIL: something is already serving $GW - quit the running app first"; exit 2
+fi
+
+# --- static packaging assertions (catch missing/mislocated bundled assets) ------------
+echo "[packaged-smoke] static bundle assertions"
+[ -f "$RES/icon.png" ] && ok "icon.png unpacked in Resources (menu-bar icon)" || bad "icon.png MISSING from Resources"
+[ -f "$RES/tts-worker.mjs" ] && ok "tts-worker.mjs in Resources" || bad "tts-worker.mjs MISSING from Resources"
+[ -x "$RES/bin/llama/llama-server" ] && ok "llama-server bundled" || bad "llama-server MISSING"
+[ -x "$RES/bin/sd/sd-server" ] && ok "sd-server bundled" || bad "sd-server MISSING"
+[ "$FAILED" = 0 ] || { echo "[packaged-smoke] static assertions failed - not launching"; exit 1; }
+
+# --- launch the bundle (PRO=0: no screen capture) -------------------------------------
+# Use the given profile if set, else the real userData profile - it has a selected model,
+# which chat/embeddings need. A blank profile has models on disk but none ACTIVE, so those
+# probes would report "no models"; TTS (kokoro) is model-independent and works regardless.
+# CI note: seed OFFGRID_USER_DATA with an active model to exercise the chat probes there.
+TMPDIR_LOG="$(mktemp -d -t ogad-smoke)"
+APPLOG="$TMPDIR_LOG/app.log"
+# OFFGRID_USER_DATA is inherited from the caller's env if set (else the app uses the real
+# userData profile, which has an active model for the chat probes). PRO=0 = no screen capture.
+OFFGRID_PRO=0 "$BIN" >"$APPLOG" 2>&1 &
+LAUNCH_PID=$!
+cleanup() { kill "$LAUNCH_PID" 2>/dev/null; pkill -f "$APP/Contents/MacOS/" 2>/dev/null; rm -rf "$TMPDIR_LOG"; }
+trap cleanup EXIT
+
+echo "[packaged-smoke] waiting for the gateway (model load can take ~30s)…"
+UP=0
+for _ in $(seq 1 60); do
+ if [ "$(curl -s -o /dev/null -w '%{http_code}' -m 3 "$GW/v1/models" 2>/dev/null)" = "200" ]; then UP=1; break; fi
+ sleep 2
+done
+if [ "$UP" != 1 ]; then
+ echo "[packaged-smoke] FAIL: packaged app never brought up the gateway"; echo "--- app log tail ---"; tail -30 "$APPLOG"; exit 1
+fi
+ok "packaged app booted + gateway up"
+
+# Model readiness: /v1/models returns 200 before llama-server finishes loading the model
+# into memory, so a chat probe fired now returns an error, not a queued wait. Poll a minimal
+# chat until it actually yields content before running the probe suite.
+echo "[packaged-smoke] waiting for the chat model to finish loading…"
+READY=0
+for _ in $(seq 1 45); do
+ R=$(curl -s -m 30 "$GW/v1/chat/completions" -H 'Content-Type: application/json' \
+ -d '{"messages":[{"role":"user","content":"Reply with: ok"}],"max_tokens":8,"stream":false}' 2>/dev/null)
+ if echo "$R" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{try{const c=JSON.parse(s).choices;process.exit(c&&c[0]&&c[0].message&&typeof c[0].message.content==="string"?0:1)}catch{process.exit(1)}})'; then READY=1; break; fi
+ sleep 2
+done
+[ "$READY" = 1 ] && ok "chat model loaded" || { echo "[packaged-smoke] FAIL: chat model never became ready"; tail -20 "$APPLOG"; exit 1; }
+
+# --- runtime engine assertions (chat/stream/embeddings/TTS/STT [+image]) ---------------
+# TTS here is the load-bearing one: it exercises the exact spawn+resourceFile path that
+# shipped broken, and proves the packaged Resources resolver works (which the tray relies on).
+OFFGRID_GATEWAY_URL="$GW" SMOKE_IMAGE="${SMOKE_IMAGE:-0}" bash "$(dirname "$0")/smoke-api.sh" || FAILED=1
+
+if [ "$FAILED" = 0 ]; then echo "[packaged-smoke] ALL PASSED"; else echo "[packaged-smoke] FAILURES - see above"; exit 1; fi
diff --git a/scripts/smoke-test.mjs b/scripts/smoke-test.mjs
index 1aa1741d..aefeb3df 100644
--- a/scripts/smoke-test.mjs
+++ b/scripts/smoke-test.mjs
@@ -4,85 +4,119 @@
//
// APP_BIN="/Volumes/Off Grid AI 0.0.18-arm64/Off Grid AI.app/Contents/MacOS/Off Grid AI" \
// node scripts/smoke-test.mjs
-import { _electron as electron } from 'playwright';
-import { mkdtempSync } from 'fs';
-import { tmpdir } from 'os';
-import { join } from 'path';
+import { _electron as electron } from '@playwright/test'
+import { mkdtempSync } from 'fs'
+import { tmpdir } from 'os'
+import { join } from 'path'
// APP_BIN = a packaged app executable; unset = launch the local build (`electron .`).
-const APP_BIN = process.env.APP_BIN;
+const APP_BIN = process.env.APP_BIN
-const profile = mkdtempSync(join(tmpdir(), 'ogsmoke-'));
-const wait = (ms) => new Promise((r) => setTimeout(r, ms));
-let pass = 0, fail = 0;
-const ok = (name, cond) => { if (cond) { pass++; console.log(` ✓ ${name}`); } else { fail++; console.error(` ✗ ${name}`); } };
+const profile = mkdtempSync(join(tmpdir(), 'ogsmoke-'))
+const wait = (ms) => new Promise((r) => setTimeout(r, ms))
+let pass = 0,
+ fail = 0
+const ok = (name, cond) => {
+ if (cond) {
+ pass++
+ console.log(` ✓ ${name}`)
+ } else {
+ fail++
+ console.error(` ✗ ${name}`)
+ }
+}
-const launchOpts = APP_BIN
- ? { executablePath: APP_BIN, args: [] }
- : { args: ['.'] };
+const launchOpts = APP_BIN ? { executablePath: APP_BIN, args: [] } : { args: ['.'] }
const app = await electron.launch({
...launchOpts,
- env: { ...process.env, OFFGRID_USER_DATA: profile, OFFGRID_PRO: '0' },
-});
-const win = await app.firstWindow();
-await win.waitForLoadState('domcontentloaded');
-await app.evaluate(({ BrowserWindow }) => { const w = BrowserWindow.getAllWindows()[0]; if (w) { w.setSize(1480, 940); w.center(); } });
-await wait(3500);
+ env: { ...process.env, OFFGRID_USER_DATA: profile, OFFGRID_PRO: '0' }
+})
+const win = await app.firstWindow()
+await win.waitForLoadState('domcontentloaded')
+await app.evaluate(({ BrowserWindow }) => {
+ const w = BrowserWindow.getAllWindows()[0]
+ if (w) {
+ w.setSize(1480, 940)
+ w.center()
+ }
+})
+await wait(3500)
-const text = async () => (await win.locator('body').innerText().catch(() => '')) || '';
+const text = async () =>
+ (await win
+ .locator('body')
+ .innerText()
+ .catch(() => '')) || ''
try {
// 1) Onboarding shows on a fresh profile (step 1)
- ok('onboarding screen appears', /Continue|Every model|Private AI|Off Grid/i.test(await text()));
+ ok('onboarding screen appears', /Continue|Every model|Private AI|Off Grid/i.test(await text()))
// 2) Advance to the orbit step + assert the modality cards aren't collapsed.
// The orbit lives on step 2, so navigate there before measuring.
const clickContinue = async () => {
- const btn = win.getByRole('button', { name: /Continue|Start using Off Grid/i }).first();
- if (await btn.isVisible().catch(() => false)) { await btn.click().catch(() => {}); await wait(1400); return true; }
- return false;
- };
- await clickContinue(); // step 1 -> step 2 (orbit)
- const labels = ['Image', 'Vision', 'Chat', 'Projects', 'Speech', 'Voice'];
- const boxes = [];
- for (const l of labels) {
- const b = await win.getByText(l, { exact: true }).first().boundingBox().catch(() => null);
- if (b) boxes.push({ l, x: b.x + b.width / 2, y: b.y + b.height / 2 });
+ const btn = win.getByRole('button', { name: /Continue|Start using Off Grid/i }).first()
+ if (await btn.isVisible().catch(() => false)) {
+ await btn.click().catch(() => {})
+ await wait(1400)
+ return true
+ }
+ return false
}
- let minDist = Infinity;
- for (let i = 0; i < boxes.length; i++) for (let j = i + 1; j < boxes.length; j++) {
- minDist = Math.min(minDist, Math.hypot(boxes[i].x - boxes[j].x, boxes[i].y - boxes[j].y));
+ await clickContinue() // step 1 -> step 2 (orbit)
+ const labels = ['Image', 'Vision', 'Chat', 'Projects', 'Speech', 'Voice']
+ const boxes = []
+ for (const l of labels) {
+ const b = await win
+ .getByText(l, { exact: true })
+ .first()
+ .boundingBox()
+ .catch(() => null)
+ if (b) boxes.push({ l, x: b.x + b.width / 2, y: b.y + b.height / 2 })
}
- ok(`onboarding orbit cards spaced (${boxes.length} cards, min gap ${Math.round(minDist)}px > 40)`, boxes.length >= 4 && minDist > 40);
+ let minDist = Infinity
+ for (let i = 0; i < boxes.length; i++)
+ for (let j = i + 1; j < boxes.length; j++) {
+ minDist = Math.min(minDist, Math.hypot(boxes[i].x - boxes[j].x, boxes[i].y - boxes[j].y))
+ }
+ ok(
+ `onboarding orbit cards spaced (${boxes.length} cards, min gap ${Math.round(minDist)}px > 40)`,
+ boxes.length >= 4 && minDist > 40
+ )
// 3) Finish onboarding into the app
- for (let i = 0; i < 4; i++) { if (!(await clickContinue())) break; }
- await wait(1500);
- const appText = await text();
+ for (let i = 0; i < 4; i++) {
+ if (!(await clickContinue())) break
+ }
+ await wait(1500)
+ const appText = await text()
// 4) No hard "Setup Required" wall in the core build
- ok('no "Setup Required" wall (core build)', !/Setup Required/i.test(appText));
+ ok('no "Setup Required" wall (core build)', !/Setup Required/i.test(appText))
// 5) Lands in the app (Models is the free default)
- ok('lands on Models screen', /Models|Download models/i.test(appText));
+ ok('lands on Models screen', /Models|Download models/i.test(appText))
// 6) Core screens render
const nav = async (label, expect) => {
try {
- await win.getByRole('button', { name: label, exact: false }).first().click({ timeout: 5000 });
- await wait(1400);
- return expect.test(await text());
- } catch { return false; }
- };
- ok('Chat renders', await nav('Chat', /Start a conversation|Ask anything|New chat/i));
- ok('Projects renders', await nav('Projects', /Projects|New chat|knowledge/i));
- ok('Gateway renders', await nav('Gateway', /Gateway|127\.0\.0\.1:7878|OpenAI/i));
- ok('Integrations renders', await nav('Integrations', /Integrations|Connect a tool|Connect/i));
+ await win.getByRole('button', { name: label, exact: false }).first().click({ timeout: 5000 })
+ await wait(1400)
+ return expect.test(await text())
+ } catch {
+ return false
+ }
+ }
+ ok('Chat renders', await nav('Chat', /Start a conversation|Ask anything|New chat/i))
+ ok('Projects renders', await nav('Projects', /Projects|New chat|knowledge/i))
+ ok('Gateway renders', await nav('Gateway', /Gateway|127\.0\.0\.1:7878|OpenAI/i))
+ ok('Integrations renders', await nav('Integrations', /Integrations|Connect a tool|Connect/i))
} catch (e) {
- fail++; console.error(' ✗ exception:', e.message);
+ fail++
+ console.error(' ✗ exception:', e.message)
} finally {
- await app.close();
+ await app.close()
}
-console.log(`\nSMOKE: ${pass} passed, ${fail} failed`);
-process.exit(fail ? 1 : 0);
+console.log(`\nSMOKE: ${pass} passed, ${fail} failed`)
+process.exit(fail ? 1 : 0)
diff --git a/scripts/test-db.sh b/scripts/test-db.sh
index a1011fc9..1ec9b335 100755
--- a/scripts/test-db.sh
+++ b/scripts/test-db.sh
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
-# DB integration tests (src/main/__tests__/*.dbtest.ts) exercise the real data layer against
+# DB integration tests (core + pro *.dbtest.ts) exercise the real data layer against
# a temp SQLite DB. They need better-sqlite3-multiple-ciphers built for the TEST RUNNER's node
# ABI - but the app builds it for ELECTRON's ABI (via electron-builder install-app-deps). So:
# rebuild for node, run the tests, then ALWAYS restore Electron's build - even on failure or
@@ -29,4 +29,7 @@ echo "[test:db] rebuilding better-sqlite3-multiple-ciphers for node $(node -v)..
npm rebuild better-sqlite3-multiple-ciphers >/dev/null 2>&1
echo "[test:db] running DB integration tests..."
-npx vitest run --config vitest.db.config.ts "$@"
+# Native/model/UI DB journeys are intentionally serial. Parallel files compete for
+# process-wide engines, Electron module state, and timing-sensitive recorder owners,
+# which turns real integration coverage into suite-load flakes.
+npx vitest run --config vitest.db.config.ts --no-file-parallelism --maxWorkers=1 "$@"
diff --git a/scripts/test-vision.mjs b/scripts/test-vision.mjs
index 5371c39d..f696fc6f 100644
--- a/scripts/test-vision.mjs
+++ b/scripts/test-vision.mjs
@@ -1,125 +1,138 @@
+import { spawn } from 'child_process'
+import path from 'path'
+import fs from 'fs'
+import { fileURLToPath } from 'url'
-import { spawn } from "child_process";
-import path from "path";
-import fs from "fs";
-import { fileURLToPath } from "url";
-
-const __dirname = path.dirname(fileURLToPath(import.meta.url));
+const __dirname = path.dirname(fileURLToPath(import.meta.url))
// Configuration
-const resourcesDir = path.join(__dirname, "../resources");
-const binName = "llama-server";
-const serverPath = path.join(resourcesDir, "bin", binName);
-const modelPath = path.join(resourcesDir, "models", "qwen2.5-vl-3b-instruct-q4_k_m.gguf");
-const mmProjPath = path.join(resourcesDir, "models", "qwen2.5-vl-3b-instruct-mmproj-f16.gguf");
-const PORT = 8081; // Use a different port for testing to avoid conflict if app is running
+const resourcesDir = path.join(__dirname, '../resources')
+const binName = 'llama-server'
+const serverPath = path.join(resourcesDir, 'bin', binName)
+const modelPath = path.join(resourcesDir, 'models', 'qwen2.5-vl-3b-instruct-q4_k_m.gguf')
+const mmProjPath = path.join(resourcesDir, 'models', 'qwen2.5-vl-3b-instruct-mmproj-f16.gguf')
+const PORT = 8081 // Use a different port for testing to avoid conflict if app is running
async function run() {
- console.log("=== Testing Vision Model via llama-server ===");
-
- // 1. Validation
- if (!fs.existsSync(serverPath)) {
- console.error(`Binary not found at: ${serverPath}`);
- console.error("Please download llama-server and place it there.");
- process.exit(1);
- }
- if (!fs.existsSync(modelPath)) {
- console.error(`Model not found at: ${modelPath}`);
- process.exit(1);
- }
-
- console.log("Starting llama-server...");
-
- // 2. Spawn Server
- const server = spawn(serverPath, [
- "-m", modelPath,
- "--mmproj", mmProjPath,
- "--port", String(PORT),
- "--host", "127.0.0.1",
- "-c", "8192"
- ]);
-
- server.stderr.on("data", (d) => {
- console.log(`[Server] ${d.toString().trim()}`);
- });
-
- server.on("close", (code) => {
- console.log(`[Server] Exited with code ${code}`);
- });
-
-
- // Cleanup on exit
- process.on("exit", () => server.kill());
- process.on("SIGINT", () => {
- server.kill();
- process.exit();
- });
-
- // 3. Wait for Ready
- console.log("Waiting for server...");
- await waitForServer(PORT);
- console.log("Server Ready!");
-
- // 4. Find an image
- const captureDir = path.join(process.env.HOME || "", "Library/Application Support/your-memories/captures");
- let imagePath = "";
- if (fs.existsSync(captureDir)) {
- const files = fs.readdirSync(captureDir).filter(f => f.endsWith('.png')).sort().reverse();
- if (files.length > 0) {
- imagePath = path.join(captureDir, files[0]);
- console.log("Using recent capture:", imagePath);
- }
- }
-
- if (!imagePath) {
- console.error("No capture found. Please provide an image path or take a screenshot.");
- server.kill();
- process.exit(1);
- }
-
- // 5. Send Request
- console.log("Sending request...");
- try {
- const imageBuffer = fs.readFileSync(imagePath);
- const base64 = imageBuffer.toString("base64");
- const mime = "image/png";
-
- const response = await fetch(`http://127.0.0.1:${PORT}/v1/chat/completions`, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({
- messages: [{
- role: "user",
- content: [
- { type: "text", text: "Describe this image in detail." },
- { type: "image_url", image_url: { url: `data:${mime};base64,${base64}` } }
- ]
- }],
- max_tokens: 500
- })
- });
-
- const data = await response.json();
- console.log("\nResponse:\n", data.choices?.[0]?.message?.content);
-
- } catch (e) {
- console.error("Error during request:", e);
- } finally {
- console.log("Stopping server...");
- server.kill();
+ console.log('=== Testing Vision Model via llama-server ===')
+
+ // 1. Validation
+ if (!fs.existsSync(serverPath)) {
+ console.error(`Binary not found at: ${serverPath}`)
+ console.error('Please download llama-server and place it there.')
+ process.exit(1)
+ }
+ if (!fs.existsSync(modelPath)) {
+ console.error(`Model not found at: ${modelPath}`)
+ process.exit(1)
+ }
+
+ console.log('Starting llama-server...')
+
+ // 2. Spawn Server
+ const server = spawn(serverPath, [
+ '-m',
+ modelPath,
+ '--mmproj',
+ mmProjPath,
+ '--port',
+ String(PORT),
+ '--host',
+ '127.0.0.1',
+ '-c',
+ '8192'
+ ])
+
+ server.stderr.on('data', (d) => {
+ console.log(`[Server] ${d.toString().trim()}`)
+ })
+
+ server.on('close', (code) => {
+ console.log(`[Server] Exited with code ${code}`)
+ })
+
+ // Cleanup on exit
+ process.on('exit', () => server.kill())
+ process.on('SIGINT', () => {
+ server.kill()
+ process.exit()
+ })
+
+ // 3. Wait for Ready
+ console.log('Waiting for server...')
+ await waitForServer(PORT)
+ console.log('Server Ready!')
+
+ // 4. Find an image
+ const captureDir = path.join(
+ process.env.HOME || '',
+ 'Library/Application Support/your-memories/captures'
+ )
+ let imagePath = ''
+ if (fs.existsSync(captureDir)) {
+ const files = fs
+ .readdirSync(captureDir)
+ .filter((f) => f.endsWith('.png'))
+ .sort()
+ .reverse()
+ if (files.length > 0) {
+ imagePath = path.join(captureDir, files[0])
+ console.log('Using recent capture:', imagePath)
}
+ }
+
+ if (!imagePath) {
+ console.error('No capture found. Please provide an image path or take a screenshot.')
+ server.kill()
+ process.exit(1)
+ }
+
+ // 5. Send Request
+ console.log('Sending request...')
+ try {
+ const imageBuffer = fs.readFileSync(imagePath)
+ const base64 = imageBuffer.toString('base64')
+ const mime = 'image/png'
+
+ const response = await fetch(`http://127.0.0.1:${PORT}/v1/chat/completions`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ messages: [
+ {
+ role: 'user',
+ content: [
+ { type: 'text', text: 'Describe this image in detail.' },
+ { type: 'image_url', image_url: { url: `data:${mime};base64,${base64}` } }
+ ]
+ }
+ ],
+ max_tokens: 500
+ })
+ })
+
+ const data = await response.json()
+ console.log('\nResponse:\n', data.choices?.[0]?.message?.content)
+ } catch (e) {
+ console.error('Error during request:', e)
+ } finally {
+ console.log('Stopping server...')
+ server.kill()
+ }
}
async function waitForServer(port) {
- const start = Date.now();
- while (Date.now() - start < 60000) {
- try {
- const res = await fetch(`http://127.0.0.1:${port}/health`);
- if (res.ok) return;
- } catch {}
- await new Promise(r => setTimeout(r, 500));
+ const start = Date.now()
+ while (Date.now() - start < 60000) {
+ try {
+ const res = await fetch(`http://127.0.0.1:${port}/health`)
+ if (res.ok) return
+ } catch {
+ // Server is still starting.
}
- throw new Error("Timeout waiting for server");
+ await new Promise((r) => setTimeout(r, 500))
+ }
+ throw new Error('Timeout waiting for server')
}
-run();
+run()
diff --git a/scripts/verify-electron-builder-artifact.js b/scripts/verify-electron-builder-artifact.js
new file mode 100644
index 00000000..9168ccc8
--- /dev/null
+++ b/scripts/verify-electron-builder-artifact.js
@@ -0,0 +1,18 @@
+const path = require('node:path')
+
+exports.default = async function verifyElectronBuilderArtifact(event) {
+ if (!event.file.toLowerCase().endsWith('.dmg')) return
+
+ const { verifyDmgArtifact } = await import('./lib/macos-artifact-integrity.mjs')
+ const appOutDir = event.packager.computeAppOutDir(event.target.outDir, event.arch)
+ const referenceBundle = path.join(appOutDir, `${event.packager.appInfo.productFilename}.app`)
+ const requireCodeSignature = process.env.OFFGRID_ALLOW_UNSIGNED_ARTIFACT !== '1'
+
+ console.log(`[artifact-integrity] verifying ${path.basename(event.file)} before publish`)
+ await verifyDmgArtifact(event.file, referenceBundle, { requireCodeSignature })
+ console.log(
+ requireCodeSignature
+ ? '[artifact-integrity] DMG bundle matches signed packaged app'
+ : '[artifact-integrity] DMG bundle matches packaged app (local unsigned build)'
+ )
+}
diff --git a/scripts/verify-macos-bundle.mjs b/scripts/verify-macos-bundle.mjs
new file mode 100644
index 00000000..3f83933a
--- /dev/null
+++ b/scripts/verify-macos-bundle.mjs
@@ -0,0 +1,18 @@
+#!/usr/bin/env node
+
+import { verifyBundlePair } from './lib/macos-artifact-integrity.mjs'
+
+const [referenceBundle, candidateBundle] = process.argv.slice(2)
+
+if (!referenceBundle || !candidateBundle) {
+ console.error('usage: verify-macos-bundle.mjs ')
+ process.exit(2)
+}
+
+try {
+ verifyBundlePair(referenceBundle, candidateBundle)
+ console.log('[bundle-integrity] candidate matches packaged app bundle')
+} catch (error) {
+ console.error(`[bundle-integrity] ${error instanceof Error ? error.message : String(error)}`)
+ process.exit(1)
+}
diff --git a/sonar-project.properties b/sonar-project.properties
new file mode 100644
index 00000000..153b0ff2
--- /dev/null
+++ b/sonar-project.properties
@@ -0,0 +1,16 @@
+# SonarCloud config for Off Grid AI Desktop — Automatic Analysis mode.
+# SonarCloud clones this repo and analyzes it on each push/PR (no CI step, no token).
+# Project: https://sonarcloud.io/organizations/off-grid-ai (off-grid-ai_off-grid-ai-desktop).
+sonar.organization=off-grid-ai
+sonar.projectKey=off-grid-ai_off-grid-ai-desktop
+sonar.projectName=Off Grid AI Desktop
+
+# Scopes the automatic scan to CORE product source. Automatic Analysis only ever
+# clones THIS (public) repo, so it never sees pro/ anyway — pro is scanned locally
+# via eslint-plugin-sonarjs in the pro repo, not here. Coverage isn't imported in
+# Automatic Analysis; the vitest ratchet (vitest.config.ts + pre-push/CI) is the
+# real coverage gate.
+sonar.sources=src
+sonar.tests=src
+sonar.test.inclusions=**/*.test.ts,**/*.test.tsx,**/__tests__/**
+sonar.exclusions=**/node_modules/**,out/**,dist/**,e2e/**,resources/**,component-library-animations/**,pro/**,**/*.d.ts
diff --git a/src/bootstrap/globals.d.ts b/src/bootstrap/globals.d.ts
index 271a0305..7c0e60af 100644
--- a/src/bootstrap/globals.d.ts
+++ b/src/bootstrap/globals.d.ts
@@ -1,4 +1,4 @@
// Build-time flag injected by electron.vite.config.ts (define). True only when
// the private pro/ submodule is present at build time (pro build); false in the
// free/open build. Read by preload (isPro) and the main pro loader.
-declare const __OFFGRID_PRO__: boolean;
+declare const __OFFGRID_PRO__: boolean
diff --git a/src/bootstrap/proStub.ts b/src/bootstrap/proStub.ts
index d10eb404..c3b0f63c 100644
--- a/src/bootstrap/proStub.ts
+++ b/src/bootstrap/proStub.ts
@@ -2,10 +2,10 @@
// `@offgrid/pro/renderer` here when the `pro/` submodule is absent. The loaders
// check for the activate* exports and skip activation, so the open build runs
// with core features only. Mirrors mobile/src/bootstrap/proStub.js.
-export default null;
+export default null
// Named no-op so core's main.tsx can `import * as Pro` and read Pro.ClipboardPopup
// in the free build (the popup window never opens there, so it never renders).
export function ClipboardPopup(): null {
- return null;
+ return null
}
diff --git a/src/components/ui/focus-cards.tsx b/src/components/ui/focus-cards.tsx
deleted file mode 100644
index 411d31ea..00000000
--- a/src/components/ui/focus-cards.tsx
+++ /dev/null
@@ -1,68 +0,0 @@
-"use client";
-
-import React, { useState } from "react";
-import { cn } from "@/lib/utils";
-
-export const Card = React.memo(
- ({
- card,
- index,
- hovered,
- setHovered,
- }: {
- card: any;
- index: number;
- hovered: number | null;
- setHovered: React.Dispatch>;
- }) => (
- setHovered(index)}
- onMouseLeave={() => setHovered(null)}
- className={cn(
- "rounded-lg relative bg-gray-100 dark:bg-neutral-900 overflow-hidden h-60 md:h-96 w-full transition-all duration-300 ease-out",
- hovered !== null && hovered !== index && "blur-sm scale-[0.98]"
- )}
- >
-
-
-
- )
-);
-
-Card.displayName = "Card";
-
-type Card = {
- title: string;
- src: string;
-};
-
-export function FocusCards({ cards }: { cards: Card[] }) {
- const [hovered, setHovered] = useState(null);
-
- return (
-
- {cards.map((card, index) => (
-
- ))}
-
- );
-}
diff --git a/src/hooks/use-outside-click.tsx b/src/hooks/use-outside-click.tsx
deleted file mode 100644
index 697ad2ac..00000000
--- a/src/hooks/use-outside-click.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import React, { useEffect } from "react";
-
-export const useOutsideClick = (
- ref: React.RefObject,
- callback: Function
-) => {
- useEffect(() => {
- const listener = (event: any) => {
- // DO NOTHING if the element being clicked is the target element or their children
- if (!ref.current || ref.current.contains(event.target)) {
- return;
- }
- callback(event);
- };
-
- document.addEventListener("mousedown", listener);
- document.addEventListener("touchstart", listener);
-
- return () => {
- document.removeEventListener("mousedown", listener);
- document.removeEventListener("touchstart", listener);
- };
- }, [ref, callback]);
-};
diff --git a/src/main/__tests__/active-models-io.test.ts b/src/main/__tests__/active-models-io.test.ts
index aed4290c..089d3e32 100644
--- a/src/main/__tests__/active-models-io.test.ts
+++ b/src/main/__tests__/active-models-io.test.ts
@@ -5,90 +5,129 @@
* against a REAL temp dir (no mock of our own logic) so a corrupt/absent file, a round
* trip, and the null-clear path all fail loudly if the behavior breaks.
*
- * modelsDir (from runtime-env, Electron-bound) is redirected to a per-test temp dir —
- * that is the only boundary mocked; the fs reads/writes are real.
+ * The store receives a per-test temp directory. The JSON serialization and
+ * filesystem reads/writes are the production implementation.
*/
-import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
-import fs from 'fs';
-import os from 'os';
-import path from 'path';
+import { describe, it, expect, beforeEach, afterEach } from 'vitest'
+import fs from 'fs'
+import os from 'os'
+import path from 'path'
-let tmpDir: string;
-vi.mock('../runtime-env', () => ({ modelsDir: () => tmpDir }));
+import {
+ ActiveModalityStore,
+ getActiveModal,
+ getAllActiveModals,
+ setActiveModal
+} from '../active-models'
-import { getActiveModal, setActiveModal, getAllActiveModals } from '../active-models';
+let tmpDir: string
+let store: ActiveModalityStore
+const originalDataDir = process.env.OFFGRID_DATA_DIR
-const storePath = () => path.join(tmpDir, 'active-modalities.json');
+const storePath = (): string => path.join(tmpDir, 'active-modalities.json')
beforeEach(() => {
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'offgrid-active-models-'));
-});
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'offgrid-active-models-'))
+ store = new ActiveModalityStore(() => tmpDir)
+ process.env.OFFGRID_DATA_DIR = tmpDir
+})
afterEach(() => {
- fs.rmSync(tmpDir, { recursive: true, force: true });
-});
+ if (originalDataDir === undefined) {
+ delete process.env.OFFGRID_DATA_DIR
+ } else {
+ process.env.OFFGRID_DATA_DIR = originalDataDir
+ }
+ fs.rmSync(tmpDir, { recursive: true, force: true })
+})
describe('getActiveModal', () => {
it('returns null for every modality when the store file is absent', () => {
- expect(getActiveModal('image')).toBeNull();
- expect(getActiveModal('speech')).toBeNull();
- expect(getActiveModal('transcription')).toBeNull();
- });
+ expect(store.get('image')).toBeNull()
+ expect(store.get('speech')).toBeNull()
+ expect(store.get('transcription')).toBeNull()
+ })
it('returns null when the store file is corrupt JSON (readAll swallows the parse error)', () => {
- fs.writeFileSync(storePath(), '{ this is not json');
- expect(getActiveModal('transcription')).toBeNull();
- });
+ fs.writeFileSync(storePath(), '{ this is not json')
+ expect(store.get('transcription')).toBeNull()
+ })
it('reads back a persisted value', () => {
- fs.writeFileSync(storePath(), JSON.stringify({ transcription: 'csukuangfj/parakeet-v2' }));
- expect(getActiveModal('transcription')).toBe('csukuangfj/parakeet-v2');
- });
-});
+ fs.writeFileSync(storePath(), JSON.stringify({ transcription: 'csukuangfj/parakeet-v2' }))
+ expect(store.get('transcription')).toBe('csukuangfj/parakeet-v2')
+ })
+})
describe('setActiveModal', () => {
it('writes a value that getActiveModal reads back (round trip)', () => {
- setActiveModal('image', 'org/jugg');
- expect(getActiveModal('image')).toBe('org/jugg');
+ store.set('image', 'org/jugg')
+ expect(store.get('image')).toBe('org/jugg')
// Persisted as pretty JSON in the store file.
- expect(JSON.parse(fs.readFileSync(storePath(), 'utf-8')).image).toBe('org/jugg');
- });
+ expect(JSON.parse(fs.readFileSync(storePath(), 'utf-8')).image).toBe('org/jugg')
+ })
it('creates the models dir if it does not exist yet', () => {
- const nested = path.join(tmpDir, 'nested', 'dir');
- tmpDir = nested; // point modelsDir() at a not-yet-created path
- setActiveModal('speech', 'kokoro');
- expect(getActiveModal('speech')).toBe('kokoro');
- });
+ const nested = path.join(tmpDir, 'nested', 'dir')
+ tmpDir = nested
+ store.set('speech', 'kokoro')
+ expect(store.get('speech')).toBe('kokoro')
+ })
it('updates one modality without clobbering the others', () => {
- setActiveModal('image', 'img-1');
- setActiveModal('speech', 'voice-1');
- setActiveModal('image', 'img-2'); // overwrite image only
- expect(getActiveModal('image')).toBe('img-2');
- expect(getActiveModal('speech')).toBe('voice-1');
- });
+ store.set('image', 'img-1')
+ store.set('speech', 'voice-1')
+ store.set('image', 'img-2')
+ expect(store.get('image')).toBe('img-2')
+ expect(store.get('speech')).toBe('voice-1')
+ })
it('clears a slot when set to null', () => {
- setActiveModal('transcription', 'whisper-small');
- setActiveModal('transcription', null);
- expect(getActiveModal('transcription')).toBeNull();
- });
-});
+ store.set('transcription', 'whisper-small')
+ store.set('transcription', null)
+ expect(store.get('transcription')).toBeNull()
+ })
+
+ it('leaves the slot empty when the configured directory cannot be created', () => {
+ const fileInPlaceOfDirectory = path.join(tmpDir, 'not-a-directory')
+ fs.writeFileSync(fileInPlaceOfDirectory, 'occupied')
+ const failingStore = new ActiveModalityStore(() => fileInPlaceOfDirectory)
+
+ expect(() => failingStore.set('image', 'img-x')).not.toThrow()
+ expect(failingStore.get('image')).toBeNull()
+ })
+})
describe('getAllActiveModals', () => {
it('fills every modality with null when the store is empty', () => {
- expect(getAllActiveModals()).toEqual({ image: null, speech: null, transcription: null });
- });
+ expect(store.all()).toEqual({ image: null, speech: null, transcription: null })
+ })
it('returns all three modalities, defaulting the unset ones to null', () => {
- setActiveModal('image', 'img-x');
- expect(getAllActiveModals()).toEqual({ image: 'img-x', speech: null, transcription: null });
- });
+ store.set('image', 'img-x')
+ expect(store.all()).toEqual({ image: 'img-x', speech: null, transcription: null })
+ })
it('reflects a full round trip across all three modalities', () => {
- setActiveModal('image', 'a');
- setActiveModal('speech', 'b');
- setActiveModal('transcription', 'c');
- expect(getAllActiveModals()).toEqual({ image: 'a', speech: 'b', transcription: 'c' });
- });
-});
+ store.set('image', 'a')
+ store.set('speech', 'b')
+ store.set('transcription', 'c')
+ expect(store.all()).toEqual({ image: 'a', speech: 'b', transcription: 'c' })
+ })
+})
+
+describe('production active-model functions', () => {
+ it('persist and read all modalities in the configured runtime profile', () => {
+ setActiveModal('image', 'image-model')
+ setActiveModal('speech', 'speech-model')
+
+ expect(getActiveModal('image')).toBe('image-model')
+ expect(getAllActiveModals()).toEqual({
+ image: 'image-model',
+ speech: 'speech-model',
+ transcription: null
+ })
+ expect(
+ JSON.parse(fs.readFileSync(path.join(tmpDir, 'models', 'active-modalities.json'), 'utf-8'))
+ ).toEqual({ image: 'image-model', speech: 'speech-model' })
+ })
+})
diff --git a/src/main/__tests__/active-models.test.ts b/src/main/__tests__/active-models.test.ts
index 1c249bc5..3cf17eb8 100644
--- a/src/main/__tests__/active-models.test.ts
+++ b/src/main/__tests__/active-models.test.ts
@@ -3,47 +3,92 @@
* chat LLM ever showed/activated — image/voice/transcription must light up when
* their modality's chosen value matches (stored as id OR primary filename).
*
- * active-models.ts pulls modelsDir from runtime-env (Electron-bound), so we mock
- * that to import the pure helpers without a real app.
+ * These rules are isolated from the Electron-bound storage location so the
+ * production decision logic can be exercised directly.
*/
-import { describe, it, expect, vi } from 'vitest';
+import { describe, it, expect } from 'vitest'
+import { modalityForKind, isModelActive } from '../active-models-logic'
-vi.mock('../runtime-env', () => ({ modelsDir: () => '/tmp/models' }));
-
-import { modalityForKind, isModelActive } from '../active-models';
-
-const NONE = { image: null, speech: null, transcription: null } as const;
+const NONE = { image: null, speech: null, transcription: null } as const
describe('modalityForKind', () => {
it('maps non-chat kinds to a modality and chat/unknown kinds to null', () => {
- expect(modalityForKind('image')).toBe('image');
- expect(modalityForKind('voice')).toBe('speech');
- expect(modalityForKind('transcription')).toBe('transcription');
- expect(modalityForKind('text')).toBeNull();
- expect(modalityForKind('vision')).toBeNull();
- expect(modalityForKind('local')).toBeNull();
- expect(modalityForKind(undefined)).toBeNull();
- });
-});
+ expect(modalityForKind('image')).toBe('image')
+ expect(modalityForKind('voice')).toBe('speech')
+ // Idempotent on the storage vocab too, so setActiveModalChoice accepts BOTH the
+ // setup 'voice' AND the dispatched 'speech' (D26 — "Configure for me" passes
+ // 'voice'; activateModel passes 'speech'; both must activate TTS).
+ expect(modalityForKind('speech')).toBe('speech')
+ expect(modalityForKind('transcription')).toBe('transcription')
+ expect(modalityForKind('text')).toBeNull()
+ expect(modalityForKind('vision')).toBeNull()
+ expect(modalityForKind('local')).toBeNull()
+ expect(modalityForKind(undefined)).toBeNull()
+ })
+})
describe('isModelActive', () => {
it('text/vision/local match the active chat LLM id', () => {
- expect(isModelActive({ kind: 'text', id: 'a/x', activeChatId: 'a/x', modals: { ...NONE } })).toBe(true);
- expect(isModelActive({ kind: 'vision', id: 'a/x', activeChatId: 'b/y', modals: { ...NONE } })).toBe(false);
- expect(isModelActive({ kind: 'local', id: 'local:1', activeChatId: 'local:1', modals: { ...NONE } })).toBe(true);
- });
+ expect(
+ isModelActive({ kind: 'text', id: 'a/x', activeChatId: 'a/x', modals: { ...NONE } })
+ ).toBe(true)
+ expect(
+ isModelActive({ kind: 'vision', id: 'a/x', activeChatId: 'b/y', modals: { ...NONE } })
+ ).toBe(false)
+ expect(
+ isModelActive({ kind: 'local', id: 'local:1', activeChatId: 'local:1', modals: { ...NONE } })
+ ).toBe(true)
+ })
it('image matches the chosen value by primary FILENAME (how image picks are stored)', () => {
- expect(isModelActive({ kind: 'image', id: 'org/jugg', primaryFile: 'jugg.gguf', activeChatId: null, modals: { ...NONE, image: 'jugg.gguf' } })).toBe(true);
- expect(isModelActive({ kind: 'image', id: 'org/jugg', primaryFile: 'jugg.gguf', activeChatId: null, modals: { ...NONE, image: 'other.gguf' } })).toBe(false);
- });
+ expect(
+ isModelActive({
+ kind: 'image',
+ id: 'org/jugg',
+ primaryFile: 'jugg.gguf',
+ activeChatId: null,
+ modals: { ...NONE, image: 'jugg.gguf' }
+ })
+ ).toBe(true)
+ expect(
+ isModelActive({
+ kind: 'image',
+ id: 'org/jugg',
+ primaryFile: 'jugg.gguf',
+ activeChatId: null,
+ modals: { ...NONE, image: 'other.gguf' }
+ })
+ ).toBe(false)
+ })
it('voice/transcription match the chosen value by id (how those picks are stored)', () => {
- expect(isModelActive({ kind: 'voice', id: 'kokoro', activeChatId: null, modals: { ...NONE, speech: 'kokoro' } })).toBe(true);
- expect(isModelActive({ kind: 'transcription', id: 'whisper-small', activeChatId: null, modals: { ...NONE, transcription: 'whisper-small' } })).toBe(true);
- });
+ expect(
+ isModelActive({
+ kind: 'voice',
+ id: 'kokoro',
+ activeChatId: null,
+ modals: { ...NONE, speech: 'kokoro' }
+ })
+ ).toBe(true)
+ expect(
+ isModelActive({
+ kind: 'transcription',
+ id: 'whisper-small',
+ activeChatId: null,
+ modals: { ...NONE, transcription: 'whisper-small' }
+ })
+ ).toBe(true)
+ })
it('a chat model is never "active" just because a modality pick exists', () => {
- expect(isModelActive({ kind: 'image', id: 'org/jugg', primaryFile: 'jugg.gguf', activeChatId: 'org/jugg', modals: { ...NONE } })).toBe(false);
- });
-});
+ expect(
+ isModelActive({
+ kind: 'image',
+ id: 'org/jugg',
+ primaryFile: 'jugg.gguf',
+ activeChatId: 'org/jugg',
+ modals: { ...NONE }
+ })
+ ).toBe(false)
+ })
+})
diff --git a/src/main/__tests__/api-docs.test.ts b/src/main/__tests__/api-docs.test.ts
index f8e1807a..ee2e88fb 100644
--- a/src/main/__tests__/api-docs.test.ts
+++ b/src/main/__tests__/api-docs.test.ts
@@ -3,91 +3,92 @@
// All three exports are pure string/object builders parameterised by the live
// gateway port and (for the spec) the current modality/image-model state, so we
// assert the port is threaded through and the live-status branches render.
-import { describe, it, expect } from 'vitest';
-import { docsText, docsHtml, openApiSpec } from '../api-docs';
+import { describe, it, expect } from 'vitest'
+import { docsText, docsHtml, openApiSpec } from '../api-docs'
describe('docsText', () => {
it('embeds the loopback base URL for the given port', () => {
- const out = docsText(8439);
- expect(out).toContain('http://127.0.0.1:8439/v1');
- expect(out).toContain('Off Grid AI — Local Model Gateway');
- expect(out).toContain('no API key required');
- });
+ const out = docsText(8439)
+ expect(out).toContain('http://127.0.0.1:8439/v1')
+ expect(out).toContain('Off Grid AI — Local Model Gateway')
+ expect(out).toContain('no API key required')
+ })
it('lists every modality endpoint', () => {
- const out = docsText(1234);
+ const out = docsText(1234)
for (const p of [
'/v1/chat/completions',
'/v1/embeddings',
'/v1/audio/transcriptions',
'/v1/audio/speech',
'/v1/audio/voices',
- '/v1/images',
+ '/v1/images'
]) {
- expect(out).toContain(`http://127.0.0.1:1234${p}`);
+ expect(out).toContain(`http://127.0.0.1:1234${p}`)
}
- });
+ })
it('threads a different port through the whole document', () => {
- expect(docsText(9999)).not.toContain('127.0.0.1:8439');
- expect(docsText(9999)).toContain('127.0.0.1:9999');
- });
-});
+ expect(docsText(9999)).not.toContain('127.0.0.1:8439')
+ expect(docsText(9999)).toContain('127.0.0.1:9999')
+ })
+})
describe('docsHtml', () => {
it('is a self-contained HTML document', () => {
- const html = docsHtml(8439);
- expect(html.startsWith('')).toBe(true);
- expect(html).toContain('