← Corpus / conversational-ui-and-native-shells / profile

conversational-ui-and-native-shells/profiles/profile__5ire

Path
profiles/Profile__5ire.md

name: 5ire Profile slug: 5ire upstream: https://github.com/nanbingxyz/5ire package: “5ire” (npm-style package.json, but distributed only as electron-builder desktop binaries — macOS/Windows/Linux, via .erb scaffold) license: Modified Apache-2.0 (“5ire Community Edition” — Apache 2.0 plus additional commercial-use and brand-protection terms; see Characterizing the license below) maintainer: Ironben / nanbingxyz (5ire.app) study: studies/conversational-ui-and-native-shells profile_path: studies/conversational-ui-and-native-shells/5ire profile_kind: Electron (React renderer), MCP client + local-first chat app mid-migration from SQLite to PGlite/Postgres date_created: 2026-07-13

5ire — Profile

A profile of 5ire as it lives in this study (studies/conversational-ui-and-native-shells/5ire/, pinned at c7fabb9, tag-derived version v0.15.4-5-gc7fabb9). Cites pinned paths so you can jump to source rather than trust paraphrase. Included as the study’s Electron comparison point — its local-persistence story and MCP-server-marketplace maturity are the load-bearing reasons it’s here, despite the study otherwise leaning Tauri. Read alongside Profile__Dive.md (Tauri+Electron dual-shell, MCP host) and Profile__Anything-Llm.md (Electron, workspace-switching).

Characterizing the license

LICENSE:1-6 is explicit: not MIT. It’s Apache License 2.0 plus additional conditions the file calls “5ire Community Edition” terms — package.json:11 echoes this as "license": "Modified Apache-2.0". The additional terms (LICENSE:16-33) require that modified/redistributed versions not use the “5ire Brand,” and mandate a commercial license if you (a) serve enterprise clients (>10 users), (b) embed 5ire in hardware/products for sale, or (c) handle government/education procurement with sensitive data. Section 3.3 also has contributors grant 5ire “exclusive rights to dual-license” their contributions and waive patent claims against 5ire’s commercial use. This is a source-available / BUSL-adjacent license, not a permissive OSS license — treat any reuse of 5ire code accordingly.

TL;DR

5ire is a desktop Electron chat client and MCP client whose defining architectural fact right now is that it’s mid-migration between two entirely different persistence stacks, visible directly in the source tree: a legacy better-sqlite3 database (src/main/sqlite.ts) still serving conversations, messages, and bookmarks over a raw-SQL IPC bridge, alongside a new Postgres-flavored PGlite + Drizzle ORM layer (src/main/database/index.ts, src/main/database/schema/tables.ts) that has already absorbed knowledge collections, documents, document-chunk embeddings (via pgvector), and — notably — MCP server configuration, which is fully modern. A dedicated LegacyDataMigrator (src/main/services/legacy-data-migrator.ts) copies rows across on startup, one domain at a time, with idempotency tracked via legacyId unique indexes and persisted migration-completion flags — but as of this pin, it does not migrate chats, messages, or bookmarks. Those still live entirely in SQLite, and full-text search over them is a hand-rolled LIKE '%term%' scan, not FTS5 or tsvector.

On the MCP side, 5ire is one of the more complete implementations in this study: a real @modelcontextprotocol/sdk Client over StdioClientTransport/StreamableHTTPClientTransport (src/main/services/mcp-connections-manager.ts:1-3,81-96), retry-with-backoff connection logic (#connect, mcp-connections-manager.ts:104-204), and — because MCP server config already lives in PGlite — reactive reconnection driven by the database’s own live-query changefeed (driver.live.changes(...), mcp-connections-manager.ts:266-320): editing a server row in the servers table causes the manager to diff columns and reconnect/disconnect automatically, no manual “refresh servers” call needed anywhere in the app. Layered on top is a genuine community marketplace: useMCPServerMarketStore.fetchServers() (src/stores/useMCPServerMarketStore.ts:22-43) does a plain fetch('https://mcpsvr.com/servers.json') against the separate nanbingxyz/mcpsvr repo, cached client-side for 24 hours, filtered/searched in ToolMarketDrawer.tsx, and installed via a callback that writes a new row into the same servers table the connections manager is already watching.

If you want one sentence: 5ire pairs one of this study’s most complete MCP-client implementations — SDK-native transports, DB-change-driven reconnection, and a live community server marketplace fetched from mcpsvr.com — with a persistence layer caught mid-flight from raw-SQL SQLite to a PGlite/Drizzle/pgvector stack, where the newer, more capable database already owns “servers” and RAG documents but has not yet absorbed chats, messages, bookmarks, or search, which still run as string-matched LIKE scans over a legacy 5ire.db file reached through an ad hoc db-all/db-run/db-get IPC surface.

Local persistence — a database mid-migration, visible in the diff between two files

  • Legacy store: one better-sqlite3 file, opened with WAL mode, exposed as raw SQL over IPC. initLegacyDatabase() (src/main/sqlite.ts:9-12) opens <userData>/5ire.db and hand-writes CREATE TABLE IF NOT EXISTS statements for folders, chats, messages, bookmarks, prompts, usages, knowledge_collections, knowledge_files, chat_knowledge_rels (sqlite.ts:14-200), followed by ad hoc alertTableChats/alertTableMessages/alertTableBookmarks/alertTableFolders functions (sqlite.ts:202-282) that PRAGMA table_info each table and ALTER TABLE ... ADD COLUMN when a column introduced in a later version is missing — a migration system built from manual, dated if checks rather than a migration framework. database.pragma("journal_mode = WAL") (sqlite.ts:308) is the one concession to concurrent-access performance. The whole thing is reachable from the renderer only through four generic ipcMain.handle verbs — db-all, db-run, db-transaction, db-get (sqlite.ts:311-373) — meaning the renderer constructs and ships raw SQL strings across the IPC boundary (see useBookmarkStore.ts below), not typed commands.
  • New store: PGlite (Postgres-in-WASM) + Drizzle ORM + pgvector, with live queries. Database.#init() (src/main/database/index.ts:47-146) creates a PGlite instance backed by NodeFS at this.#environment.databaseDataFolder, loads the vector and live PGlite extensions, runs CREATE EXTENSION IF NOT EXISTS vector, defensively drops stale live_query_% views/tables/prepared-statements left over from unclean shutdowns (index.ts:62-114 — a real operational scar, not theoretical), then builds a drizzle(driver, { schema }) client and calls Drizzle’s own migrate() against a databaseMigrationsFolder of SQL migration files (generated via drizzle-kit generate, package.json:26, output under drizzle/migrations). This is a schema-versioned, tool-generated migration pipeline — the opposite of the legacy file’s manual ALTER TABLE checks.
  • The new schema already covers knowledge/RAG and MCP servers, not yet chat. src/main/database/schema/tables.ts defines collection, document, documentChunk (with an HNSW index over a 1024-dim vector column, tables.ts:198,220bge-m3 embedding dimensionality, matching the README’s local-embedding claim), conversationCollection, prompt, project, conversation, turn, provider, usage, and server (the MCP server table). Conspicuously, tables.ts:570-573 has commented-out bookmarkColumns/bookmark table stubs — direct evidence bookmarks are slated to move but haven’t yet.
  • legacyId is the migration’s join key, and onConflictDoNothing makes re-runs idempotent. Every migrated table (collection, document, documentChunk) carries a legacyId: varchar(...) column with a uniqueIndex().on(table.legacyId).where(isNotNull(...)) (tables.ts:91,167,223). LegacyDataMigrator (src/main/services/legacy-data-migrator.ts) uses this for a five-stage, order-dependent migration — #migrateCollections#migrateDocuments#migrateDocumentChunks#migrateTransitionalChatCollections#migrateServersConfig (legacy-data-migrator.ts:41-433), each guarded by a this.state.migrated.<name> flag persisted via Stateful.Persistable so a completed stage is skipped on next launch (legacy-data-migrator.ts:46-48,85-90). Document-chunk vectors are pulled not from SQLite but from a third store, a legacy LanceDB table (context.legacyLanceDB.openTable("knowledge"), legacy-data-migrator.ts:202-291) — so the pre-migration app actually spanned three storage engines (SQLite for structured data, LanceDB for vectors, a JSON servers-config file) before consolidating toward PGlite+pgvector.
  • Chats, messages, and bookmarks are the visible gap. LegacyDataMigrator.State["migrated"] (legacy-data-migrator.ts:518-526) only tracks "collections" | "documents" | "documentChunks" | "transitionChatCollections" | "serversConfig" — there is no messages or bookmarks key. Combined with useBookmarkStore.ts and SearchDialog.tsx both still calling window.electron.db.* (the legacy IPC verbs, not a Drizzle/PGlite path), this is direct proof the conversation-history domain has not crossed over yet, even though the underlying infrastructure (PGlite, migrator, legacyId pattern) is already built and working for other domains.

Full-text search — two call sites, same technique, no FTS engine

  • Global search across all chats (src/renderer/components/SearchDialog.tsx:109-146) splits the query on whitespace into keywords, builds one (prompt like ? OR reply like ?) clause per keyword ANDed together, wraps each keyword in %...%, and runs it via window.electron.db.all(sql, params) against the legacy messages table — SELECT id, chatId, prompt, reply FROM messages WHERE ... ORDER BY messages.createdAt ASC LIMIT 10. Matched snippets are then extracted client-side by extractMatchedSnippet() (SearchDialog.tsx:32-87), which does its own substring-index math to build a highlighted window around each hit and merges overlapping windows — a hand-built excerpt/highlight system operating on the raw strings, not on any search-engine-provided ranking or offsets.
  • Bookmark search (src/stores/useBookmarkStore.ts:162-194, fetchBookmarks) is the same pattern applied to a single combined keyword string rather than tokenized keywords: (prompt like ? or reply like ? or memo like ?) with one %keyword% parameter reused three times, ANDed with an optional favorite = ? filter, ORDER BY createdAt DESC.
  • No SQLite FTS5 virtual table, no Postgres tsvector/to_tsquery, no ranking function anywhere in src/main or src/renderer. Both search paths are LIKE-based substring scans against un-indexed text columns (the messages/bookmarks table definitions in sqlite.ts have no index/virtual-table statements for prompt/reply/memo) — correctness-first, not scale-first. This is consistent with 5ire’s position as a single-user local desktop app rather than a searchable-at-scale knowledge base; the newer PGlite side does get proper indexing (tables.ts has index()/uniqueIndex() calls throughout), just not yet for conversation text.

Bookmarks — a durable snapshot decoupled from the source message

  • Bookmarks copy content rather than reference it live. The legacy bookmarks table (src/main/sqlite.ts:88-109) stores its own prompt, reply, reasoning, temperature, model, memo, citedFiles, citedChunks columns — a full snapshot of the turn, not a foreign key alone — with a UNIQUE ("msgId") constraint (sqlite.ts:105) that still ties it back to the originating message for de-duplication (one bookmark per message) while remaining independently readable. The README states this design intent directly: “You can bookmark each conversation, and even if the original messages are deleted, the saved bookmarked content remains unaffected” (README.md, Bookmarks section).
  • Bookmark store is a thin Zustand wrapper over the same raw-SQL IPC bridge as search. useBookmarkStore.ts builds INSERT/UPDATE/DELETE SQL by hand from whichever fields are present (createBookmark, updateBookmark, sqlite.ts:58-131 — note the ?.repeat(columns.length) placeholder-generation idiom at useBookmarkStore.ts:67), with client-side state (bookmarks, favorites arrays) kept in sync manually after each mutation rather than via any live-query mechanism — a contrast with the new PGlite MCPServersManager/MCPConnectionsManager, which get automatic UI updates for free from driver.live.query/driver.live.changes.
  • Favorites are just a favorite boolean column on the same table, not a separate collection — loadFavorites() calls the same fetchBookmarks({ favorite: true }) path (useBookmarkStore.ts:200-208).

MCP client — SDK-native transports, DB-driven reconnection, and a community marketplace

  • Two real transports, chosen by a transport enum column, not string-sniffing a URL. serverTransport = pgEnum(..., ["stdio", "http-streamable"]) (tables.ts:579) and MCPConnectionsManager.#transport() (mcp-connections-manager.ts:72-97) builds either a StdioClientTransport (parsing endpoint as a shell command line via parseCommandLine, passing config as env vars) or a StreamableHTTPClientTransport (treating endpoint as a URL, config as headers) — explicitly noting in a comment that StreamableHTTPClientTransport now subsumes what used to require a separate SSE transport (mcp-connections-manager.ts:94-96).
  • Connection lifecycle is a real state machine with retry, abort, and error surfacing, not fire-and-forget. #connect() (mcp-connections-manager.ts:104-204) retries up to 3 times with a linearly increasing delay (1000 * retries ms), threads an AbortController through so a server removed mid-connect cleans up correctly, and lands in one of three states — connecting | connected | error — each carrying the data relevant to that state (Connection union type, mcp-connections-manager.ts:367-421). getConnectedOrThrow() (mcp-connections-manager.ts:328-348) gives callers actionable per-state error messages rather than a generic null-check.
  • Server config changes reconnect automatically via the database’s live-query changefeed — the clearest example in this study of persistence and MCP-connection-management being unified through one reactive primitive. init() (mcp-connections-manager.ts:244-321) runs a Drizzle query for active servers, then wraps it in driver.live.changes(...) (PGlite’s live-query extension) keyed by id; on every emitted change it diffs __changed_columns__, decides whether the effective server snapshot actually differs (isEqual), and disconnects+reconnects only when it does. Toggling a server’s active flag or editing its endpoint/config from the UI needs no explicit “apply” step in the connections manager — it’s driven entirely by the DB emitting a row change.
  • Short numeric IDs exist purely for LLM-facing tool-call ergonomics. MCPServersManager maintains a shortIds: Map<string, number> (mcp-servers-manager.ts:402-411) assigning small monotonic integers to each UUID server ID as servers are created — presumably so tool-call URIs or prompts shown to a model don’t need to carry full UUIDs (getShortId/getIdFromShortId, mcp-servers-manager.ts:302-323).
  • Tools are addressed by a custom URI scheme layering connection + tool name. MCPToolsManager.#formatToolURI() builds tool:${connectionId}/${encodeURIComponent(tool.name)} (mcp-tools-manager.ts:30-32), with a matching #parseToolURI — giving every tool call a single string identifier that round-trips back to both which server and which tool, capped at MAX_TOOLS_PAGE = 10 pages of pagination per server (mcp-tools-manager.ts:15).
  • The marketplace is an external static JSON feed, not an in-app registry. useMCPServerMarketStore.fetchServers() (src/stores/useMCPServerMarketStore.ts:22-43) fetches https://mcpsvr.com/servers.json directly from the renderer process (a plain fetch, no IPC involved), caches the result plus an updatedAt timestamp in Zustand state with a REMOTE_CONFIG_TTL of 1000 * 60 * 60 * 24 (24 hours), and only re-fetches when force is passed or the cache has expired. ToolMarketDrawer.tsx (src/renderer/pages/tool/MarketDrawer.tsx:36-63) does client-side filtering across name/description against space-split search terms, and its “Submit” button (MarketDrawer.tsx:93-98) links out to https://github.com/nanbingxyz/mcpsvr — the marketplace’s own repo — for community members to add their server. onInstall (wired at src/renderer/pages/tool/index.tsx:149) ultimately calls the same createServer path as manual configuration, writing a new row into the shared servers table that MCPConnectionsManager is already watching — so “install from marketplace” and “hand-configure a server” are the exact same code path downstream of the drawer.

What’s inside this submodule

PathWhat’s there
src/main/sqlite.tsLegacy better-sqlite3 store — table DDL, ALTER TABLE-based ad hoc migrations, the 4-verb raw-SQL IPC bridge (db-all/db-run/db-transaction/db-get)
src/main/services/legacy-data-migrator.tsLegacyDataMigrator — five-stage SQLite/LanceDB → PGlite migration, legacyId-keyed idempotency, persisted per-stage completion flags
src/main/database/index.tsDatabase — PGlite (NodeFS, vector+live extensions) + Drizzle client construction, stale live-query cleanup, Drizzle-generated migrations
src/main/database/schema/tables.tsThe new Postgres-flavored schema — collection/document/documentChunk (pgvector HNSW), conversation/turn, provider, server (MCP), commented-out bookmark stub
src/main/services/mcp-connections-manager.tsMCPConnectionsManager — SDK transports, retry/abort connection state machine, live-query-driven auto-reconnect
src/main/services/mcp-servers-manager.tsMCPServersManager — CRUD over the servers table, live queries for UI, short-ID assignment
src/main/services/mcp-tools-manager.tsMCPToolsManager — tool discovery/pagination, tool: URI scheme, tool execution
src/main/services/mcp-prompts-manager.ts, mcp-resources-manager.ts, mcp-completion-handler.ts, mcp-content-converter.tsRemaining MCP capability managers (prompts, resources, completions) and content-block conversion
src/stores/useMCPServerMarketStore.tsMarketplace client — fetch('https://mcpsvr.com/servers.json'), 24h TTL cache
src/renderer/pages/tool/MarketDrawer.tsx, index.tsxMarketplace browse/search/install UI; install path converges on the same createServer call as manual config
src/stores/useBookmarkStore.tsBookmark CRUD — hand-built SQL over the legacy IPC bridge, favorite-as-boolean-column
src/renderer/components/SearchDialog.tsxGlobal chat search — LIKE-based, hand-rolled snippet highlighting
src/intellichat/Chat-turn assembly, MCP content-block conversion (mcp/ContentBlockConverter.ts), reader/service abstractions
LICENSEModified Apache-2.0 — the commercial-use and brand-protection carve-outs
INSTALLATION.md, DEVELOPMENT.mdSetup docs — Python/Node/uv prerequisites for MCP servers, dev environment

If you read three files: src/main/services/legacy-data-migrator.ts (the migration’s actual shape and its gaps), src/main/services/mcp-connections-manager.ts (the cleanest MCP-client + live-query integration in this study), and src/main/database/schema/tables.ts (what has and hasn’t crossed over — the commented-out bookmark stub is the tell).

Mental model for using it well

  • Treat 5ire as two codebases occupying one repo. Anything touching knowledge/RAG, providers, or MCP servers is on the modern PGlite+Drizzle+live-query stack; anything touching chats, messages, bookmarks, or search is still on raw-SQL SQLite reached through a stringly-typed IPC bridge. Don’t assume a pattern found in one domain (e.g., live-query reactivity) applies to the other yet.
  • legacyId + onConflictDoNothing is the reusable migration idiom worth lifting. A nullable, uniquely-indexed legacyId column on the new table lets a migration re-run safely and be resumed/interrupted without duplicate rows — cleaner than a one-shot “big bang” migration script.
  • MCP server config-as-database-row, watched via live query, is the reusable MCP-host idiom. Rather than an explicit “reconnect” action, treat server activation/config edits as ordinary row mutations and let a driver.live.changes subscriber own the connect/disconnect side effects — this collapses “server management UI” and “connection manager” into one reactive pipeline.
  • The marketplace is intentionally dumb — a static JSON feed plus client-side filtering — and that’s the point. No backend, no server-side search index, no submission workflow inside the app; new entries land via PRs to a separate mcpsvr repo. Don’t over-build a marketplace surface if a static feed with a TTL cache covers the actual need.
  • Read the license before treating anything here as a drop-in dependency. The “Modified Apache-2.0” terms gate commercial redistribution and enterprise use — this is reference material for architecture, not a library to vendor.

When NOT to reach for this

  • You want a single, finished persistence architecture to copy wholesale. 5ire’s database layer is mid-migration by its own admission (the migrator’s incomplete migrated state, the commented-out bookmark table) — copy the migration technique, not the current end-state, unless you specifically want the two-tier legacy+modern split as a template for your own gradual migration.
  • You need FTS-quality conversation search out of the box. Both search surfaces (SearchDialog.tsx, useBookmarkStore.ts) are LIKE-scan implementations with hand-rolled highlighting — adequate for a single user’s local chat history, not a pattern to scale past that.
  • You need a permissive-license reference implementation to fork freely. The Modified Apache-2.0 terms impose real constraints (brand use, enterprise-scale commercial licensing) that MIT-licensed siblings in this study (e.g. Kaas) don’t have.
  • You want a Tauri-shell reference or a dual-shell (Tauri+Electron) comparison. 5ire is Electron-only via the .erb scaffold; for shell-strategy comparisons, look to dive (Tauri+Electron dual) instead.

How this compares to the rest of the study

Axis5irediveanything-llm
ShellElectron (.erb scaffold) onlyTauri and Electron (dual shell)Electron only
LicenseModified Apache-2.0 (source-available, commercial-use carve-outs)(see Profile__Dive.md)(see Profile__Anything-Llm.md)
PersistenceTwo-tier: legacy better-sqlite3 (chats/messages/bookmarks, raw-SQL IPC) + new PGlite/Drizzle/pgvector (knowledge, providers, MCP servers), migration in progress via legacyId-keyed idempotent copier(see Profile__Dive.md) — MCP-host comparison point(see Profile__Anything-Llm.md) — workspace-switching comparison point
Full-text searchHand-rolled LIKE '%term%' scans over SQLite messages/bookmarks, client-side snippet highlighting; no FTS5/tsvector
BookmarksFull-content snapshot per message (survives source-message deletion), UNIQUE(msgId), favorite-as-boolean-column, no live-query sync
MCP clientSDK-native (@modelcontextprotocol/sdk), stdio + streamable-HTTP transports, retry/abort state machine, live-query-driven auto-reconnect on server-row changeMCP host (dual-shell) — see sibling profile
MCP marketplaceExternal static JSON feed (mcpsvr.com/servers.json, own community repo), 24h client-side TTL cache, install path converges on manual-config createServer
Reactive UI wiringPGlite’s live.query/live.changes extension powers server-list and connection-state UI automatically; legacy-domain stores (bookmarks) still update state manually after each mutation
Best fitStudying a live SQLite→Postgres(WASM) migration in a shipping desktop app, and/or the most complete MCP-client+marketplace pairing in this studyCross-shell (Tauri/Electron) MCP-host comparisonWorkspace-switching UX comparison

5ire’s load-bearing contribution to this study is a real, in-progress persistence migration (SQLite/LanceDB → PGlite/pgvector) caught mid-flight in the source tree, paired with the study’s most complete MCP-client implementation — SDK-native transports, a retry/abort connection state machine, and reconnection driven entirely by the new database’s live-query changefeed — plus a working, minimal community marketplace (a static JSON feed with a TTL cache) that any MCP-client-building project could copy almost verbatim.

One-line summary

5ire is an Electron desktop MCP client caught mid-migration between two persistence stacks — a legacy better-sqlite3 file serving chats, messages, and bookmarks through a raw-SQL IPC bridge with LIKE-scan search, and a newer PGlite/Drizzle/pgvector database that already owns knowledge-base RAG and MCP server config, copied over by an idempotent legacyId-keyed migrator that hasn’t yet reached conversation history — while its MCP-client layer is the study’s most complete: SDK-native stdio/streamable-HTTP transports, a retry-and-abort connection state machine, reconnection driven automatically by the database’s own live-query changefeed, and a real community marketplace (a static mcpsvr.com/servers.json feed, 24-hour cached) whose “install” button writes into the very same servers table the connection manager is already watching.