Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Guidance for coding agents working in this repository.
- Grimoire is a local-first bookmark manager.
- The frontend is a React 18 + Vite + TypeScript SPA under `src/`.
- The background daemon is a Bun service under `daemon/` that listens on `127.0.0.1:3210`, stores data in SQLite, and exposes the REST API used by the frontend.
- Current release: `1.0.0`.
- Current release: `1.1.0`.

## First Reads

Expand Down
105 changes: 99 additions & 6 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -1592,6 +1592,70 @@ Responses:
|---|---|---|---|
| `200` | application/json | `ConnectivityTestResponse` | Connectivity result |

#### GET /settings/ai-models

List models available from an AI provider catalog (currently OpenRouter).

The OpenRouter catalog endpoint is public and needs no API key. Pass free=true to list only models with zero prompt and completion pricing. The endpoint triggers outbound requests on behalf of the browser, so it requires the X-LittleImp-Frontend header that the app always sends; foreign web pages cannot set custom headers and are therefore blocked.

Query parameters:

| Field | Type | Required | Description |
|---|---|---:|---|
| `provider` | "openrouter" | no | Provider to list models for |
| `free` | "true" \| "false" | no | When true, only free models are returned |

Responses:

| Status | Content type | Schema | Description |
|---|---|---|---|
| `200` | application/json | `AiModelCatalogResponse` | AI model catalog |
| `400` | application/problem+json | `ProblemDetails` | Unsupported provider |
| `403` | application/problem+json | `ProblemDetails` | Missing X-LittleImp-Frontend header |
| `422` | application/problem+json | `ProblemDetails` | Configured OpenRouter base URL is invalid |
| `502` | application/problem+json | `ProblemDetails` | Provider model catalog could not be fetched |

Examples:

**List free OpenRouter models**

Request:

```bash
curl -H "X-LittleImp-Frontend: 1" "http://127.0.0.1:3210/settings/ai-models?provider=openrouter&free=true"
```

Response:

```http
HTTP/1.1 200 OK
Content-Type: application/json

{
"data": {
"provider": "openrouter",
"free": true,
"fetched_at": "2026-06-01T09:30:00.000Z",
"models": [
{
"id": "inclusionai/ling-3.0-flash:free",
"name": "Ling-3.0-flash (free)",
"context_length": 262144,
"prompt_price": "0",
"completion_price": "0"
},
{
"id": "meta-llama/llama-3.2-3b-instruct:free",
"name": "Meta: Llama 3.2 3B Instruct (free)",
"context_length": 131072,
"prompt_price": "0",
"completion_price": "0"
}
]
}
}
```

### Backup

#### POST /backup
Expand Down Expand Up @@ -1874,7 +1938,6 @@ Request body:
| `key` | string | no | Remote S3 snapshot.db key |
| `path` | string | no | Absolute path to an encrypted backup package file accessible by the daemon |
| `password` | string | no | Password used to decrypt the encrypted package |
| `allow_unsafe_no_checksum` | boolean | no | Allow restoring a backup with no checksum file |

Responses:

Expand Down Expand Up @@ -2393,7 +2456,7 @@ Request:
curl -X POST http://127.0.0.1:3210/mcp \
-H "Authorization: Bearer limp_it_example" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"curl","version":"1.0.1"}}}'
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"curl","version":"1.1.0"}}}'
```

Response:
Expand All @@ -2410,7 +2473,7 @@ Content-Type: application/json
"capabilities": {},
"serverInfo": {
"name": "grimoire",
"version": "1.0.1"
"version": "1.1.0"
}
}
}
Expand Down Expand Up @@ -3402,7 +3465,7 @@ Resolved category and tag remapping decisions applied to an import preview or co

| Field | Type | Required | Description |
|---|---|---:|---|
| `classification` | "new" \| "active_duplicate" \| "archived_duplicate" \| "trashed_duplicate" \| "invalid_url" \| "private_url" | yes | Import row classification |
| `classification` | "new" \| "active_duplicate" \| "archived_duplicate" \| "trashed_duplicate" \| "invalid_url" \| "private_url" \| "credential_url" | yes | Import row classification |
| `action` | "create" \| "skip" \| "merge" \| "restore_merge" | yes | Action that the selected policy would apply |
| `url` | string \| null | yes | Source bookmark URL |
| `title` | string | yes | Source bookmark title |
Expand Down Expand Up @@ -3505,7 +3568,7 @@ Final committed import row result
|---|---|---:|---|
| `status` | "created" \| "merged" \| "restored" \| "skipped" \| "failed" | yes | Final committed row status |
| `action` | "create" \| "skip" \| "merge" \| "restore_merge" | yes | Requested action selected by the duplicate policy |
| `classification` | "new" \| "active_duplicate" \| "archived_duplicate" \| "trashed_duplicate" \| "invalid_url" \| "private_url" | yes | Import row classification |
| `classification` | "new" \| "active_duplicate" \| "archived_duplicate" \| "trashed_duplicate" \| "invalid_url" \| "private_url" \| "credential_url" | yes | Import row classification |
| `url` | string \| null | yes | Source bookmark URL |
| `title` | string | yes | Source bookmark title |
| `notes` | string \| null | yes | Source note text when the import format provides note-like metadata |
Expand Down Expand Up @@ -3880,6 +3943,37 @@ Response data
| `error` | string | no | Failure reason |
| `message` | string | no | Success message |

### AiModel

| Field | Type | Required | Description |
|---|---|---:|---|
| `id` | string | yes | Provider model slug, e.g. openai/gpt-4o |
| `name` | string | yes | Human-readable model name |
| `context_length` | integer \| null | yes | Context window in tokens, when advertised |
| `prompt_price` | string \| null | yes | Prompt price per token as a decimal string; "0" means free |
| `completion_price` | string \| null | yes | Completion price per token as a decimal string; "0" means free |

### AiModelCatalog

| Field | Type | Required | Description |
|---|---|---:|---|
| `provider` | "openrouter" | yes | Provider the catalog was fetched from |
| `free` | boolean | yes | Whether only free models were requested |
| `fetched_at` | string | yes | Catalog fetch timestamp |
| `models` | array<AiModel> | yes | Available models |

### AiModelCatalogResponse

AI model catalog response

| Field | Type | Required | Description |
|---|---|---:|---|
| `data` | AiModelCatalog | yes | |
| `data.provider` | "openrouter" | yes | Provider the catalog was fetched from |
| `data.free` | boolean | yes | Whether only free models were requested |
| `data.fetched_at` | string | yes | Catalog fetch timestamp |
| `data.models` | array<AiModel> | yes | Available models |

### BackupSchedule

| Field | Type | Required | Description |
Expand Down Expand Up @@ -4032,7 +4126,6 @@ Response data
| `key` | string | no | Remote S3 snapshot.db key |
| `path` | string | no | Absolute path to an encrypted backup package file accessible by the daemon |
| `password` | string | no | Password used to decrypt the encrypted package |
| `allow_unsafe_no_checksum` | boolean | no | Allow restoring a backup with no checksum file |

### RestoreResult

Expand Down
46 changes: 44 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,57 @@ All notable changes to Grimoire will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [1.1.0] - 2026-08-12

### Added
- AI provider model picker in Settings: a searchable OpenRouter model combobox
with a Free models only filter and custom (typed) model entry, backed by a
new `GET /settings/ai-models` catalog endpoint (SSRF-guarded, size-capped).
- Route-level error boundary so malformed API data cannot blank the app.

### Fixed
- OpenRouter default model normalized to `openai/gpt-latest` (the `~` fallback
prefix is dropped and stored values are stripped on load).
- Concurrent same-URL bookmark creates return the existing bookmark instead of
failing with a raw error; trashed/archived duplicates get a clear 409.
- Mutations await daemon confirmation before success toasts, dialog close, and
undo actions; failures surface as destructive toasts.
- Bulk category moves use category IDs, and list/search filters prefer
`category_id` over the category name.
- The bookmarklet embeds the token and daemon URL via `JSON.stringify` so
quotes or backslashes cannot break the generated script.

### Security
- SSRF hardening: outbound page fetches and update checks follow redirects
manually and validate every hop; private hosts, embedded credentials, and
non-http(s) schemes are rejected at bookmarks, capture, MCP, and import.
- Remote upgrades require HTTPS release bases and detached signatures by
default, with optional signing-key fingerprint pinning and download size
caps; `--allow-unsigned` / `LITTLEIMP_ALLOW_UNSIGNED_UPGRADE=1` is an
explicit escape hatch.
- The daemon refuses non-loopback binds unless running in a container or
`LITTLEIMP_ALLOW_NON_LOOPBACK_BIND=1` is set explicitly.
- Mutating HTTP routes get default JSON body limits; restore no longer accepts
`allow_unsafe_no_checksum` over the HTTP API.

## [1.0.0] - 2026-07-20

### Changed
- General-availability release. The `littleimp`/`littleimpd` daemon and CLI
tooling names are stable on-disk identifiers and remain unchanged.

## [Unreleased]
## [1.0.1] - 2026-08-03

No unreleased changes.
### Fixed
- The one-command installer now defaults to the current release version instead
of the stale `0.1.0-beta`.
- GitHub issue and repository extraction now accepts `www.github.com` URLs and
keeps stripping a trailing `.git` from repository names.
- Outbound HTTP requests identify as `Grimoire/<version>` (derived from the
packaged version) instead of the stale `LittleImp/0.0`.
- The sqlite-vec index is rebuilt in a single atomic transaction at daemon start.
- E2E mocks, test fixtures, and release docs aligned with the 1.0.x version
identity and the `goniszewski/grimoire` repository.

## [1.0.1] - 2026-08-03

Expand Down
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ ENV XDG_CONFIG_HOME=/data/config
ENV HOME=/data
ENV NODE_ENV=production
ENV LOG_FORMAT=json
ENV LITTLEIMP_IN_CONTAINER=1

# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
Expand Down
8 changes: 4 additions & 4 deletions Formula/grimoire.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ class Grimoire < Formula
depends_on "oven-sh/bun/bun"

if OS.mac?
url "https://github.com/goniszewski/grimoire/releases/download/v1.0.1/little-imp-1.0.1-macos.tar.gz"
sha256 "a8c934821cc8db588ef9b3213f013c4cd99f1ae23ba8f18e400727191a9d49c1"
url "https://github.com/goniszewski/grimoire/releases/download/v1.1.0/little-imp-1.1.0-macos.tar.gz"
sha256 "f9435a3d956bf47aaa7b724319e49e41699b51d462d8474d91eaf5ca1f325cd2"
elsif OS.linux?
url "https://github.com/goniszewski/grimoire/releases/download/v1.0.1/little-imp-1.0.1-linux.tar.gz"
sha256 "42cf4ea63bb31ea2380a0b3c8c4c65f7af943974ce1024dd9562a2484e343cff"
url "https://github.com/goniszewski/grimoire/releases/download/v1.1.0/little-imp-1.1.0-linux.tar.gz"
sha256 "8b903ee96aa5ea9fa2edcec223c969c10e5e2231b1bfe4e7ad442be31d295b0c"
end

def install
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<br>

[![Quality Gates](https://github.com/goniszewski/grimoire/actions/workflows/quality.yml/badge.svg?branch=main)](https://github.com/goniszewski/grimoire/actions/workflows/quality.yml)
![Release target](https://img.shields.io/badge/release-1.0.1-7c3aed)
![Release target](https://img.shields.io/badge/release-1.1.0-7c3aed)
![Bun 1.x](https://img.shields.io/badge/Bun-1.x-black)
[![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](./LICENSE)

Expand Down
9 changes: 5 additions & 4 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@

| Version | Supported |
| --- | --- |
| `1.0.0` | Yes |
| `1.1.0` | Yes |
| `1.0.x` | No |
| `0.1.0-beta` | No |

## Reporting A Vulnerability
Expand All @@ -29,7 +30,7 @@ Please include:

## Security Boundary Summary

Grimoire is local-first, single-user, and loopback-first for `1.0.0`.
Grimoire is local-first, single-user, and loopback-first for `1.1.0`.

- Native daemon default: `127.0.0.1:3210`.
- Docker host port default: `127.0.0.1:3210:3210`.
Expand Down Expand Up @@ -122,7 +123,7 @@ claimed while unauthenticated release URLs return `404`.
- Any local process that can reach `127.0.0.1:3210` can call unprotected REST
routes. This is part of the local-first trust model.
- Grimoire is not designed for multi-user hosts or shared public servers in
`1.0.0`.
`1.1.0`.
- DNS rebinding is not fully mitigated and is accepted only because the daemon
is loopback-only.
- Content extraction fetches user-supplied public URLs. Protections reduce
Expand Down Expand Up @@ -159,7 +160,7 @@ When contributing:

## Future Security Work

These are not shipped in `1.0.0`:
These are not shipped in `1.1.0`:

- Optional authentication for future public or multi-user modes.
- Per-client rate limiting for authenticated non-local deployment modes.
Expand Down
2 changes: 1 addition & 1 deletion daemon/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "littleimpd",
"version": "1.0.1",
"version": "1.1.0",
"private": true,
"type": "module",
"bin": {
Expand Down
Loading
Loading