@frontmcp/storage-sqlite
Advanced tools
| /** | ||
| * SQLite Task Store | ||
| * | ||
| * Implements the FrontMCP `TaskStore` interface on top of better-sqlite3. | ||
| * Suitable for single-host deployments — including the CLI runner — where | ||
| * tasks outlive a single HTTP or stdio session but don't need cross-node | ||
| * pub/sub. Pub/sub is served by an in-process EventEmitter (same pattern as | ||
| * SqliteElicitationStore). | ||
| * | ||
| * Schema: | ||
| * ```sql | ||
| * CREATE TABLE IF NOT EXISTS mcp_tasks ( | ||
| * task_id TEXT PRIMARY KEY, | ||
| * session_id TEXT NOT NULL, | ||
| * status TEXT NOT NULL, | ||
| * expires_at INTEGER NOT NULL, | ||
| * created_at INTEGER NOT NULL, | ||
| * updated_at INTEGER NOT NULL, | ||
| * executor_pid INTEGER, | ||
| * record_json TEXT NOT NULL | ||
| * ); | ||
| * ``` | ||
| * | ||
| * @module storage-sqlite/sqlite-task.store | ||
| */ | ||
| import type Database from 'better-sqlite3'; | ||
| import type { SqliteStorageOptions } from './sqlite.options'; | ||
| export interface TaskJsonRpcError { | ||
| code: number; | ||
| message: string; | ||
| data?: unknown; | ||
| } | ||
| export type TaskOutcome = { | ||
| kind: 'ok'; | ||
| data: unknown; | ||
| } | { | ||
| kind: 'error'; | ||
| error: TaskJsonRpcError; | ||
| }; | ||
| export type TaskStatus = 'working' | 'input_required' | 'completed' | 'failed' | 'cancelled'; | ||
| export interface TaskRecord { | ||
| taskId: string; | ||
| sessionId: string; | ||
| status: TaskStatus; | ||
| statusMessage?: string; | ||
| createdAt: string; | ||
| lastUpdatedAt: string; | ||
| ttlMs: number | null; | ||
| pollIntervalMs?: number; | ||
| expiresAt: number; | ||
| request: { | ||
| method: 'tools/call'; | ||
| params: Record<string, unknown>; | ||
| }; | ||
| outcome?: TaskOutcome; | ||
| progressToken?: string | number; | ||
| executor?: { | ||
| host: 'in-process' | 'cli'; | ||
| pid?: number; | ||
| spawnedAt?: string; | ||
| }; | ||
| } | ||
| export type TaskTerminalCallback = (record: TaskRecord) => void; | ||
| export type TaskCancelCallback = () => void; | ||
| export type TaskUnsubscribe = () => Promise<void>; | ||
| export interface TaskListPage { | ||
| tasks: TaskRecord[]; | ||
| nextCursor?: string; | ||
| } | ||
| export interface TaskStoreInterface { | ||
| create(record: TaskRecord): Promise<void>; | ||
| get(taskId: string, sessionId: string): Promise<TaskRecord | null>; | ||
| update(taskId: string, sessionId: string, patch: Partial<TaskRecord>): Promise<TaskRecord | null>; | ||
| delete(taskId: string, sessionId: string): Promise<void>; | ||
| list(sessionId: string, opts?: { | ||
| cursor?: string; | ||
| pageSize?: number; | ||
| }): Promise<TaskListPage>; | ||
| subscribeTerminal(taskId: string, sessionId: string, cb: TaskTerminalCallback): Promise<TaskUnsubscribe>; | ||
| publishTerminal(record: TaskRecord): Promise<void>; | ||
| subscribeCancel(taskId: string, sessionId: string, cb: TaskCancelCallback): Promise<TaskUnsubscribe>; | ||
| publishCancel(taskId: string, sessionId: string): Promise<void>; | ||
| destroy?(): Promise<void>; | ||
| } | ||
| export interface SqliteTaskLogger { | ||
| debug?: (message: string, meta?: Record<string, unknown>) => void; | ||
| warn?: (message: string, meta?: Record<string, unknown>) => void; | ||
| error?: (message: string, meta?: Record<string, unknown>) => void; | ||
| } | ||
| /** Returns `true` if the process with the given PID is alive. */ | ||
| export type ProcessLivenessProbe = (pid: number) => boolean; | ||
| export interface SqliteTaskStoreOptions extends SqliteStorageOptions { | ||
| logger?: SqliteTaskLogger; | ||
| /** | ||
| * Override the PID liveness check (primarily for tests). | ||
| * Defaults to `process.kill(pid, 0)` semantics. | ||
| */ | ||
| livenessProbe?: ProcessLivenessProbe; | ||
| } | ||
| export declare class SqliteTaskStore implements TaskStoreInterface { | ||
| private readonly db; | ||
| private readonly emitter; | ||
| private readonly logger?; | ||
| private readonly liveness; | ||
| private readonly cleanupTimer; | ||
| /** | ||
| * AES-256-GCM key derived from `options.encryption.secret`, applied to the | ||
| * `record_json` column on write and reversed on read. Other columns | ||
| * (`task_id`, `session_id`, `status`, `expires_at`, `executor_pid`) are kept | ||
| * in plaintext so the indexes remain usable. | ||
| */ | ||
| private readonly encryptionKey; | ||
| private stmts; | ||
| constructor(options: SqliteTaskStoreOptions); | ||
| private initSchema; | ||
| private prepareStatements; | ||
| private prepared; | ||
| create(record: TaskRecord): Promise<void>; | ||
| get(taskId: string, sessionId: string): Promise<TaskRecord | null>; | ||
| update(taskId: string, sessionId: string, patch: Partial<TaskRecord>): Promise<TaskRecord | null>; | ||
| delete(taskId: string, sessionId: string): Promise<void>; | ||
| list(sessionId: string, opts?: { | ||
| cursor?: string; | ||
| pageSize?: number; | ||
| }): Promise<TaskListPage>; | ||
| subscribeTerminal(taskId: string, _sessionId: string, cb: TaskTerminalCallback): Promise<TaskUnsubscribe>; | ||
| publishTerminal(record: TaskRecord): Promise<void>; | ||
| subscribeCancel(taskId: string, _sessionId: string, cb: TaskCancelCallback): Promise<TaskUnsubscribe>; | ||
| publishCancel(taskId: string, _sessionId: string): Promise<void>; | ||
| destroy(): Promise<void>; | ||
| purgeExpired(): number; | ||
| getDatabase(): Database.Database; | ||
| private rowToRecord; | ||
| /** | ||
| * Serialize + optionally encrypt a TaskRecord before it lands in the DB. | ||
| * Only the `record_json` column is encrypted; indexed columns stay plaintext. | ||
| */ | ||
| private serializeRecord; | ||
| } | ||
| //# sourceMappingURL=sqlite-task.store.d.ts.map |
| {"version":3,"file":"sqlite-task.store.d.ts","sourceRoot":"","sources":["../src/sqlite-task.store.ts"],"names":[],"mappings":"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"} |
+274
-1
@@ -374,2 +374,274 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, { | ||
| // libs/storage-sqlite/src/sqlite-task.store.ts | ||
| import { EventEmitter as EventEmitter2 } from "node:events"; | ||
| var defaultLivenessProbe = (pid) => { | ||
| try { | ||
| process.kill(pid, 0); | ||
| return true; | ||
| } catch { | ||
| return false; | ||
| } | ||
| }; | ||
| var DEFAULT_PAGE_SIZE = 50; | ||
| var SqliteTaskStore = class { | ||
| db; | ||
| emitter = new EventEmitter2(); | ||
| logger; | ||
| liveness; | ||
| cleanupTimer; | ||
| /** | ||
| * AES-256-GCM key derived from `options.encryption.secret`, applied to the | ||
| * `record_json` column on write and reversed on read. Other columns | ||
| * (`task_id`, `session_id`, `status`, `expires_at`, `executor_pid`) are kept | ||
| * in plaintext so the indexes remain usable. | ||
| */ | ||
| encryptionKey = null; | ||
| stmts = null; | ||
| constructor(options) { | ||
| const BetterSqlite3 = __require("better-sqlite3"); | ||
| try { | ||
| this.db = new BetterSqlite3(options.path); | ||
| } catch (err) { | ||
| const message = err instanceof Error ? err.message : String(err); | ||
| throw new Error(`SqliteTaskStore: failed to open database at "${options.path}": ${message}`); | ||
| } | ||
| if (options.walMode !== false) { | ||
| this.db.pragma("journal_mode = WAL"); | ||
| } | ||
| this.logger = options.logger; | ||
| this.liveness = options.livenessProbe ?? defaultLivenessProbe; | ||
| if (options.encryption?.secret) { | ||
| this.encryptionKey = deriveEncryptionKey(options.encryption.secret); | ||
| } | ||
| this.emitter.setMaxListeners(200); | ||
| this.initSchema(); | ||
| this.prepareStatements(); | ||
| const cleanupInterval = options.ttlCleanupIntervalMs ?? 6e4; | ||
| if (cleanupInterval > 0) { | ||
| this.cleanupTimer = setInterval(() => this.purgeExpired(), cleanupInterval); | ||
| this.cleanupTimer.unref?.(); | ||
| } else { | ||
| this.cleanupTimer = null; | ||
| } | ||
| } | ||
| initSchema() { | ||
| this.db.exec(` | ||
| CREATE TABLE IF NOT EXISTS mcp_tasks ( | ||
| task_id TEXT PRIMARY KEY, | ||
| session_id TEXT NOT NULL, | ||
| status TEXT NOT NULL, | ||
| expires_at INTEGER NOT NULL, | ||
| created_at INTEGER NOT NULL, | ||
| updated_at INTEGER NOT NULL, | ||
| executor_pid INTEGER, | ||
| record_json TEXT NOT NULL | ||
| ); | ||
| CREATE INDEX IF NOT EXISTS idx_mcp_tasks_session ON mcp_tasks (session_id); | ||
| CREATE INDEX IF NOT EXISTS idx_mcp_tasks_status ON mcp_tasks (status); | ||
| CREATE INDEX IF NOT EXISTS idx_mcp_tasks_expires ON mcp_tasks (expires_at); | ||
| `); | ||
| } | ||
| prepareStatements() { | ||
| this.stmts = { | ||
| insert: this.db.prepare( | ||
| "INSERT INTO mcp_tasks (task_id, session_id, status, expires_at, created_at, updated_at, executor_pid, record_json) VALUES (?, ?, ?, ?, ?, ?, ?, ?)" | ||
| ), | ||
| get: this.db.prepare("SELECT * FROM mcp_tasks WHERE task_id = ? AND session_id = ?"), | ||
| update: this.db.prepare( | ||
| "UPDATE mcp_tasks SET status = ?, expires_at = ?, updated_at = ?, executor_pid = ?, record_json = ? WHERE task_id = ? AND session_id = ?" | ||
| ), | ||
| del: this.db.prepare("DELETE FROM mcp_tasks WHERE task_id = ? AND session_id = ?"), | ||
| listBySession: this.db.prepare( | ||
| "SELECT * FROM mcp_tasks WHERE session_id = ? AND expires_at > ? ORDER BY created_at ASC, task_id ASC LIMIT ? OFFSET ?" | ||
| ), | ||
| countBySession: this.db.prepare("SELECT COUNT(*) as n FROM mcp_tasks WHERE session_id = ? AND expires_at > ?"), | ||
| cleanup: this.db.prepare("DELETE FROM mcp_tasks WHERE expires_at <= ?") | ||
| }; | ||
| } | ||
| prepared() { | ||
| if (!this.stmts) throw new Error("SqliteTaskStore: prepared statements not initialized"); | ||
| return this.stmts; | ||
| } | ||
| // ───────────────────── CRUD ───────────────────── | ||
| async create(record) { | ||
| if (record.expiresAt <= Date.now()) { | ||
| this.logger?.warn?.("[SqliteTaskStore] create: record already expired", { taskId: record.taskId }); | ||
| return; | ||
| } | ||
| this.prepared().insert.run( | ||
| record.taskId, | ||
| record.sessionId, | ||
| record.status, | ||
| record.expiresAt, | ||
| new Date(record.createdAt).getTime(), | ||
| new Date(record.lastUpdatedAt).getTime(), | ||
| record.executor?.pid ?? null, | ||
| this.serializeRecord(record) | ||
| ); | ||
| } | ||
| async get(taskId, sessionId) { | ||
| const row = this.prepared().get.get(taskId, sessionId); | ||
| if (!row) return null; | ||
| if (row.expires_at <= Date.now()) { | ||
| this.prepared().del.run(taskId, sessionId); | ||
| return null; | ||
| } | ||
| const record = this.rowToRecord(row); | ||
| if ((record.status === "working" || record.status === "input_required") && record.executor?.host === "cli" && typeof record.executor.pid === "number" && !this.liveness(record.executor.pid)) { | ||
| const patched = await this.update(taskId, sessionId, { | ||
| status: "failed", | ||
| statusMessage: "Task runner exited before completing the task" | ||
| }); | ||
| if (patched) { | ||
| await this.publishTerminal(patched); | ||
| return patched; | ||
| } | ||
| } | ||
| return record; | ||
| } | ||
| async update(taskId, sessionId, patch) { | ||
| const row = this.prepared().get.get(taskId, sessionId); | ||
| if (!row) return null; | ||
| if (row.expires_at <= Date.now()) { | ||
| this.prepared().del.run(taskId, sessionId); | ||
| return null; | ||
| } | ||
| const existing = this.rowToRecord(row); | ||
| const merged = { | ||
| ...existing, | ||
| ...patch, | ||
| taskId: existing.taskId, | ||
| sessionId: existing.sessionId, | ||
| createdAt: existing.createdAt, | ||
| lastUpdatedAt: (/* @__PURE__ */ new Date()).toISOString() | ||
| }; | ||
| if (merged.expiresAt <= Date.now()) { | ||
| this.prepared().del.run(taskId, sessionId); | ||
| return null; | ||
| } | ||
| this.prepared().update.run( | ||
| merged.status, | ||
| merged.expiresAt, | ||
| new Date(merged.lastUpdatedAt).getTime(), | ||
| merged.executor?.pid ?? null, | ||
| this.serializeRecord(merged), | ||
| taskId, | ||
| sessionId | ||
| ); | ||
| return merged; | ||
| } | ||
| async delete(taskId, sessionId) { | ||
| this.prepared().del.run(taskId, sessionId); | ||
| } | ||
| async list(sessionId, opts = {}) { | ||
| const pageSize = Math.max(1, Math.min(opts.pageSize ?? DEFAULT_PAGE_SIZE, 500)); | ||
| const offset = opts.cursor ? decodeCursor(opts.cursor) : 0; | ||
| const rows = this.prepared().listBySession.all(sessionId, Date.now(), pageSize, offset); | ||
| const tasks = []; | ||
| for (const row of rows) { | ||
| let record = this.rowToRecord(row); | ||
| if ((record.status === "working" || record.status === "input_required") && record.executor?.host === "cli" && typeof record.executor.pid === "number" && !this.liveness(record.executor.pid)) { | ||
| const patched = await this.update(record.taskId, sessionId, { | ||
| status: "failed", | ||
| statusMessage: "Task runner exited before completing the task" | ||
| }); | ||
| if (patched) { | ||
| await this.publishTerminal(patched); | ||
| record = patched; | ||
| } | ||
| } | ||
| tasks.push(record); | ||
| } | ||
| const total = this.prepared().countBySession.get(sessionId, Date.now()).n; | ||
| const page = { tasks }; | ||
| if (offset + rows.length < total) { | ||
| page.nextCursor = encodeCursor(offset + rows.length); | ||
| } | ||
| return page; | ||
| } | ||
| // ───────────────────── Pub/Sub (in-process) ───────────────────── | ||
| async subscribeTerminal(taskId, _sessionId, cb) { | ||
| const channel = `terminal:${taskId}`; | ||
| const handler = (record) => cb(record); | ||
| this.emitter.on(channel, handler); | ||
| return async () => { | ||
| this.emitter.removeListener(channel, handler); | ||
| }; | ||
| } | ||
| async publishTerminal(record) { | ||
| this.emitter.emit(`terminal:${record.taskId}`, record); | ||
| } | ||
| async subscribeCancel(taskId, _sessionId, cb) { | ||
| const channel = `cancel:${taskId}`; | ||
| const handler = () => cb(); | ||
| this.emitter.on(channel, handler); | ||
| return async () => { | ||
| this.emitter.removeListener(channel, handler); | ||
| }; | ||
| } | ||
| async publishCancel(taskId, _sessionId) { | ||
| this.emitter.emit(`cancel:${taskId}`); | ||
| } | ||
| // ───────────────────── Misc ───────────────────── | ||
| async destroy() { | ||
| if (this.cleanupTimer) clearInterval(this.cleanupTimer); | ||
| this.emitter.removeAllListeners(); | ||
| try { | ||
| this.db.close(); | ||
| } catch { | ||
| } | ||
| } | ||
| purgeExpired() { | ||
| try { | ||
| const res = this.prepared().cleanup.run(Date.now()); | ||
| return res.changes; | ||
| } catch (err) { | ||
| this.logger?.warn?.("[SqliteTaskStore] purgeExpired failed", { | ||
| error: err instanceof Error ? err.message : String(err) | ||
| }); | ||
| return 0; | ||
| } | ||
| } | ||
| getDatabase() { | ||
| return this.db; | ||
| } | ||
| rowToRecord(row) { | ||
| const plaintext = this.encryptionKey ? decryptValue(this.encryptionKey, row.record_json) : row.record_json; | ||
| return JSON.parse(plaintext); | ||
| } | ||
| /** | ||
| * Serialize + optionally encrypt a TaskRecord before it lands in the DB. | ||
| * Only the `record_json` column is encrypted; indexed columns stay plaintext. | ||
| */ | ||
| serializeRecord(record) { | ||
| const json = JSON.stringify(record); | ||
| return this.encryptionKey ? encryptValue(this.encryptionKey, json) : json; | ||
| } | ||
| }; | ||
| function encodeCursor(offset) { | ||
| return toB64Url(new TextEncoder().encode(JSON.stringify({ offset }))); | ||
| } | ||
| function decodeCursor(cursor) { | ||
| try { | ||
| const bytes = fromB64Url(cursor); | ||
| const parsed = JSON.parse(new TextDecoder().decode(bytes)); | ||
| if (typeof parsed?.offset === "number" && parsed.offset >= 0 && Number.isInteger(parsed.offset)) { | ||
| return parsed.offset; | ||
| } | ||
| } catch { | ||
| } | ||
| return 0; | ||
| } | ||
| function toB64Url(data) { | ||
| const b64 = typeof Buffer !== "undefined" ? Buffer.from(data).toString("base64") : btoa(Array.from(data, (b) => String.fromCodePoint(b)).join("")); | ||
| return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, ""); | ||
| } | ||
| function fromB64Url(data) { | ||
| let b64 = data.replace(/-/g, "+").replace(/_/g, "/"); | ||
| while (b64.length % 4) b64 += "="; | ||
| if (typeof Buffer !== "undefined") return new Uint8Array(Buffer.from(b64, "base64")); | ||
| const bin = atob(b64); | ||
| return Uint8Array.from(bin, (c) => c.codePointAt(0) ?? 0); | ||
| } | ||
| // libs/storage-sqlite/src/sqlite-event.store.ts | ||
@@ -522,3 +794,3 @@ var SqliteEventStore = class { | ||
| // libs/storage-sqlite/src/sqlite.options.ts | ||
| import { z } from "zod"; | ||
| import { z } from "@frontmcp/lazy-zod"; | ||
| var sqliteStorageOptionsSchema = z.object({ | ||
@@ -537,2 +809,3 @@ path: z.string().min(1), | ||
| SqliteSessionStore, | ||
| SqliteTaskStore, | ||
| decryptValue, | ||
@@ -539,0 +812,0 @@ deriveEncryptionKey, |
+3
-3
| { | ||
| "name": "@frontmcp/storage-sqlite", | ||
| "version": "1.0.4", | ||
| "version": "1.1.0-beta.1", | ||
| "description": "SQLite storage backend for FrontMCP - local session, elicitation, and event persistence without Redis", | ||
@@ -47,7 +47,7 @@ "author": "AgentFront <info@agentfront.dev>", | ||
| "dependencies": { | ||
| "@frontmcp/utils": "1.0.4", | ||
| "@frontmcp/utils": "1.1.0-beta.1", | ||
| "better-sqlite3": "^12.6.2" | ||
| }, | ||
| "peerDependencies": { | ||
| "zod": "^4.0.0" | ||
| "@frontmcp/lazy-zod": "1.1.0-beta.1" | ||
| }, | ||
@@ -54,0 +54,0 @@ "devDependencies": { |
+2
-0
@@ -12,2 +12,4 @@ /** | ||
| export type { SqliteElicitationStoreOptions, ElicitationStoreInterface } from './sqlite-elicitation.store'; | ||
| export { SqliteTaskStore } from './sqlite-task.store'; | ||
| export type { SqliteTaskStoreOptions, TaskStoreInterface, TaskRecord as SqliteTaskRecord, TaskStatus as SqliteTaskStatus, TaskOutcome as SqliteTaskOutcome, TaskListPage as SqliteTaskListPage, ProcessLivenessProbe, SqliteTaskLogger, } from './sqlite-task.store'; | ||
| export { SqliteEventStore } from './sqlite-event.store'; | ||
@@ -14,0 +16,0 @@ export type { SqliteEventStoreOptions, EventStoreInterface } from './sqlite-event.store'; |
+1
-1
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAC5D,YAAY,EAAE,yBAAyB,EAAE,qBAAqB,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAClH,OAAO,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AACpE,YAAY,EAAE,6BAA6B,EAAE,yBAAyB,EAAE,MAAM,4BAA4B,CAAC;AAC3G,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AACxD,YAAY,EAAE,uBAAuB,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AACzF,OAAO,EAAE,mBAAmB,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC/E,OAAO,EAAE,0BAA0B,EAAE,MAAM,kBAAkB,CAAC;AAC9D,YAAY,EAAE,oBAAoB,EAAE,yBAAyB,EAAE,0BAA0B,EAAE,MAAM,kBAAkB,CAAC"} | ||
| {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAC5D,YAAY,EAAE,yBAAyB,EAAE,qBAAqB,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAClH,OAAO,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AACpE,YAAY,EAAE,6BAA6B,EAAE,yBAAyB,EAAE,MAAM,4BAA4B,CAAC;AAC3G,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACtD,YAAY,EACV,sBAAsB,EACtB,kBAAkB,EAClB,UAAU,IAAI,gBAAgB,EAC9B,UAAU,IAAI,gBAAgB,EAC9B,WAAW,IAAI,iBAAiB,EAChC,YAAY,IAAI,kBAAkB,EAClC,oBAAoB,EACpB,gBAAgB,GACjB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AACxD,YAAY,EAAE,uBAAuB,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AACzF,OAAO,EAAE,mBAAmB,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC/E,OAAO,EAAE,0BAA0B,EAAE,MAAM,kBAAkB,CAAC;AAC9D,YAAY,EAAE,oBAAoB,EAAE,yBAAyB,EAAE,0BAA0B,EAAE,MAAM,kBAAkB,CAAC"} |
+281
-7
@@ -27,2 +27,3 @@ "use strict"; | ||
| SqliteSessionStore: () => SqliteSessionStore, | ||
| SqliteTaskStore: () => SqliteTaskStore, | ||
| decryptValue: () => decryptValue, | ||
@@ -394,2 +395,274 @@ deriveEncryptionKey: () => deriveEncryptionKey, | ||
| // libs/storage-sqlite/src/sqlite-task.store.ts | ||
| var import_node_events2 = require("node:events"); | ||
| var defaultLivenessProbe = (pid) => { | ||
| try { | ||
| process.kill(pid, 0); | ||
| return true; | ||
| } catch { | ||
| return false; | ||
| } | ||
| }; | ||
| var DEFAULT_PAGE_SIZE = 50; | ||
| var SqliteTaskStore = class { | ||
| db; | ||
| emitter = new import_node_events2.EventEmitter(); | ||
| logger; | ||
| liveness; | ||
| cleanupTimer; | ||
| /** | ||
| * AES-256-GCM key derived from `options.encryption.secret`, applied to the | ||
| * `record_json` column on write and reversed on read. Other columns | ||
| * (`task_id`, `session_id`, `status`, `expires_at`, `executor_pid`) are kept | ||
| * in plaintext so the indexes remain usable. | ||
| */ | ||
| encryptionKey = null; | ||
| stmts = null; | ||
| constructor(options) { | ||
| const BetterSqlite3 = require("better-sqlite3"); | ||
| try { | ||
| this.db = new BetterSqlite3(options.path); | ||
| } catch (err) { | ||
| const message = err instanceof Error ? err.message : String(err); | ||
| throw new Error(`SqliteTaskStore: failed to open database at "${options.path}": ${message}`); | ||
| } | ||
| if (options.walMode !== false) { | ||
| this.db.pragma("journal_mode = WAL"); | ||
| } | ||
| this.logger = options.logger; | ||
| this.liveness = options.livenessProbe ?? defaultLivenessProbe; | ||
| if (options.encryption?.secret) { | ||
| this.encryptionKey = deriveEncryptionKey(options.encryption.secret); | ||
| } | ||
| this.emitter.setMaxListeners(200); | ||
| this.initSchema(); | ||
| this.prepareStatements(); | ||
| const cleanupInterval = options.ttlCleanupIntervalMs ?? 6e4; | ||
| if (cleanupInterval > 0) { | ||
| this.cleanupTimer = setInterval(() => this.purgeExpired(), cleanupInterval); | ||
| this.cleanupTimer.unref?.(); | ||
| } else { | ||
| this.cleanupTimer = null; | ||
| } | ||
| } | ||
| initSchema() { | ||
| this.db.exec(` | ||
| CREATE TABLE IF NOT EXISTS mcp_tasks ( | ||
| task_id TEXT PRIMARY KEY, | ||
| session_id TEXT NOT NULL, | ||
| status TEXT NOT NULL, | ||
| expires_at INTEGER NOT NULL, | ||
| created_at INTEGER NOT NULL, | ||
| updated_at INTEGER NOT NULL, | ||
| executor_pid INTEGER, | ||
| record_json TEXT NOT NULL | ||
| ); | ||
| CREATE INDEX IF NOT EXISTS idx_mcp_tasks_session ON mcp_tasks (session_id); | ||
| CREATE INDEX IF NOT EXISTS idx_mcp_tasks_status ON mcp_tasks (status); | ||
| CREATE INDEX IF NOT EXISTS idx_mcp_tasks_expires ON mcp_tasks (expires_at); | ||
| `); | ||
| } | ||
| prepareStatements() { | ||
| this.stmts = { | ||
| insert: this.db.prepare( | ||
| "INSERT INTO mcp_tasks (task_id, session_id, status, expires_at, created_at, updated_at, executor_pid, record_json) VALUES (?, ?, ?, ?, ?, ?, ?, ?)" | ||
| ), | ||
| get: this.db.prepare("SELECT * FROM mcp_tasks WHERE task_id = ? AND session_id = ?"), | ||
| update: this.db.prepare( | ||
| "UPDATE mcp_tasks SET status = ?, expires_at = ?, updated_at = ?, executor_pid = ?, record_json = ? WHERE task_id = ? AND session_id = ?" | ||
| ), | ||
| del: this.db.prepare("DELETE FROM mcp_tasks WHERE task_id = ? AND session_id = ?"), | ||
| listBySession: this.db.prepare( | ||
| "SELECT * FROM mcp_tasks WHERE session_id = ? AND expires_at > ? ORDER BY created_at ASC, task_id ASC LIMIT ? OFFSET ?" | ||
| ), | ||
| countBySession: this.db.prepare("SELECT COUNT(*) as n FROM mcp_tasks WHERE session_id = ? AND expires_at > ?"), | ||
| cleanup: this.db.prepare("DELETE FROM mcp_tasks WHERE expires_at <= ?") | ||
| }; | ||
| } | ||
| prepared() { | ||
| if (!this.stmts) throw new Error("SqliteTaskStore: prepared statements not initialized"); | ||
| return this.stmts; | ||
| } | ||
| // ───────────────────── CRUD ───────────────────── | ||
| async create(record) { | ||
| if (record.expiresAt <= Date.now()) { | ||
| this.logger?.warn?.("[SqliteTaskStore] create: record already expired", { taskId: record.taskId }); | ||
| return; | ||
| } | ||
| this.prepared().insert.run( | ||
| record.taskId, | ||
| record.sessionId, | ||
| record.status, | ||
| record.expiresAt, | ||
| new Date(record.createdAt).getTime(), | ||
| new Date(record.lastUpdatedAt).getTime(), | ||
| record.executor?.pid ?? null, | ||
| this.serializeRecord(record) | ||
| ); | ||
| } | ||
| async get(taskId, sessionId) { | ||
| const row = this.prepared().get.get(taskId, sessionId); | ||
| if (!row) return null; | ||
| if (row.expires_at <= Date.now()) { | ||
| this.prepared().del.run(taskId, sessionId); | ||
| return null; | ||
| } | ||
| const record = this.rowToRecord(row); | ||
| if ((record.status === "working" || record.status === "input_required") && record.executor?.host === "cli" && typeof record.executor.pid === "number" && !this.liveness(record.executor.pid)) { | ||
| const patched = await this.update(taskId, sessionId, { | ||
| status: "failed", | ||
| statusMessage: "Task runner exited before completing the task" | ||
| }); | ||
| if (patched) { | ||
| await this.publishTerminal(patched); | ||
| return patched; | ||
| } | ||
| } | ||
| return record; | ||
| } | ||
| async update(taskId, sessionId, patch) { | ||
| const row = this.prepared().get.get(taskId, sessionId); | ||
| if (!row) return null; | ||
| if (row.expires_at <= Date.now()) { | ||
| this.prepared().del.run(taskId, sessionId); | ||
| return null; | ||
| } | ||
| const existing = this.rowToRecord(row); | ||
| const merged = { | ||
| ...existing, | ||
| ...patch, | ||
| taskId: existing.taskId, | ||
| sessionId: existing.sessionId, | ||
| createdAt: existing.createdAt, | ||
| lastUpdatedAt: (/* @__PURE__ */ new Date()).toISOString() | ||
| }; | ||
| if (merged.expiresAt <= Date.now()) { | ||
| this.prepared().del.run(taskId, sessionId); | ||
| return null; | ||
| } | ||
| this.prepared().update.run( | ||
| merged.status, | ||
| merged.expiresAt, | ||
| new Date(merged.lastUpdatedAt).getTime(), | ||
| merged.executor?.pid ?? null, | ||
| this.serializeRecord(merged), | ||
| taskId, | ||
| sessionId | ||
| ); | ||
| return merged; | ||
| } | ||
| async delete(taskId, sessionId) { | ||
| this.prepared().del.run(taskId, sessionId); | ||
| } | ||
| async list(sessionId, opts = {}) { | ||
| const pageSize = Math.max(1, Math.min(opts.pageSize ?? DEFAULT_PAGE_SIZE, 500)); | ||
| const offset = opts.cursor ? decodeCursor(opts.cursor) : 0; | ||
| const rows = this.prepared().listBySession.all(sessionId, Date.now(), pageSize, offset); | ||
| const tasks = []; | ||
| for (const row of rows) { | ||
| let record = this.rowToRecord(row); | ||
| if ((record.status === "working" || record.status === "input_required") && record.executor?.host === "cli" && typeof record.executor.pid === "number" && !this.liveness(record.executor.pid)) { | ||
| const patched = await this.update(record.taskId, sessionId, { | ||
| status: "failed", | ||
| statusMessage: "Task runner exited before completing the task" | ||
| }); | ||
| if (patched) { | ||
| await this.publishTerminal(patched); | ||
| record = patched; | ||
| } | ||
| } | ||
| tasks.push(record); | ||
| } | ||
| const total = this.prepared().countBySession.get(sessionId, Date.now()).n; | ||
| const page = { tasks }; | ||
| if (offset + rows.length < total) { | ||
| page.nextCursor = encodeCursor(offset + rows.length); | ||
| } | ||
| return page; | ||
| } | ||
| // ───────────────────── Pub/Sub (in-process) ───────────────────── | ||
| async subscribeTerminal(taskId, _sessionId, cb) { | ||
| const channel = `terminal:${taskId}`; | ||
| const handler = (record) => cb(record); | ||
| this.emitter.on(channel, handler); | ||
| return async () => { | ||
| this.emitter.removeListener(channel, handler); | ||
| }; | ||
| } | ||
| async publishTerminal(record) { | ||
| this.emitter.emit(`terminal:${record.taskId}`, record); | ||
| } | ||
| async subscribeCancel(taskId, _sessionId, cb) { | ||
| const channel = `cancel:${taskId}`; | ||
| const handler = () => cb(); | ||
| this.emitter.on(channel, handler); | ||
| return async () => { | ||
| this.emitter.removeListener(channel, handler); | ||
| }; | ||
| } | ||
| async publishCancel(taskId, _sessionId) { | ||
| this.emitter.emit(`cancel:${taskId}`); | ||
| } | ||
| // ───────────────────── Misc ───────────────────── | ||
| async destroy() { | ||
| if (this.cleanupTimer) clearInterval(this.cleanupTimer); | ||
| this.emitter.removeAllListeners(); | ||
| try { | ||
| this.db.close(); | ||
| } catch { | ||
| } | ||
| } | ||
| purgeExpired() { | ||
| try { | ||
| const res = this.prepared().cleanup.run(Date.now()); | ||
| return res.changes; | ||
| } catch (err) { | ||
| this.logger?.warn?.("[SqliteTaskStore] purgeExpired failed", { | ||
| error: err instanceof Error ? err.message : String(err) | ||
| }); | ||
| return 0; | ||
| } | ||
| } | ||
| getDatabase() { | ||
| return this.db; | ||
| } | ||
| rowToRecord(row) { | ||
| const plaintext = this.encryptionKey ? decryptValue(this.encryptionKey, row.record_json) : row.record_json; | ||
| return JSON.parse(plaintext); | ||
| } | ||
| /** | ||
| * Serialize + optionally encrypt a TaskRecord before it lands in the DB. | ||
| * Only the `record_json` column is encrypted; indexed columns stay plaintext. | ||
| */ | ||
| serializeRecord(record) { | ||
| const json = JSON.stringify(record); | ||
| return this.encryptionKey ? encryptValue(this.encryptionKey, json) : json; | ||
| } | ||
| }; | ||
| function encodeCursor(offset) { | ||
| return toB64Url(new TextEncoder().encode(JSON.stringify({ offset }))); | ||
| } | ||
| function decodeCursor(cursor) { | ||
| try { | ||
| const bytes = fromB64Url(cursor); | ||
| const parsed = JSON.parse(new TextDecoder().decode(bytes)); | ||
| if (typeof parsed?.offset === "number" && parsed.offset >= 0 && Number.isInteger(parsed.offset)) { | ||
| return parsed.offset; | ||
| } | ||
| } catch { | ||
| } | ||
| return 0; | ||
| } | ||
| function toB64Url(data) { | ||
| const b64 = typeof Buffer !== "undefined" ? Buffer.from(data).toString("base64") : btoa(Array.from(data, (b) => String.fromCodePoint(b)).join("")); | ||
| return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, ""); | ||
| } | ||
| function fromB64Url(data) { | ||
| let b64 = data.replace(/-/g, "+").replace(/_/g, "/"); | ||
| while (b64.length % 4) b64 += "="; | ||
| if (typeof Buffer !== "undefined") return new Uint8Array(Buffer.from(b64, "base64")); | ||
| const bin = atob(b64); | ||
| return Uint8Array.from(bin, (c) => c.codePointAt(0) ?? 0); | ||
| } | ||
| // libs/storage-sqlite/src/sqlite-event.store.ts | ||
@@ -542,10 +815,10 @@ var SqliteEventStore = class { | ||
| // libs/storage-sqlite/src/sqlite.options.ts | ||
| var import_zod = require("zod"); | ||
| var sqliteStorageOptionsSchema = import_zod.z.object({ | ||
| path: import_zod.z.string().min(1), | ||
| encryption: import_zod.z.object({ | ||
| secret: import_zod.z.string().min(1) | ||
| var import_lazy_zod = require("@frontmcp/lazy-zod"); | ||
| var sqliteStorageOptionsSchema = import_lazy_zod.z.object({ | ||
| path: import_lazy_zod.z.string().min(1), | ||
| encryption: import_lazy_zod.z.object({ | ||
| secret: import_lazy_zod.z.string().min(1) | ||
| }).optional(), | ||
| ttlCleanupIntervalMs: import_zod.z.number().int().nonnegative().optional().default(6e4), | ||
| walMode: import_zod.z.boolean().optional().default(true) | ||
| ttlCleanupIntervalMs: import_lazy_zod.z.number().int().nonnegative().optional().default(6e4), | ||
| walMode: import_lazy_zod.z.boolean().optional().default(true) | ||
| }); | ||
@@ -558,2 +831,3 @@ // Annotate the CommonJS export names for ESM import in node: | ||
| SqliteSessionStore, | ||
| SqliteTaskStore, | ||
| decryptValue, | ||
@@ -560,0 +834,0 @@ deriveEncryptionKey, |
+3
-3
| { | ||
| "name": "@frontmcp/storage-sqlite", | ||
| "version": "1.0.4", | ||
| "version": "1.1.0-beta.1", | ||
| "description": "SQLite storage backend for FrontMCP - local session, elicitation, and event persistence without Redis", | ||
@@ -47,7 +47,7 @@ "author": "AgentFront <info@agentfront.dev>", | ||
| "dependencies": { | ||
| "@frontmcp/utils": "1.0.4", | ||
| "@frontmcp/utils": "1.1.0-beta.1", | ||
| "better-sqlite3": "^12.6.2" | ||
| }, | ||
| "peerDependencies": { | ||
| "zod": "^4.0.0" | ||
| "@frontmcp/lazy-zod": "1.1.0-beta.1" | ||
| }, | ||
@@ -54,0 +54,0 @@ "devDependencies": { |
@@ -6,3 +6,3 @@ /** | ||
| */ | ||
| import { z } from 'zod'; | ||
| import { z } from '@frontmcp/lazy-zod'; | ||
| /** | ||
@@ -36,10 +36,10 @@ * SQLite storage configuration options. | ||
| */ | ||
| export declare const sqliteStorageOptionsSchema: z.ZodObject<{ | ||
| path: z.ZodString; | ||
| encryption: z.ZodOptional<z.ZodObject<{ | ||
| secret: z.ZodString; | ||
| }, z.core.$strip>>; | ||
| ttlCleanupIntervalMs: z.ZodDefault<z.ZodOptional<z.ZodNumber>>; | ||
| walMode: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>; | ||
| }, z.core.$strip>; | ||
| export declare const sqliteStorageOptionsSchema: import("@frontmcp/lazy-zod").ZodObject<{ | ||
| path: import("@frontmcp/lazy-zod").ZodString; | ||
| encryption: import("@frontmcp/lazy-zod").ZodOptional<import("@frontmcp/lazy-zod").ZodObject<{ | ||
| secret: import("@frontmcp/lazy-zod").ZodString; | ||
| }, import("zod/v4/core").$strip>>; | ||
| ttlCleanupIntervalMs: import("@frontmcp/lazy-zod").ZodDefault<import("@frontmcp/lazy-zod").ZodOptional<import("@frontmcp/lazy-zod").ZodNumber>>; | ||
| walMode: import("@frontmcp/lazy-zod").ZodDefault<import("@frontmcp/lazy-zod").ZodOptional<import("@frontmcp/lazy-zod").ZodBoolean>>; | ||
| }, import("zod/v4/core").$strip>; | ||
| /** | ||
@@ -46,0 +46,0 @@ * SQLite storage input type (before Zod defaults). |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"sqlite.options.d.ts","sourceRoot":"","sources":["../src/sqlite.options.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,wCAAwC;IACxC,IAAI,EAAE,MAAM,CAAC;IAEb;;;OAGG;IACH,UAAU,CAAC,EAAE;QACX,qEAAqE;QACrE,MAAM,EAAE,MAAM,CAAC;KAChB,CAAC;IAEF;;;OAGG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAE9B;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;GAEG;AACH,eAAO,MAAM,0BAA0B;;;;;;;iBASrC,CAAC;AAEH;;GAEG;AACH,MAAM,MAAM,yBAAyB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,0BAA0B,CAAC,CAAC;AAEnF;;GAEG;AACH,MAAM,MAAM,0BAA0B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,0BAA0B,CAAC,CAAC"} | ||
| {"version":3,"file":"sqlite.options.d.ts","sourceRoot":"","sources":["../src/sqlite.options.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,oBAAoB,CAAC;AAEvC;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,wCAAwC;IACxC,IAAI,EAAE,MAAM,CAAC;IAEb;;;OAGG;IACH,UAAU,CAAC,EAAE;QACX,qEAAqE;QACrE,MAAM,EAAE,MAAM,CAAC;KAChB,CAAC;IAEF;;;OAGG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAE9B;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;GAEG;AACH,eAAO,MAAM,0BAA0B;;;;;;;gCASrC,CAAC;AAEH;;GAEG;AACH,MAAM,MAAM,yBAAyB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,0BAA0B,CAAC,CAAC;AAEnF;;GAEG;AACH,MAAM,MAAM,0BAA0B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,0BAA0B,CAAC,CAAC"} |
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
104668
41.17%21
10.53%2202
45.25%1
Infinity%+ Added
+ Added
- Removed
Updated