diff --git a/core/store/db.ts b/core/store/db.ts index e4a8860..eae9fc1 100644 --- a/core/store/db.ts +++ b/core/store/db.ts @@ -6,7 +6,7 @@ // ever touches the state DB — never write paper markdown/PDF bytes here. import { existsSync, mkdirSync } from 'node:fs' -import { dirname } from 'node:path' +import { dirname, resolve } from 'node:path' import Database from 'better-sqlite3' @@ -15,10 +15,22 @@ import { runMigrations } from './migrate.js' export interface OpenDbOptions { /** Override the DB file path. Defaults to `data/app.db` under `cwd`. Pass `:memory:` for tests. */ path?: string + /** Override the native binding location. Auto-detected for Electron if omitted. */ + nativeBinding?: string } const DEFAULT_DB_PATH = 'data/app.db' +function resolveNativeBinding(): string | undefined { + if (process.versions.electron) { + const electronBinding = resolve(process.cwd(), 'resources/native/better_sqlite3.electron.node') + if (existsSync(electronBinding)) { + return electronBinding + } + } + return undefined +} + /** * Open (creating if needed) the Vellum state database and bring its schema * up to date. Safe to call repeatedly — schema application is idempotent via @@ -34,7 +46,8 @@ export function openDb(options: OpenDbOptions = {}): Database.Database { } } - const db = new Database(path) + const nativeBinding = options.nativeBinding ?? resolveNativeBinding() + const db = new Database(path, nativeBinding ? { nativeBinding } : undefined) db.pragma('journal_mode = WAL') db.pragma('foreign_keys = ON') diff --git a/electron.vite.config.ts b/electron.vite.config.ts index 4cb7db0..e90fce3 100644 --- a/electron.vite.config.ts +++ b/electron.vite.config.ts @@ -1,5 +1,5 @@ import { resolve } from 'node:path' -import { defineConfig } from 'electron-vite' +import { defineConfig, externalizeDepsPlugin } from 'electron-vite' import react from '@vitejs/plugin-react' // Single-package Electron layout: @@ -8,10 +8,18 @@ import react from '@vitejs/plugin-react' // core/ -> backend logic imported by the main process export default defineConfig({ main: { + plugins: [ + externalizeDepsPlugin({ + exclude: ['@agentclientprotocol/sdk', 'unpdf', 'pdfjs-dist'], + }), + ], build: { rollupOptions: { input: resolve('electron/main.ts'), - output: { entryFileNames: 'index.js' }, + output: { + entryFileNames: 'index.cjs', + format: 'cjs', + }, }, }, resolve: { @@ -19,6 +27,7 @@ export default defineConfig({ }, }, preload: { + plugins: [externalizeDepsPlugin()], build: { rollupOptions: { input: resolve('electron/preload.ts'), diff --git a/electron/main.ts b/electron/main.ts index 32681ec..edc1e7f 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -3,6 +3,7 @@ import type { IpcMainInvokeEvent } from 'electron' import { randomUUID } from 'node:crypto' import { fileURLToPath } from 'node:url' import { dirname, join } from 'node:path' +import { existsSync } from 'node:fs' import { readFile } from 'node:fs/promises' import type { Database } from 'better-sqlite3' @@ -35,13 +36,19 @@ const SLUG_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/ // // Everything below is the minimal boot skeleton. Feature work attaches here. +function resolvePreloadPath(): string { + const mjs = join(__dirname, '../preload/preload.mjs') + if (existsSync(mjs)) return mjs + return join(__dirname, '../preload/index.js') +} + function createWindow(): void { const win = new BrowserWindow({ width: 1440, height: 900, show: false, webPreferences: { - preload: join(__dirname, '../preload/index.js'), + preload: resolvePreloadPath(), contextIsolation: true, nodeIntegration: false, sandbox: false, @@ -102,7 +109,7 @@ ipcMain.handle('vellum:ingest', async (_event, input: unknown): Promise=22" + "node": "20.x || 22.x || 23.x || 24.x || 25.x || 26.x" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" } }, "node_modules/bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, "license": "MIT", "dependencies": { "buffer": "^5.5.0", @@ -3026,7 +3035,6 @@ "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, "funding": [ { "type": "github", @@ -3792,7 +3800,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "dev": true, "license": "MIT", "dependencies": { "mimic-response": "^3.1.0" @@ -3808,7 +3815,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -3827,6 +3833,15 @@ "node": ">=6" } }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/defaults": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", @@ -3919,7 +3934,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, "license": "Apache-2.0", "engines": { "node": ">=8" @@ -4394,7 +4408,6 @@ "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "dev": true, "license": "MIT", "dependencies": { "once": "^1.4.0" @@ -4567,6 +4580,15 @@ "@types/estree": "^1.0.0" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, "node_modules/expect-type": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", @@ -4640,6 +4662,12 @@ "pend": "~1.2.0" } }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, "node_modules/filelist": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", @@ -4731,9 +4759,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/fs-extra": { "version": "8.1.0", @@ -4891,6 +4917,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -5262,7 +5294,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, "funding": [ { "type": "github", @@ -5322,7 +5353,12 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "license": "ISC" }, "node_modules/ip-address": { @@ -5968,7 +6004,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -6098,6 +6133,12 @@ "node": ">=10" } }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -6124,6 +6165,12 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, "node_modules/negotiator": { "version": "0.6.4", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", @@ -6138,7 +6185,6 @@ "version": "3.94.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.94.0.tgz", "integrity": "sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g==", - "dev": true, "license": "MIT", "dependencies": { "semver": "^7.3.5" @@ -6151,7 +6197,6 @@ "version": "7.8.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -6160,15 +6205,6 @@ "node": ">=10" } }, - "node_modules/node-addon-api": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.9.0.tgz", - "integrity": "sha512-ekZMeaaIzSQTSpr7X2X3iJM7lTzgnx8ahAG9pJfT/7+14mlEM8ZYQ9cgCDvSSRbReFK0oHli3WrZdCiRsgAT9Q==", - "license": "MIT", - "engines": { - "node": "^18 || ^20 || >= 21" - } - }, "node_modules/node-api-version": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.2.1.tgz", @@ -6320,7 +6356,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -6805,6 +6840,35 @@ "dev": true, "license": "ISC" }, + "node_modules/playwright": { + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.63.0.tgz", + "integrity": "sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.63.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/playwright-core": { + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.63.0.tgz", + "integrity": "sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/plist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.1.tgz", @@ -6849,6 +6913,33 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/pretty-format": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", @@ -6922,7 +7013,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", - "dev": true, "license": "MIT", "dependencies": { "end-of-stream": "^1.1.0", @@ -6952,6 +7042,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -7014,7 +7119,6 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, "license": "MIT", "dependencies": { "inherits": "^2.0.3", @@ -7247,7 +7351,6 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, "funding": [ { "type": "github", @@ -7393,6 +7496,51 @@ "dev": true, "license": "ISC" }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", @@ -7569,7 +7717,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "~5.2.0" @@ -7646,6 +7793,15 @@ "node": ">=8" } }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/sumchecker": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", @@ -7698,13 +7854,29 @@ "node": ">=10" } }, + "node_modules/tar-fs": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz", + "integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-fs/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, "node_modules/tar-stream": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", @@ -8354,6 +8526,18 @@ "@esbuild/win32-x64": "0.28.1" } }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/type-fest": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", @@ -8491,7 +8675,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, "license": "MIT" }, "node_modules/verror": { @@ -8814,7 +8997,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, "license": "ISC" }, "node_modules/ws": { diff --git a/package.json b/package.json index 3efc212..844aac4 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "Vellum — local-first AI paper workspace that runs on your own Claude/Codex plan via ACP. No extra API key.", "author": "Kritarth Dandapat", "license": "MIT", - "main": "out/main/index.js", + "main": "out/main/index.cjs", "type": "module", "scripts": { "dev": "electron-vite dev", @@ -18,7 +18,7 @@ }, "dependencies": { "@agentclientprotocol/sdk": "1.2.1", - "better-sqlite3": "^13.0.1", + "better-sqlite3": "12.11.1", "pdfjs-dist": "^6.1.200", "unpdf": "^1.6.2" }, @@ -35,6 +35,7 @@ "electron-builder": "^25.0.0", "electron-vite": "^2.3.0", "jsdom": "^25.0.1", + "playwright": "^1.63.0", "react": "^18.3.0", "react-dom": "^18.3.0", "tsx": "^4.23.1", @@ -44,6 +45,7 @@ }, "//": "@agentclientprotocol/sdk is the current canonical ACP TS lib (verified on npm 2026-07-21). @zed-industries/agent-client-protocol is deprecated and renamed to this package. better-sqlite3 bumped 11->13: v11's prebuilt/native build doesn't compile against newer V8 (Node 26) — v13 fixes the V8 API compat, no behavior change for our usage.", "allowScripts": { + "better-sqlite3@12.11.1": true, "better-sqlite3@13.0.1": true, "electron@32.3.3": true, "esbuild@0.21.5": true, diff --git a/resources/native/better_sqlite3.electron.node b/resources/native/better_sqlite3.electron.node new file mode 100755 index 0000000..abefb82 Binary files /dev/null and b/resources/native/better_sqlite3.electron.node differ diff --git a/src/app/App.test.tsx b/src/app/App.test.tsx index f9c0ec6..a5b2833 100644 --- a/src/app/App.test.tsx +++ b/src/app/App.test.tsx @@ -29,6 +29,12 @@ beforeEach(() => { listPapers: vi.fn().mockResolvedValue([ { slug: 'arxiv-1706.03762', title: 'Attention Is All You Need', authors: ['Ashish Vaswani'], year: 2017, addedAt: '2026-01-01T00:00:00.000Z' }, ]), + ingest: vi.fn().mockResolvedValue({ + slug: 'arxiv-1706.03762', + title: 'Attention Is All You Need', + metadata: { title: 'Attention Is All You Need', authors: ['Ashish Vaswani'] }, + paths: { pdfPath: 'paper.pdf', mdPath: 'paper.md' }, + }), // Opening a paper mounts RightPanel's AskPanel ([P1-10]) bound to its // slug — this test only cares about tab/pane shell behavior, so a // never-resolving promise keeps AskPanel harmlessly in its loading @@ -93,4 +99,22 @@ describe('App shell', () => { expect(screen.getAllByRole('tab', { name: 'Attention Is All You Need' })).toHaveLength(1) expect(screen.queryByLabelText(/search papers by title/i)).not.toBeInTheDocument() }) + + it('clicking Create opens IngestModal, and ingesting a paper opens it into a new tab', async () => { + const user = userEvent.setup() + render() + + expect(screen.queryByRole('dialog')).not.toBeInTheDocument() + + await user.click(screen.getByRole('button', { name: 'Create' })) + expect(screen.getByRole('dialog')).toBeInTheDocument() + expect(screen.getByRole('heading', { name: /add paper/i })).toBeInTheDocument() + + const input = screen.getByPlaceholderText(/1706\.03762/) + await user.type(input, '1706.03762') + await user.click(screen.getByRole('button', { name: /add paper/i })) + + expect(screen.queryByRole('dialog')).not.toBeInTheDocument() + expect(await screen.findByRole('tab', { name: 'Attention Is All You Need' })).toBeInTheDocument() + }) }) diff --git a/src/app/App.tsx b/src/app/App.tsx index b05943b..b274c20 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from 'react' +import { IngestModal } from './IngestModal' import { Library } from './Library' import { Reader } from './Reader' import { ReaderToolbar } from './ReaderToolbar' @@ -12,7 +13,8 @@ import styles from './App.module.css' // Vellum shell — [P1-07] anara-style frame, [P1-08] wires in real data: // top: TabStrip (open papers as tabs — one per opened paper, keyed by slug) // left: Sidebar (Create / Home / Library / Search + folder tree); selecting -// 'Library' swaps the center pane to the Library grid +// 'Library' swaps the center pane to the Library grid; selecting 'Create' +// opens the IngestModal. // center: Library grid (click a card -> opens/focuses that paper's tab and // switches back to the Reader) or the [P1-09] Reader for the active tab // right: RightPanel (Ask | Notes | Details | Annotations) @@ -27,6 +29,7 @@ export function App(): JSX.Element { const [tabs, setTabs] = useState([]) const [activeTabId, setActiveTabId] = useState(null) const [view, setView] = useState<'reader' | 'library'>('reader') + const [isIngestOpen, setIsIngestOpen] = useState(false) useEffect(() => { window.vellum?.ping().then(setPong).catch(() => setPong('no-bridge')) @@ -44,7 +47,11 @@ export function App(): JSX.Element { } function handleNavChange(item: NavItem): void { - setView(item === 'Library' ? 'library' : 'reader') + if (item === 'Create') { + setIsIngestOpen(true) + } else { + setView(item === 'Library' ? 'library' : 'reader') + } } return ( @@ -64,6 +71,14 @@ export function App(): JSX.Element { + setIsIngestOpen(false)} + onSuccess={(paper) => { + setIsIngestOpen(false) + openPaper(paper) + }} + />