Skip to content
Merged
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ coverage/
.omc/
*.timestamp-*.mjs
.env
*.tsbuildinfo

# Internal development artifacts not suitable for the public repo
docs/superpowers/
Expand Down
4 changes: 2 additions & 2 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -2393,7 +2393,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.0"}}}'
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"curl","version":"1.0.1"}}}'
```

Response:
Expand All @@ -2410,7 +2410,7 @@ Content-Type: application/json
"capabilities": {},
"serverInfo": {
"name": "grimoire",
"version": "1.0.0"
"version": "1.0.1"
}
}
}
Expand Down
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

No unreleased changes.

## [1.0.1] - 2026-08-03

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Place the newest release before older changelog entries

The new 1.0.1 section is below both the older 1.0.0 section and Unreleased, so readers or release tooling scanning the changelog from the top encounter 1.0.0 as the latest published version. Keep Unreleased first and move 1.0.1 immediately below it, ahead of 1.0.0.

Useful? React with 👍 / 👎.


### 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.

## [0.1.0-beta] - 2026-05-28

### Added
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.0/little-imp-1.0.0-macos.tar.gz"
sha256 "000000000000000000000000000000000000000000000000000000000000000a"
url "https://github.com/goniszewski/grimoire/releases/download/v1.0.1/little-imp-1.0.1-macos.tar.gz"
sha256 "a8c934821cc8db588ef9b3213f013c4cd99f1ae23ba8f18e400727191a9d49c1"
elsif OS.linux?
url "https://github.com/goniszewski/grimoire/releases/download/v1.0.0/little-imp-1.0.0-linux.tar.gz"
sha256 "000000000000000000000000000000000000000000000000000000000000000b"
url "https://github.com/goniszewski/grimoire/releases/download/v1.0.1/little-imp-1.0.1-linux.tar.gz"
sha256 "42cf4ea63bb31ea2380a0b3c8c4c65f7af943974ce1024dd9562a2484e343cff"
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.0-7c3aed)
![Release target](https://img.shields.io/badge/release-1.0.1-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
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.0",
"version": "1.0.1",
"private": true,
"type": "module",
"bin": {
Expand Down
4 changes: 2 additions & 2 deletions daemon/src/api/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3031,7 +3031,7 @@ export const apiContract = {
{
title: "Call the MCP endpoint",
request:
'curl -X POST http://127.0.0.1:3210/mcp \\\n -H "Authorization: Bearer limp_it_example" \\\n -H "Content-Type: application/json" \\\n -d \'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"curl","version":"1.0.0"}}}\'',
'curl -X POST http://127.0.0.1:3210/mcp \\\n -H "Authorization: Bearer limp_it_example" \\\n -H "Content-Type: application/json" \\\n -d \'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"curl","version":"1.0.1"}}}\'',
response: {
status: 200,
contentType: "application/json",
Expand All @@ -3043,7 +3043,7 @@ export const apiContract = {
capabilities: {},
serverInfo: {
name: "grimoire",
version: "1.0.0",
version: "1.0.1",
},
},
},
Expand Down
30 changes: 24 additions & 6 deletions daemon/src/db/embedding-repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,12 +123,30 @@ export class EmbeddingRepository {
.query<EmbeddingRow, []>("SELECT * FROM embeddings")
.all();

for (const table of this.listVectorTables()) {
this.db.run(`DELETE FROM ${table}`);
}
for (const row of rows) {
this.upsertVectorIndex(row.bookmark_id, row.model, row.dimensions, row.vector);
}
// One transaction for the whole rebuild: per-row transactions would be
// thousands of BEGIN/COMMIT cycles on every daemon start, and the single
// transaction also keeps the index atomic if a crash interrupts a boot.
this.db.transaction(() => {
for (const table of this.listVectorTables()) {
this.db.run(`DELETE FROM ${table}`);
}
for (const row of rows) {
const table = vectorTableName(row.dimensions);
this.db.exec(
`CREATE VIRTUAL TABLE IF NOT EXISTS ${table} USING vec0(
bookmark_id TEXT PRIMARY KEY,
model TEXT PARTITION KEY,
embedding FLOAT[${row.dimensions}] distance_metric=cosine
)`
);
this.db
.query<unknown, [string, string, Uint8Array]>(
`INSERT INTO ${table} (bookmark_id, model, embedding)
VALUES (?, ?, ?)`
)
.run(row.bookmark_id, row.model, row.vector);
}
})();
Comment on lines +129 to +149
} catch (error) {
this.disableVectorIndex(error);
}
Expand Down
3 changes: 2 additions & 1 deletion daemon/src/media/bookmark-media.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { existsSync, rmSync } from "node:fs";
import { mkdir } from "node:fs/promises";
import { join, resolve, sep } from "node:path";
import { isPrivateHost } from "../lib/network.js";
import { version as APP_VERSION } from "../../package.json";
import type { BookmarkMediaKind, BookmarkMediaRow } from "../db/types.js";

export interface BookmarkMediaCandidate {
Expand Down Expand Up @@ -281,7 +282,7 @@ async function fetchMedia(candidate: BookmarkMediaCandidate): Promise<{
response = await fetch(source.toString(), {
headers: {
Accept: "image/avif,image/webp,image/png,image/jpeg,image/gif,image/x-icon,*/*;q=0.2",
"User-Agent": "Mozilla/5.0 (compatible; LittleImp/0.0; +https://github.com/goniszewski/little-imp)",
"User-Agent": `Mozilla/5.0 (compatible; Grimoire/${APP_VERSION}; +https://github.com/goniszewski/grimoire)`,
},
redirect: "manual",
signal: controller.signal,
Expand Down
7 changes: 5 additions & 2 deletions daemon/src/pipeline/extractors/github-issues.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/

import { ExtractionResult } from "./types.js";
import { version as APP_VERSION } from "../../../package.json";

const GITHUB_API = "https://api.github.com";
const TIMEOUT_MS = 15_000;
Expand Down Expand Up @@ -53,7 +54,7 @@ async function ghIssueFetch(path: string): Promise<unknown> {
const headers: Record<string, string> = {
Accept: "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
"User-Agent": "LittleImp/0.0",
"User-Agent": `Grimoire/${APP_VERSION}`,
};
if (token) headers["Authorization"] = `Bearer ${token}`;

Expand All @@ -79,7 +80,8 @@ async function ghIssueFetch(path: string): Promise<unknown> {
export function parseGitHubIssueUrl(url: string): GitHubIssueUrl | null {
try {
const parsedUrl = new URL(url);
if (parsedUrl.hostname !== "github.com") return null;
// GitHub redirects www.github.com to github.com; treat both as canonical.
if (parsedUrl.hostname.replace(/^www\./, "") !== "github.com") return null;

const parts = parsedUrl.pathname.split("/").filter(Boolean);
if (parts.length < 4 || parts[2] !== "issues") return null;
Expand All @@ -90,6 +92,7 @@ export function parseGitHubIssueUrl(url: string): GitHubIssueUrl | null {

return {
owner: parts[0],
// A trailing .git is common in pasted repo URLs; the API rejects it.
repo: parts[1].replace(/\.git$/, ""),
number,
};
Expand Down
7 changes: 5 additions & 2 deletions daemon/src/pipeline/extractors/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
*/

import { ExtractionResult } from "./types.js";
import { version as APP_VERSION } from "../../../package.json";

const GITHUB_API = "https://api.github.com";
const TIMEOUT_MS = 15_000;
Expand Down Expand Up @@ -38,7 +39,7 @@ async function ghFetch(path: string): Promise<unknown> {
const headers: Record<string, string> = {
Accept: "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
"User-Agent": "LittleImp/0.0",
"User-Agent": `Grimoire/${APP_VERSION}`,
};
if (token) headers["Authorization"] = `Bearer ${token}`;

Expand All @@ -58,9 +59,11 @@ async function ghFetch(path: string): Promise<unknown> {
export function parseGitHubUrl(url: string): { owner: string; repo: string } | null {
try {
const u = new URL(url);
if (u.hostname !== "github.com") return null;
// GitHub redirects www.github.com to github.com; treat both as canonical.
if (u.hostname.replace(/^www\./, "") !== "github.com") return null;
const parts = u.pathname.split("/").filter(Boolean);
if (parts.length < 2) return null;
// A trailing .git is common in pasted repo URLs; the API rejects it.
return { owner: parts[0], repo: parts[1].replace(/\.git$/, "") };
} catch {
return null;
Expand Down
3 changes: 2 additions & 1 deletion daemon/src/pipeline/extractors/stackoverflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
*/

import { ExtractionResult } from "./types.js";
import { version as APP_VERSION } from "../../../package.json";

const SE_API = "https://api.stackexchange.com/2.3";
const TIMEOUT_MS = 15_000;
Expand Down Expand Up @@ -49,7 +50,7 @@ async function seFetch(path: string, site: string): Promise<unknown> {
const res = await fetch(
`${SE_API}${path}${path.includes("?") ? "&" : "?"}${siteParam}${keyParam}`,
{
headers: { "User-Agent": "LittleImp/0.0" },
headers: { "User-Agent": `Grimoire/${APP_VERSION}` },
signal: controller.signal,
}
);
Expand Down
3 changes: 2 additions & 1 deletion daemon/src/pipeline/extractors/youtube.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import { ExtractionResult } from "./types.js";
import { log } from "../../logger.js";
import { isPrivateHost } from "../../lib/network.js";
import { version as APP_VERSION } from "../../../package.json";

const TIMEOUT_MS = 15_000;
const MAX_TRANSCRIPT_CHARS = 500_000;
Expand Down Expand Up @@ -124,7 +125,7 @@ async function fetchCaptionTracks(videoId: string): Promise<TimedTextTrack[]> {
headers: {
"Content-Type": "application/json",
"User-Agent":
"Mozilla/5.0 (compatible; LittleImp/0.0; +https://github.com/goniszewski/little-imp)",
`Mozilla/5.0 (compatible; Grimoire/${APP_VERSION}; +https://github.com/goniszewski/grimoire)`,
},
body,
});
Expand Down
3 changes: 2 additions & 1 deletion daemon/src/pipeline/fetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@
*/

import { isPrivateHost } from "../lib/network.js";
import { version as APP_VERSION } from "../../package.json";

const USER_AGENT =
"Mozilla/5.0 (compatible; LittleImp/0.0; +https://github.com/goniszewski/little-imp)";
`Mozilla/5.0 (compatible; Grimoire/${APP_VERSION}; +https://github.com/goniszewski/grimoire)`;

const FETCH_TIMEOUT_MS = 20_000;
/** Reject responses larger than this to prevent memory exhaustion. */
Expand Down
24 changes: 12 additions & 12 deletions daemon/src/test/cli-update.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,12 +172,12 @@ describe("littleimp update CLI", () => {
html_url: "https://github.com/goniszewski/grimoire/releases/tag/v1.1.0-beta.1",
},
{
tag_name: "v1.0.1",
name: "Grimoire 1.0.1",
tag_name: "v1.1.0",
name: "Grimoire 1.1.0",
draft: false,
prerelease: false,
published_at: "2026-05-17T12:00:00Z",
html_url: "https://github.com/goniszewski/grimoire/releases/tag/v1.0.1",
html_url: "https://github.com/goniszewski/grimoire/releases/tag/v1.1.0",
},
]);

Expand All @@ -194,20 +194,20 @@ describe("littleimp update CLI", () => {
expect(harness.calls[0].url).toBe("https://updates.example.test/releases");
expect(harness.calls[0].init?.headers).toEqual({
accept: "application/vnd.github+json",
"user-agent": "littleimp-update-check/1.0.0",
"user-agent": "littleimp-update-check/1.0.1",
});
expect(JSON.parse(harness.stdout[0])).toEqual({
current_version: "1.0.0",
current_version: "1.0.1",
update_available: true,
source: "https://updates.example.test/releases",
channel: "stable",
latest: {
version: "1.0.1",
tag: "v1.0.1",
name: "Grimoire 1.0.1",
version: "1.1.0",
tag: "v1.1.0",
name: "Grimoire 1.1.0",
prerelease: false,
published_at: "2026-05-17T12:00:00Z",
url: "https://github.com/goniszewski/grimoire/releases/tag/v1.0.1",
url: "https://github.com/goniszewski/grimoire/releases/tag/v1.1.0",
},
});
});
Expand Down Expand Up @@ -294,7 +294,7 @@ describe("littleimp update CLI", () => {

expect(code).toBe(0);
expect(harness.stdout.join("\n")).toContain("Grimoire is up to date");
expect(harness.stdout.join("\n")).toContain("1.0.0");
expect(harness.stdout.join("\n")).toContain("1.0.1");
});

it("uses LITTLEIMP_UPDATE_SOURCE when no source flag is provided", async () => {
Expand Down Expand Up @@ -343,7 +343,7 @@ describe("littleimp update CLI", () => {
expect(harness.spawnCalls[0].args).toContain("--upgrade");
expect(harness.fetchCalls[0].url).toBe("http://127.0.0.1:3210/health");
expect(JSON.parse(harness.stdout[0])).toMatchObject({
current_version: "1.0.0",
current_version: "1.0.1",
upgraded_version: fixture.version,
archive: fixture.archivePath,
checksum_verified: true,
Expand Down Expand Up @@ -470,7 +470,7 @@ describe("littleimp update CLI", () => {
});

it("checks the release source before downloading the latest compatible upgrade when no version is provided", async () => {
const fixture = createUpgradeArchiveFixture({ version: "1.0.1" });
const fixture = createUpgradeArchiveFixture({ version: "1.1.0" });
const stdout: string[] = [];
const stderr: string[] = [];
const fetchCalls: FetchCall[] = [];
Expand Down
41 changes: 41 additions & 0 deletions daemon/src/test/embedding-repository.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,47 @@ describe("EmbeddingRepository vector index", () => {
].sort(byBookmarkId));
});

it("rebuildVectorIndex restores vector tables from the embeddings table when available", () => {
const db = makeTestDb();
const bookmarks = new BookmarkRepository(db);
const embeddings = new EmbeddingRepository(db);

const vectorIndexAvailable = embeddings.isVectorIndexAvailable(2);
if (!vectorIndexAvailable) {
expect(vectorIndexAvailable).toBe(false);
return;
}

const first = bookmarks.create("https://vectors.example.com/rebuild-a", "Rebuild A");
const second = bookmarks.create("https://vectors.example.com/rebuild-b", "Rebuild B");
embeddings.upsert(first.id, "test-model", [1, 0]);
embeddings.upsert(second.id, "test-model", [0, 1]);

// Simulate a stale or partially-written index (e.g. a crash between the
// embeddings write and the vec mirror, or a pre-sqlite-vec database).
db.run("DELETE FROM embedding_vec_2");
expect(
db.query<{ c: number }, []>("SELECT COUNT(*) AS c FROM embedding_vec_2").get()?.c
).toBe(0);

embeddings.rebuildVectorIndex();

const restoredRows = db
.query<{ bookmark_id: string }, []>(
"SELECT bookmark_id FROM embedding_vec_2 ORDER BY bookmark_id"
)
.all();
expect(restoredRows.map((row) => row.bookmark_id).sort()).toEqual(
[first.id, second.id].sort()
);

const nearest = embeddings.findNearest("test-model", [0.99, 0.01], {
limit: 1,
excludeBookmarkId: null,
});
expect(nearest.map((row) => row.bookmarkId)).toEqual([first.id]);
});

it("returns nearest neighbors from sqlite-vec with cosine similarity scores when available", () => {
const db = makeTestDb();
const bookmarks = new BookmarkRepository(db);
Expand Down
Loading
Loading