Просмотр исходного кода

core: migrate from custom JSON storage to standard Drizzle migrations to improve database reliability and performance

This replaces the previous manual JSON file system with standard Drizzle migrations, enabling:
- Proper database schema migrations with timestamp-based versioning
- Batched migration for faster migration of large datasets
- Better data integrity with proper table schemas instead of JSON blobs
- Easier database upgrades and rollback capabilities

Migration changes:
- Todo table now uses individual columns with composite PK instead of JSON blob
- Share table removes unused download share data
- Session diff table moved from database table to file storage
- All migrations now use proper Drizzle format with per-folder layout

Users will see a one-time migration on next run that migrates existing JSON data to the new SQLite database.
Dax Raad 2 месяцев назад
Родитель
Сommit
a48a5a3462

+ 0 - 6
.opencode/opencode.jsonc

@@ -9,12 +9,6 @@
       "options": {},
     },
   },
-  "mcp": {
-    "context7": {
-      "type": "remote",
-      "url": "https://mcp.context7.com/mcp",
-    },
-  },
   "tools": {
     "github-triage": false,
     "github-pr-search": false,

+ 38 - 139
bun.lock

@@ -115,7 +115,7 @@
         "@opencode-ai/console-resource": "workspace:*",
         "@planetscale/database": "1.19.0",
         "aws4fetch": "1.0.20",
-        "drizzle-orm": "0.41.0",
+        "drizzle-orm": "catalog:",
         "postgres": "3.4.7",
         "stripe": "18.0.0",
         "ulid": "catalog:",
@@ -127,7 +127,7 @@
         "@types/bun": "1.3.0",
         "@types/node": "catalog:",
         "@typescript/native-preview": "catalog:",
-        "drizzle-kit": "0.30.5",
+        "drizzle-kit": "catalog:",
         "mysql2": "3.14.4",
         "typescript": "catalog:",
       },
@@ -354,6 +354,7 @@
         "@types/yargs": "17.0.33",
         "@typescript/native-preview": "catalog:",
         "drizzle-kit": "1.0.0-beta.12-a5629fb",
+        "drizzle-orm": "1.0.0-beta.12-a5629fb",
         "typescript": "catalog:",
         "vscode-languageserver-types": "3.17.5",
         "why-is-node-running": "3.2.2",
@@ -524,6 +525,8 @@
     "ai": "5.0.119",
     "diff": "8.0.2",
     "dompurify": "3.3.1",
+    "drizzle-kit": "1.0.0-beta.12-a5629fb",
+    "drizzle-orm": "1.0.0-beta.12-a5629fb",
     "fuzzysort": "3.1.0",
     "hono": "4.10.7",
     "hono-openapi": "1.1.2",
@@ -849,7 +852,7 @@
 
     "@dot/log": ["@dot/[email protected]", "", { "dependencies": { "chalk": "^4.1.2", "loglevelnext": "^6.0.0", "p-defer": "^3.0.0" } }, "sha512-ECraEVJWv2f2mWK93lYiefUkphStVlKD6yKDzisuoEmxuLKrxO9iGetHK2DoEAkj7sxjE886n0OUVVCUx0YPNg=="],
 
-    "@drizzle-team/brocli": ["@drizzle-team/[email protected]0.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="],
+    "@drizzle-team/brocli": ["@drizzle-team/[email protected]1.0", "", {}, "sha512-hD3pekGiPg0WPCCGAZmusBBJsDqGUR66Y452YgQsZOnkdQ7ViEPKuyP4huUGEZQefp8g34RRodXYmJ2TbCH+tg=="],
 
     "@emnapi/core": ["@emnapi/[email protected]", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg=="],
 
@@ -861,10 +864,6 @@
 
     "@emotion/memoize": ["@emotion/[email protected]", "", {}, "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw=="],
 
-    "@esbuild-kit/core-utils": ["@esbuild-kit/[email protected]", "", { "dependencies": { "esbuild": "~0.18.20", "source-map-support": "^0.5.21" } }, "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ=="],
-
-    "@esbuild-kit/esm-loader": ["@esbuild-kit/[email protected]", "", { "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", "get-tsconfig": "^4.7.0" } }, "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA=="],
-
     "@esbuild/aix-ppc64": ["@esbuild/[email protected]", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q=="],
 
     "@esbuild/android-arm": ["@esbuild/[email protected]", "", { "os": "android", "cpu": "arm" }, "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ=="],
@@ -2353,9 +2352,9 @@
 
     "dotenv": ["[email protected]", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="],
 
-    "drizzle-kit": ["drizzle-kit@0.30.5", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.19.7", "esbuild-register": "^3.5.0", "gel": "^2.0.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-l6dMSE100u7sDaTbLczibrQZjA35jLsHNqIV+jmhNVO3O8jzM6kywMOmV9uOz9ZVSCMPQhAZEFjL/qDPVrqpUA=="],
+    "drizzle-kit": ["drizzle-kit@1.0.0-beta.12-a5629fb", "", { "dependencies": { "@drizzle-team/brocli": "^0.11.0", "@js-temporal/polyfill": "^0.5.1", "esbuild": "^0.25.10", "tsx": "^4.20.6" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-l+p4QOMvPGYBYEE9NBlU7diu+NSlxuOUwi0I7i01Uj1PpfU0NxhPzaks/9q1MDw4FAPP8vdD0dOhoqosKtRWWQ=="],
 
-    "drizzle-orm": ["drizzle-orm@0.41.0", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-7A4ZxhHk9gdlXmTdPj/lREtP+3u8KvZ4yEN6MYVxBzZGex5Wtdc+CWSbu7btgF6TB0N+MNPrvW7RKBbxJchs/Q=="],
+    "drizzle-orm": ["drizzle-orm@1.0.0-beta.12-a5629fb", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@effect/sql": "^0.48.5", "@effect/sql-pg": "^0.49.7", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@sqlitecloud/drivers": ">=1.0.653", "@tidbcloud/serverless": "*", "@tursodatabase/database": ">=0.2.1", "@tursodatabase/database-common": ">=0.2.1", "@tursodatabase/database-wasm": ">=0.2.1", "@types/better-sqlite3": "*", "@types/mssql": "^9.1.4", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=9.3.0", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "mssql": "^11.0.1", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@effect/sql", "@effect/sql-pg", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@sqlitecloud/drivers", "@tidbcloud/serverless", "@tursodatabase/database", "@tursodatabase/database-common", "@tursodatabase/database-wasm", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-wyOAgr9Cy9oEN6z5S0JGhfipLKbRRJtQKgbDO9SXGR9swMBbGNIlXkeMqPRrqYQ8k70mh+7ZJ/eVmJ2F7zR3Vg=="],
 
     "dset": ["[email protected]", "", {}, "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA=="],
 
@@ -2417,8 +2416,6 @@
 
     "esbuild-plugin-copy": ["[email protected]", "", { "dependencies": { "chalk": "^4.1.2", "chokidar": "^3.5.3", "fs-extra": "^10.0.1", "globby": "^11.0.3" }, "peerDependencies": { "esbuild": ">= 0.14.0" } }, "sha512-Bk66jpevTcV8KMFzZI1P7MZKZ+uDcrZm2G2egZ2jNIvVnivDpodZI+/KnpL3Jnap0PBdIHU7HwFGB8r+vV5CVw=="],
 
-    "esbuild-register": ["[email protected]", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="],
-
     "escalade": ["[email protected]", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
 
     "escape-html": ["[email protected]", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
@@ -4127,8 +4124,6 @@
 
     "@dot/log/chalk": ["[email protected]", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
 
-    "@esbuild-kit/core-utils/esbuild": ["[email protected]", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="],
-
     "@expressive-code/plugin-shiki/shiki": ["[email protected]", "", { "dependencies": { "@shikijs/core": "3.15.0", "@shikijs/engine-javascript": "3.15.0", "@shikijs/engine-oniguruma": "3.15.0", "@shikijs/langs": "3.15.0", "@shikijs/themes": "3.15.0", "@shikijs/types": "3.15.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-kLdkY6iV3dYbtPwS9KXU7mjfmDm25f5m0IPNFnaXO7TBPcvbUOY72PYXSuSqDzwp+vlH/d7MXpHlKO/x+QoLXw=="],
 
     "@gitlab/gitlab-ai-provider/openai": ["[email protected]", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-fZ1uBqjFUjXzbGc35fFtYKEOxd20kd9fDpFeqWtsOZWiubY8CZ1NAlXHW3iathaFvqmNtCWMIsosCuyeI7Joxg=="],
@@ -4381,9 +4376,11 @@
 
     "cross-spawn/which": ["[email protected]", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
 
+    "db0/drizzle-orm": ["[email protected]", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-7A4ZxhHk9gdlXmTdPj/lREtP+3u8KvZ4yEN6MYVxBzZGex5Wtdc+CWSbu7btgF6TB0N+MNPrvW7RKBbxJchs/Q=="],
+
     "dot-prop/type-fest": ["[email protected]", "", {}, "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g=="],
 
-    "drizzle-kit/esbuild": ["esbuild@0.19.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.19.12", "@esbuild/android-arm": "0.19.12", "@esbuild/android-arm64": "0.19.12", "@esbuild/android-x64": "0.19.12", "@esbuild/darwin-arm64": "0.19.12", "@esbuild/darwin-x64": "0.19.12", "@esbuild/freebsd-arm64": "0.19.12", "@esbuild/freebsd-x64": "0.19.12", "@esbuild/linux-arm": "0.19.12", "@esbuild/linux-arm64": "0.19.12", "@esbuild/linux-ia32": "0.19.12", "@esbuild/linux-loong64": "0.19.12", "@esbuild/linux-mips64el": "0.19.12", "@esbuild/linux-ppc64": "0.19.12", "@esbuild/linux-riscv64": "0.19.12", "@esbuild/linux-s390x": "0.19.12", "@esbuild/linux-x64": "0.19.12", "@esbuild/netbsd-x64": "0.19.12", "@esbuild/openbsd-x64": "0.19.12", "@esbuild/sunos-x64": "0.19.12", "@esbuild/win32-arm64": "0.19.12", "@esbuild/win32-ia32": "0.19.12", "@esbuild/win32-x64": "0.19.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg=="],
+    "drizzle-kit/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
 
     "editorconfig/commander": ["[email protected]", "", {}, "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug=="],
 
@@ -4469,10 +4466,6 @@
 
     "opencode/@ai-sdk/openai-compatible": ["@ai-sdk/[email protected]", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-thubwhRtv9uicAxSWwNpinM7hiL/0CkhL/ymPaHuKvI494J7HIzn8KQZQ2ymRz284WTIZnI7VMyyejxW4RMM6w=="],
 
-    "opencode/drizzle-kit": ["[email protected]", "", { "dependencies": { "@drizzle-team/brocli": "^0.11.0", "@js-temporal/polyfill": "^0.5.1", "esbuild": "^0.25.10", "tsx": "^4.20.6" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-l+p4QOMvPGYBYEE9NBlU7diu+NSlxuOUwi0I7i01Uj1PpfU0NxhPzaks/9q1MDw4FAPP8vdD0dOhoqosKtRWWQ=="],
-
-    "opencode/drizzle-orm": ["[email protected]", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@effect/sql": "^0.48.5", "@effect/sql-pg": "^0.49.7", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@sqlitecloud/drivers": ">=1.0.653", "@tidbcloud/serverless": "*", "@tursodatabase/database": ">=0.2.1", "@tursodatabase/database-common": ">=0.2.1", "@tursodatabase/database-wasm": ">=0.2.1", "@types/better-sqlite3": "*", "@types/mssql": "^9.1.4", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=9.3.0", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "mssql": "^11.0.1", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@effect/sql", "@effect/sql-pg", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@sqlitecloud/drivers", "@tidbcloud/serverless", "@tursodatabase/database", "@tursodatabase/database-common", "@tursodatabase/database-wasm", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-wyOAgr9Cy9oEN6z5S0JGhfipLKbRRJtQKgbDO9SXGR9swMBbGNIlXkeMqPRrqYQ8k70mh+7ZJ/eVmJ2F7zR3Vg=="],
-
     "opencontrol/@modelcontextprotocol/sdk": ["@modelcontextprotocol/[email protected]", "", { "dependencies": { "content-type": "^1.0.5", "cors": "^2.8.5", "eventsource": "^3.0.2", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^4.1.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-oxzMzYCkZHMntzuyerehK3fV6A2Kwh5BD6CGEJSVDU2QNEhfLOptf2X7esQgaHZXHZY0oHmMsOtIDLP71UJXgA=="],
 
     "opencontrol/@tsconfig/bun": ["@tsconfig/[email protected]", "", {}, "sha512-udGrGJBNQdXGVulehc1aWT73wkR9wdaGBtB6yL70RJsqwW/yJhIg6ZbRlPOfIUiFNrnBuYLBi9CSmMKfDC7dvA=="],
@@ -4645,50 +4638,6 @@
 
     "@babel/helper-compilation-targets/lru-cache/yallist": ["[email protected]", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
 
-    "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/[email protected]", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="],
-
-    "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/[email protected]", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="],
-
-    "@esbuild-kit/core-utils/esbuild/@esbuild/android-x64": ["@esbuild/[email protected]", "", { "os": "android", "cpu": "x64" }, "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg=="],
-
-    "@esbuild-kit/core-utils/esbuild/@esbuild/darwin-arm64": ["@esbuild/[email protected]", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA=="],
-
-    "@esbuild-kit/core-utils/esbuild/@esbuild/darwin-x64": ["@esbuild/[email protected]", "", { "os": "darwin", "cpu": "x64" }, "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ=="],
-
-    "@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-arm64": ["@esbuild/[email protected]", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw=="],
-
-    "@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-x64": ["@esbuild/[email protected]", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ=="],
-
-    "@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "arm" }, "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg=="],
-
-    "@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm64": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA=="],
-
-    "@esbuild-kit/core-utils/esbuild/@esbuild/linux-ia32": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "ia32" }, "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA=="],
-
-    "@esbuild-kit/core-utils/esbuild/@esbuild/linux-loong64": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg=="],
-
-    "@esbuild-kit/core-utils/esbuild/@esbuild/linux-mips64el": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ=="],
-
-    "@esbuild-kit/core-utils/esbuild/@esbuild/linux-ppc64": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA=="],
-
-    "@esbuild-kit/core-utils/esbuild/@esbuild/linux-riscv64": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A=="],
-
-    "@esbuild-kit/core-utils/esbuild/@esbuild/linux-s390x": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "s390x" }, "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ=="],
-
-    "@esbuild-kit/core-utils/esbuild/@esbuild/linux-x64": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w=="],
-
-    "@esbuild-kit/core-utils/esbuild/@esbuild/netbsd-x64": ["@esbuild/[email protected]", "", { "os": "none", "cpu": "x64" }, "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A=="],
-
-    "@esbuild-kit/core-utils/esbuild/@esbuild/openbsd-x64": ["@esbuild/[email protected]", "", { "os": "openbsd", "cpu": "x64" }, "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg=="],
-
-    "@esbuild-kit/core-utils/esbuild/@esbuild/sunos-x64": ["@esbuild/[email protected]", "", { "os": "sunos", "cpu": "x64" }, "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ=="],
-
-    "@esbuild-kit/core-utils/esbuild/@esbuild/win32-arm64": ["@esbuild/[email protected]", "", { "os": "win32", "cpu": "arm64" }, "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg=="],
-
-    "@esbuild-kit/core-utils/esbuild/@esbuild/win32-ia32": ["@esbuild/[email protected]", "", { "os": "win32", "cpu": "ia32" }, "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g=="],
-
-    "@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="],
-
     "@expressive-code/plugin-shiki/shiki/@shikijs/core": ["@shikijs/[email protected]", "", { "dependencies": { "@shikijs/types": "3.15.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-8TOG6yG557q+fMsSVa8nkEDOZNTSxjbbR8l6lF2gyr6Np+jrPlslqDxQkN6rMXCECQ3isNPZAGszAfYoJOPGlg=="],
 
     "@expressive-code/plugin-shiki/shiki/@shikijs/engine-javascript": ["@shikijs/[email protected]", "", { "dependencies": { "@shikijs/types": "3.15.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.3" } }, "sha512-ZedbOFpopibdLmvTz2sJPJgns8Xvyabe2QbmqMTz07kt1pTzfEvKZc5IqPVO/XFiEbbNyaOpjPBkkr1vlwS+qg=="],
@@ -5017,51 +4966,55 @@
 
     "cross-spawn/which/isexe": ["[email protected]", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
 
-    "drizzle-kit/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.19.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA=="],
+    "drizzle-kit/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="],
 
-    "drizzle-kit/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.19.12", "", { "os": "android", "cpu": "arm" }, "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w=="],
+    "drizzle-kit/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="],
 
-    "drizzle-kit/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.19.12", "", { "os": "android", "cpu": "arm64" }, "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA=="],
+    "drizzle-kit/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="],
 
-    "drizzle-kit/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.19.12", "", { "os": "android", "cpu": "x64" }, "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew=="],
+    "drizzle-kit/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="],
 
-    "drizzle-kit/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.19.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g=="],
+    "drizzle-kit/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="],
 
-    "drizzle-kit/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.19.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A=="],
+    "drizzle-kit/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="],
 
-    "drizzle-kit/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.19.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA=="],
+    "drizzle-kit/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="],
 
-    "drizzle-kit/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.19.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg=="],
+    "drizzle-kit/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="],
 
-    "drizzle-kit/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.19.12", "", { "os": "linux", "cpu": "arm" }, "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w=="],
+    "drizzle-kit/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="],
 
-    "drizzle-kit/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.19.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA=="],
+    "drizzle-kit/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="],
 
-    "drizzle-kit/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.19.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA=="],
+    "drizzle-kit/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="],
 
-    "drizzle-kit/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.19.12", "", { "os": "linux", "cpu": "none" }, "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA=="],
+    "drizzle-kit/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="],
 
-    "drizzle-kit/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.19.12", "", { "os": "linux", "cpu": "none" }, "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w=="],
+    "drizzle-kit/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="],
 
-    "drizzle-kit/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.19.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg=="],
+    "drizzle-kit/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="],
 
-    "drizzle-kit/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.19.12", "", { "os": "linux", "cpu": "none" }, "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg=="],
+    "drizzle-kit/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="],
 
-    "drizzle-kit/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.19.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg=="],
+    "drizzle-kit/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="],
 
-    "drizzle-kit/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.19.12", "", { "os": "linux", "cpu": "x64" }, "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg=="],
+    "drizzle-kit/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="],
 
-    "drizzle-kit/esbuild/@esbuild/netbsd-x64": ["@esbuild/[email protected]", "", { "os": "none", "cpu": "x64" }, "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA=="],
+    "drizzle-kit/esbuild/@esbuild/netbsd-arm64": ["@esbuild/[email protected]", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="],
 
-    "drizzle-kit/esbuild/@esbuild/openbsd-x64": ["@esbuild/[email protected]", "", { "os": "openbsd", "cpu": "x64" }, "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw=="],
+    "drizzle-kit/esbuild/@esbuild/netbsd-x64": ["@esbuild/[email protected]", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="],
 
-    "drizzle-kit/esbuild/@esbuild/sunos-x64": ["@esbuild/[email protected]", "", { "os": "sunos", "cpu": "x64" }, "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA=="],
+    "drizzle-kit/esbuild/@esbuild/openbsd-arm64": ["@esbuild/[email protected]", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="],
 
-    "drizzle-kit/esbuild/@esbuild/win32-arm64": ["@esbuild/[email protected]", "", { "os": "win32", "cpu": "arm64" }, "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A=="],
+    "drizzle-kit/esbuild/@esbuild/openbsd-x64": ["@esbuild/[email protected]", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="],
 
-    "drizzle-kit/esbuild/@esbuild/win32-ia32": ["@esbuild/[email protected]", "", { "os": "win32", "cpu": "ia32" }, "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ=="],
+    "drizzle-kit/esbuild/@esbuild/sunos-x64": ["@esbuild/[email protected]", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="],
 
-    "drizzle-kit/esbuild/@esbuild/win32-x64": ["@esbuild/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA=="],
+    "drizzle-kit/esbuild/@esbuild/win32-arm64": ["@esbuild/[email protected]", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="],
+
+    "drizzle-kit/esbuild/@esbuild/win32-ia32": ["@esbuild/[email protected]", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="],
+
+    "drizzle-kit/esbuild/@esbuild/win32-x64": ["@esbuild/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
 
     "esbuild-plugin-copy/chokidar/readdirp": ["[email protected]", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
 
@@ -5085,10 +5038,6 @@
 
     "lazystream/readable-stream/string_decoder": ["[email protected]", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
 
-    "opencode/drizzle-kit/@drizzle-team/brocli": ["@drizzle-team/[email protected]", "", {}, "sha512-hD3pekGiPg0WPCCGAZmusBBJsDqGUR66Y452YgQsZOnkdQ7ViEPKuyP4huUGEZQefp8g34RRodXYmJ2TbCH+tg=="],
-
-    "opencode/drizzle-kit/esbuild": ["[email protected]", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
-
     "opencontrol/@modelcontextprotocol/sdk/express": ["[email protected]", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="],
 
     "opencontrol/@modelcontextprotocol/sdk/pkce-challenge": ["[email protected]", "", {}, "sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ=="],
@@ -5315,56 +5264,6 @@
 
     "js-beautify/glob/path-scurry/lru-cache": ["[email protected]", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
 
-    "opencode/drizzle-kit/esbuild/@esbuild/aix-ppc64": ["@esbuild/[email protected]", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="],
-
-    "opencode/drizzle-kit/esbuild/@esbuild/android-arm": ["@esbuild/[email protected]", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="],
-
-    "opencode/drizzle-kit/esbuild/@esbuild/android-arm64": ["@esbuild/[email protected]", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="],
-
-    "opencode/drizzle-kit/esbuild/@esbuild/android-x64": ["@esbuild/[email protected]", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="],
-
-    "opencode/drizzle-kit/esbuild/@esbuild/darwin-arm64": ["@esbuild/[email protected]", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="],
-
-    "opencode/drizzle-kit/esbuild/@esbuild/darwin-x64": ["@esbuild/[email protected]", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="],
-
-    "opencode/drizzle-kit/esbuild/@esbuild/freebsd-arm64": ["@esbuild/[email protected]", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="],
-
-    "opencode/drizzle-kit/esbuild/@esbuild/freebsd-x64": ["@esbuild/[email protected]", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="],
-
-    "opencode/drizzle-kit/esbuild/@esbuild/linux-arm": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="],
-
-    "opencode/drizzle-kit/esbuild/@esbuild/linux-arm64": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="],
-
-    "opencode/drizzle-kit/esbuild/@esbuild/linux-ia32": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="],
-
-    "opencode/drizzle-kit/esbuild/@esbuild/linux-loong64": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="],
-
-    "opencode/drizzle-kit/esbuild/@esbuild/linux-mips64el": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="],
-
-    "opencode/drizzle-kit/esbuild/@esbuild/linux-ppc64": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="],
-
-    "opencode/drizzle-kit/esbuild/@esbuild/linux-riscv64": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="],
-
-    "opencode/drizzle-kit/esbuild/@esbuild/linux-s390x": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="],
-
-    "opencode/drizzle-kit/esbuild/@esbuild/linux-x64": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="],
-
-    "opencode/drizzle-kit/esbuild/@esbuild/netbsd-arm64": ["@esbuild/[email protected]", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="],
-
-    "opencode/drizzle-kit/esbuild/@esbuild/netbsd-x64": ["@esbuild/[email protected]", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="],
-
-    "opencode/drizzle-kit/esbuild/@esbuild/openbsd-arm64": ["@esbuild/[email protected]", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="],
-
-    "opencode/drizzle-kit/esbuild/@esbuild/openbsd-x64": ["@esbuild/[email protected]", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="],
-
-    "opencode/drizzle-kit/esbuild/@esbuild/sunos-x64": ["@esbuild/[email protected]", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="],
-
-    "opencode/drizzle-kit/esbuild/@esbuild/win32-arm64": ["@esbuild/[email protected]", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="],
-
-    "opencode/drizzle-kit/esbuild/@esbuild/win32-ia32": ["@esbuild/[email protected]", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="],
-
-    "opencode/drizzle-kit/esbuild/@esbuild/win32-x64": ["@esbuild/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
-
     "opencontrol/@modelcontextprotocol/sdk/express/accepts": ["[email protected]", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
 
     "opencontrol/@modelcontextprotocol/sdk/express/body-parser": ["[email protected]", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg=="],

+ 2 - 0
package.json

@@ -38,6 +38,8 @@
       "@tailwindcss/vite": "4.1.11",
       "diff": "8.0.2",
       "dompurify": "3.3.1",
+      "drizzle-kit": "1.0.0-beta.12-a5629fb",
+      "drizzle-orm": "1.0.0-beta.12-a5629fb",
       "ai": "5.0.119",
       "hono": "4.10.7",
       "hono-openapi": "1.1.2",

+ 2 - 2
packages/console/core/package.json

@@ -12,7 +12,7 @@
     "@opencode-ai/console-resource": "workspace:*",
     "@planetscale/database": "1.19.0",
     "aws4fetch": "1.0.20",
-    "drizzle-orm": "0.41.0",
+    "drizzle-orm": "catalog:",
     "postgres": "3.4.7",
     "stripe": "18.0.0",
     "ulid": "catalog:",
@@ -43,7 +43,7 @@
     "@tsconfig/node22": "22.0.2",
     "@types/bun": "1.3.0",
     "@types/node": "catalog:",
-    "drizzle-kit": "0.30.5",
+    "drizzle-kit": "catalog:",
     "mysql2": "3.14.4",
     "typescript": "catalog:",
     "@typescript/native-preview": "catalog:"

+ 8 - 25
packages/opencode/AGENTS.md

@@ -1,27 +1,10 @@
-# opencode agent guidelines
+# opencode database guide
 
-## Build/Test Commands
+## Database
 
-- **Install**: `bun install`
-- **Run**: `bun run --conditions=browser ./src/index.ts`
-- **Typecheck**: `bun run typecheck` (npm run typecheck)
-- **Test**: `bun test` (runs all tests)
-- **Single test**: `bun test test/tool/tool.test.ts` (specific test file)
-
-## Code Style
-
-- **Runtime**: Bun with TypeScript ESM modules
-- **Imports**: Use relative imports for local modules, named imports preferred
-- **Types**: Zod schemas for validation, TypeScript interfaces for structure
-- **Naming**: camelCase for variables/functions, PascalCase for classes/namespaces
-- **Error handling**: Use Result patterns, avoid throwing exceptions in tools
-- **File structure**: Namespace-based organization (e.g., `Tool.define()`, `Session.create()`)
-
-## Architecture
-
-- **Tools**: Implement `Tool.Info` interface with `execute()` method
-- **Context**: Pass `sessionID` in tool context, use `App.provide()` for DI
-- **Validation**: All inputs validated with Zod schemas
-- **Logging**: Use `Log.create({ service: "name" })` pattern
-- **Storage**: Use `Storage` namespace for persistence
-- **API Client**: The TypeScript TUI (built with SolidJS + OpenTUI) communicates with the OpenCode server using `@opencode-ai/sdk`. When adding/modifying server endpoints in `packages/opencode/src/server/server.ts`, run `./script/generate.ts` to regenerate the SDK and related files.
+- **Schema**: Drizzle schema lives in `src/**/*.sql.ts`.
+- **Naming**: tables and columns use snake*case; join columns are `<entity>_id`; indexes are `<table>*<column>\_idx`.
+- **Migrations**: generated by Drizzle Kit using `drizzle.config.ts` (schema: `./src/**/*.sql.ts`, output: `./migration`).
+- **Command**: `bun run db generate --name <slug>`.
+- **Output**: creates `migration/<timestamp>_<slug>/migration.sql` and `snapshot.json`.
+- **Tests**: migration tests should read the per-folder layout (no `_journal.json`).

+ 3 - 0
packages/opencode/drizzle.config.ts

@@ -4,4 +4,7 @@ export default defineConfig({
   dialect: "sqlite",
   schema: "./src/**/*.sql.ts",
   out: "./migration",
+  dbCredentials: {
+    url: "/home/thdxr/.local/share/opencode/opencode.db",
+  },
 })

+ 28 - 30
packages/opencode/migration/0000_magical_strong_guy.sql → packages/opencode/migration/20260127173238_melted_union_jack/migration.sql

@@ -1,5 +1,5 @@
 CREATE TABLE `project` (
-	`id` text PRIMARY KEY NOT NULL,
+	`id` text PRIMARY KEY,
 	`worktree` text NOT NULL,
 	`vcs` text,
 	`name` text,
@@ -12,38 +12,29 @@ CREATE TABLE `project` (
 );
 --> statement-breakpoint
 CREATE TABLE `message` (
-	`id` text PRIMARY KEY NOT NULL,
+	`id` text PRIMARY KEY,
 	`session_id` text NOT NULL,
 	`created_at` integer NOT NULL,
 	`data` text NOT NULL,
-	FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON UPDATE no action ON DELETE cascade
+	CONSTRAINT `fk_message_session_id_session_id_fk` FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON DELETE CASCADE
 );
 --> statement-breakpoint
-CREATE INDEX `message_session_idx` ON `message` (`session_id`);--> statement-breakpoint
 CREATE TABLE `part` (
-	`id` text PRIMARY KEY NOT NULL,
+	`id` text PRIMARY KEY,
 	`message_id` text NOT NULL,
 	`session_id` text NOT NULL,
 	`data` text NOT NULL,
-	FOREIGN KEY (`message_id`) REFERENCES `message`(`id`) ON UPDATE no action ON DELETE cascade
+	CONSTRAINT `fk_part_message_id_message_id_fk` FOREIGN KEY (`message_id`) REFERENCES `message`(`id`) ON DELETE CASCADE
 );
 --> statement-breakpoint
-CREATE INDEX `part_message_idx` ON `part` (`message_id`);--> statement-breakpoint
-CREATE INDEX `part_session_idx` ON `part` (`session_id`);--> statement-breakpoint
 CREATE TABLE `permission` (
-	`project_id` text PRIMARY KEY NOT NULL,
+	`project_id` text PRIMARY KEY,
 	`data` text NOT NULL,
-	FOREIGN KEY (`project_id`) REFERENCES `project`(`id`) ON UPDATE no action ON DELETE cascade
-);
---> statement-breakpoint
-CREATE TABLE `session_diff` (
-	`session_id` text PRIMARY KEY NOT NULL,
-	`data` text NOT NULL,
-	FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON UPDATE no action ON DELETE cascade
+	CONSTRAINT `fk_permission_project_id_project_id_fk` FOREIGN KEY (`project_id`) REFERENCES `project`(`id`) ON DELETE CASCADE
 );
 --> statement-breakpoint
 CREATE TABLE `session` (
-	`id` text PRIMARY KEY NOT NULL,
+	`id` text PRIMARY KEY,
 	`project_id` text NOT NULL,
 	`parent_id` text,
 	`slug` text NOT NULL,
@@ -64,24 +55,31 @@ CREATE TABLE `session` (
 	`time_updated` integer NOT NULL,
 	`time_compacting` integer,
 	`time_archived` integer,
-	FOREIGN KEY (`project_id`) REFERENCES `project`(`id`) ON UPDATE no action ON DELETE cascade
+	CONSTRAINT `fk_session_project_id_project_id_fk` FOREIGN KEY (`project_id`) REFERENCES `project`(`id`) ON DELETE CASCADE
 );
 --> statement-breakpoint
-CREATE INDEX `session_project_idx` ON `session` (`project_id`);--> statement-breakpoint
-CREATE INDEX `session_parent_idx` ON `session` (`parent_id`);--> statement-breakpoint
 CREATE TABLE `todo` (
-	`session_id` text PRIMARY KEY NOT NULL,
-	`data` text NOT NULL,
-	FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON UPDATE no action ON DELETE cascade
+	`session_id` text NOT NULL,
+	`id` text NOT NULL,
+	`content` text NOT NULL,
+	`status` text NOT NULL,
+	`priority` text NOT NULL,
+	`position` integer NOT NULL,
+	CONSTRAINT `todo_pk` PRIMARY KEY(`session_id`, `id`),
+	CONSTRAINT `fk_todo_session_id_session_id_fk` FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON DELETE CASCADE
 );
 --> statement-breakpoint
 CREATE TABLE `session_share` (
-	`session_id` text PRIMARY KEY NOT NULL,
-	`data` text NOT NULL,
-	FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON UPDATE no action ON DELETE cascade
+	`session_id` text PRIMARY KEY,
+	`id` text NOT NULL,
+	`secret` text NOT NULL,
+	`url` text NOT NULL,
+	CONSTRAINT `fk_session_share_session_id_session_id_fk` FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON DELETE CASCADE
 );
 --> statement-breakpoint
-CREATE TABLE `share` (
-	`session_id` text PRIMARY KEY NOT NULL,
-	`data` text NOT NULL
-);
+CREATE INDEX `message_session_idx` ON `message` (`session_id`);--> statement-breakpoint
+CREATE INDEX `part_message_idx` ON `part` (`message_id`);--> statement-breakpoint
+CREATE INDEX `part_session_idx` ON `part` (`session_id`);--> statement-breakpoint
+CREATE INDEX `session_project_idx` ON `session` (`project_id`);--> statement-breakpoint
+CREATE INDEX `session_parent_idx` ON `session` (`parent_id`);--> statement-breakpoint
+CREATE INDEX `todo_session_idx` ON `todo` (`session_id`);

+ 787 - 0
packages/opencode/migration/20260127173238_melted_union_jack/snapshot.json

@@ -0,0 +1,787 @@
+{
+  "version": "7",
+  "dialect": "sqlite",
+  "id": "0e365b40-39c4-447f-9729-9714d865d8ff",
+  "prevIds": [
+    "00000000-0000-0000-0000-000000000000"
+  ],
+  "ddl": [
+    {
+      "name": "project",
+      "entityType": "tables"
+    },
+    {
+      "name": "message",
+      "entityType": "tables"
+    },
+    {
+      "name": "part",
+      "entityType": "tables"
+    },
+    {
+      "name": "permission",
+      "entityType": "tables"
+    },
+    {
+      "name": "session",
+      "entityType": "tables"
+    },
+    {
+      "name": "todo",
+      "entityType": "tables"
+    },
+    {
+      "name": "session_share",
+      "entityType": "tables"
+    },
+    {
+      "type": "text",
+      "notNull": false,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "id",
+      "entityType": "columns",
+      "table": "project"
+    },
+    {
+      "type": "text",
+      "notNull": true,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "worktree",
+      "entityType": "columns",
+      "table": "project"
+    },
+    {
+      "type": "text",
+      "notNull": false,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "vcs",
+      "entityType": "columns",
+      "table": "project"
+    },
+    {
+      "type": "text",
+      "notNull": false,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "name",
+      "entityType": "columns",
+      "table": "project"
+    },
+    {
+      "type": "text",
+      "notNull": false,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "icon_url",
+      "entityType": "columns",
+      "table": "project"
+    },
+    {
+      "type": "text",
+      "notNull": false,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "icon_color",
+      "entityType": "columns",
+      "table": "project"
+    },
+    {
+      "type": "integer",
+      "notNull": true,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "time_created",
+      "entityType": "columns",
+      "table": "project"
+    },
+    {
+      "type": "integer",
+      "notNull": true,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "time_updated",
+      "entityType": "columns",
+      "table": "project"
+    },
+    {
+      "type": "integer",
+      "notNull": false,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "time_initialized",
+      "entityType": "columns",
+      "table": "project"
+    },
+    {
+      "type": "text",
+      "notNull": true,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "sandboxes",
+      "entityType": "columns",
+      "table": "project"
+    },
+    {
+      "type": "text",
+      "notNull": false,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "id",
+      "entityType": "columns",
+      "table": "message"
+    },
+    {
+      "type": "text",
+      "notNull": true,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "session_id",
+      "entityType": "columns",
+      "table": "message"
+    },
+    {
+      "type": "integer",
+      "notNull": true,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "created_at",
+      "entityType": "columns",
+      "table": "message"
+    },
+    {
+      "type": "text",
+      "notNull": true,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "data",
+      "entityType": "columns",
+      "table": "message"
+    },
+    {
+      "type": "text",
+      "notNull": false,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "id",
+      "entityType": "columns",
+      "table": "part"
+    },
+    {
+      "type": "text",
+      "notNull": true,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "message_id",
+      "entityType": "columns",
+      "table": "part"
+    },
+    {
+      "type": "text",
+      "notNull": true,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "session_id",
+      "entityType": "columns",
+      "table": "part"
+    },
+    {
+      "type": "text",
+      "notNull": true,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "data",
+      "entityType": "columns",
+      "table": "part"
+    },
+    {
+      "type": "text",
+      "notNull": false,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "project_id",
+      "entityType": "columns",
+      "table": "permission"
+    },
+    {
+      "type": "text",
+      "notNull": true,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "data",
+      "entityType": "columns",
+      "table": "permission"
+    },
+    {
+      "type": "text",
+      "notNull": false,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "id",
+      "entityType": "columns",
+      "table": "session"
+    },
+    {
+      "type": "text",
+      "notNull": true,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "project_id",
+      "entityType": "columns",
+      "table": "session"
+    },
+    {
+      "type": "text",
+      "notNull": false,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "parent_id",
+      "entityType": "columns",
+      "table": "session"
+    },
+    {
+      "type": "text",
+      "notNull": true,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "slug",
+      "entityType": "columns",
+      "table": "session"
+    },
+    {
+      "type": "text",
+      "notNull": true,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "directory",
+      "entityType": "columns",
+      "table": "session"
+    },
+    {
+      "type": "text",
+      "notNull": true,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "title",
+      "entityType": "columns",
+      "table": "session"
+    },
+    {
+      "type": "text",
+      "notNull": true,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "version",
+      "entityType": "columns",
+      "table": "session"
+    },
+    {
+      "type": "text",
+      "notNull": false,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "share_url",
+      "entityType": "columns",
+      "table": "session"
+    },
+    {
+      "type": "integer",
+      "notNull": false,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "summary_additions",
+      "entityType": "columns",
+      "table": "session"
+    },
+    {
+      "type": "integer",
+      "notNull": false,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "summary_deletions",
+      "entityType": "columns",
+      "table": "session"
+    },
+    {
+      "type": "integer",
+      "notNull": false,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "summary_files",
+      "entityType": "columns",
+      "table": "session"
+    },
+    {
+      "type": "text",
+      "notNull": false,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "summary_diffs",
+      "entityType": "columns",
+      "table": "session"
+    },
+    {
+      "type": "text",
+      "notNull": false,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "revert_message_id",
+      "entityType": "columns",
+      "table": "session"
+    },
+    {
+      "type": "text",
+      "notNull": false,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "revert_part_id",
+      "entityType": "columns",
+      "table": "session"
+    },
+    {
+      "type": "text",
+      "notNull": false,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "revert_snapshot",
+      "entityType": "columns",
+      "table": "session"
+    },
+    {
+      "type": "text",
+      "notNull": false,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "revert_diff",
+      "entityType": "columns",
+      "table": "session"
+    },
+    {
+      "type": "text",
+      "notNull": false,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "permission",
+      "entityType": "columns",
+      "table": "session"
+    },
+    {
+      "type": "integer",
+      "notNull": true,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "time_created",
+      "entityType": "columns",
+      "table": "session"
+    },
+    {
+      "type": "integer",
+      "notNull": true,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "time_updated",
+      "entityType": "columns",
+      "table": "session"
+    },
+    {
+      "type": "integer",
+      "notNull": false,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "time_compacting",
+      "entityType": "columns",
+      "table": "session"
+    },
+    {
+      "type": "integer",
+      "notNull": false,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "time_archived",
+      "entityType": "columns",
+      "table": "session"
+    },
+    {
+      "type": "text",
+      "notNull": true,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "session_id",
+      "entityType": "columns",
+      "table": "todo"
+    },
+    {
+      "type": "text",
+      "notNull": true,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "id",
+      "entityType": "columns",
+      "table": "todo"
+    },
+    {
+      "type": "text",
+      "notNull": true,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "content",
+      "entityType": "columns",
+      "table": "todo"
+    },
+    {
+      "type": "text",
+      "notNull": true,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "status",
+      "entityType": "columns",
+      "table": "todo"
+    },
+    {
+      "type": "text",
+      "notNull": true,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "priority",
+      "entityType": "columns",
+      "table": "todo"
+    },
+    {
+      "type": "integer",
+      "notNull": true,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "position",
+      "entityType": "columns",
+      "table": "todo"
+    },
+    {
+      "type": "text",
+      "notNull": false,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "session_id",
+      "entityType": "columns",
+      "table": "session_share"
+    },
+    {
+      "type": "text",
+      "notNull": true,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "id",
+      "entityType": "columns",
+      "table": "session_share"
+    },
+    {
+      "type": "text",
+      "notNull": true,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "secret",
+      "entityType": "columns",
+      "table": "session_share"
+    },
+    {
+      "type": "text",
+      "notNull": true,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "url",
+      "entityType": "columns",
+      "table": "session_share"
+    },
+    {
+      "columns": [
+        "session_id"
+      ],
+      "tableTo": "session",
+      "columnsTo": [
+        "id"
+      ],
+      "onUpdate": "NO ACTION",
+      "onDelete": "CASCADE",
+      "nameExplicit": false,
+      "name": "fk_message_session_id_session_id_fk",
+      "entityType": "fks",
+      "table": "message"
+    },
+    {
+      "columns": [
+        "message_id"
+      ],
+      "tableTo": "message",
+      "columnsTo": [
+        "id"
+      ],
+      "onUpdate": "NO ACTION",
+      "onDelete": "CASCADE",
+      "nameExplicit": false,
+      "name": "fk_part_message_id_message_id_fk",
+      "entityType": "fks",
+      "table": "part"
+    },
+    {
+      "columns": [
+        "project_id"
+      ],
+      "tableTo": "project",
+      "columnsTo": [
+        "id"
+      ],
+      "onUpdate": "NO ACTION",
+      "onDelete": "CASCADE",
+      "nameExplicit": false,
+      "name": "fk_permission_project_id_project_id_fk",
+      "entityType": "fks",
+      "table": "permission"
+    },
+    {
+      "columns": [
+        "project_id"
+      ],
+      "tableTo": "project",
+      "columnsTo": [
+        "id"
+      ],
+      "onUpdate": "NO ACTION",
+      "onDelete": "CASCADE",
+      "nameExplicit": false,
+      "name": "fk_session_project_id_project_id_fk",
+      "entityType": "fks",
+      "table": "session"
+    },
+    {
+      "columns": [
+        "session_id"
+      ],
+      "tableTo": "session",
+      "columnsTo": [
+        "id"
+      ],
+      "onUpdate": "NO ACTION",
+      "onDelete": "CASCADE",
+      "nameExplicit": false,
+      "name": "fk_todo_session_id_session_id_fk",
+      "entityType": "fks",
+      "table": "todo"
+    },
+    {
+      "columns": [
+        "session_id"
+      ],
+      "tableTo": "session",
+      "columnsTo": [
+        "id"
+      ],
+      "onUpdate": "NO ACTION",
+      "onDelete": "CASCADE",
+      "nameExplicit": false,
+      "name": "fk_session_share_session_id_session_id_fk",
+      "entityType": "fks",
+      "table": "session_share"
+    },
+    {
+      "columns": [
+        "session_id",
+        "id"
+      ],
+      "nameExplicit": false,
+      "name": "todo_pk",
+      "entityType": "pks",
+      "table": "todo"
+    },
+    {
+      "columns": [
+        "id"
+      ],
+      "nameExplicit": false,
+      "name": "project_pk",
+      "table": "project",
+      "entityType": "pks"
+    },
+    {
+      "columns": [
+        "id"
+      ],
+      "nameExplicit": false,
+      "name": "message_pk",
+      "table": "message",
+      "entityType": "pks"
+    },
+    {
+      "columns": [
+        "id"
+      ],
+      "nameExplicit": false,
+      "name": "part_pk",
+      "table": "part",
+      "entityType": "pks"
+    },
+    {
+      "columns": [
+        "project_id"
+      ],
+      "nameExplicit": false,
+      "name": "permission_pk",
+      "table": "permission",
+      "entityType": "pks"
+    },
+    {
+      "columns": [
+        "id"
+      ],
+      "nameExplicit": false,
+      "name": "session_pk",
+      "table": "session",
+      "entityType": "pks"
+    },
+    {
+      "columns": [
+        "session_id"
+      ],
+      "nameExplicit": false,
+      "name": "session_share_pk",
+      "table": "session_share",
+      "entityType": "pks"
+    },
+    {
+      "columns": [
+        {
+          "value": "session_id",
+          "isExpression": false
+        }
+      ],
+      "isUnique": false,
+      "where": null,
+      "origin": "manual",
+      "name": "message_session_idx",
+      "entityType": "indexes",
+      "table": "message"
+    },
+    {
+      "columns": [
+        {
+          "value": "message_id",
+          "isExpression": false
+        }
+      ],
+      "isUnique": false,
+      "where": null,
+      "origin": "manual",
+      "name": "part_message_idx",
+      "entityType": "indexes",
+      "table": "part"
+    },
+    {
+      "columns": [
+        {
+          "value": "session_id",
+          "isExpression": false
+        }
+      ],
+      "isUnique": false,
+      "where": null,
+      "origin": "manual",
+      "name": "part_session_idx",
+      "entityType": "indexes",
+      "table": "part"
+    },
+    {
+      "columns": [
+        {
+          "value": "project_id",
+          "isExpression": false
+        }
+      ],
+      "isUnique": false,
+      "where": null,
+      "origin": "manual",
+      "name": "session_project_idx",
+      "entityType": "indexes",
+      "table": "session"
+    },
+    {
+      "columns": [
+        {
+          "value": "parent_id",
+          "isExpression": false
+        }
+      ],
+      "isUnique": false,
+      "where": null,
+      "origin": "manual",
+      "name": "session_parent_idx",
+      "entityType": "indexes",
+      "table": "session"
+    },
+    {
+      "columns": [
+        {
+          "value": "session_id",
+          "isExpression": false
+        }
+      ],
+      "isUnique": false,
+      "where": null,
+      "origin": "manual",
+      "name": "todo_session_idx",
+      "entityType": "indexes",
+      "table": "todo"
+    }
+  ],
+  "renames": []
+}

+ 0 - 587
packages/opencode/migration/meta/0000_snapshot.json

@@ -1,587 +0,0 @@
-{
-  "version": "6",
-  "dialect": "sqlite",
-  "id": "797eb060-2c45-4abf-925d-6b8375dd8a64",
-  "prevId": "00000000-0000-0000-0000-000000000000",
-  "tables": {
-    "project": {
-      "name": "project",
-      "columns": {
-        "id": {
-          "name": "id",
-          "type": "text",
-          "primaryKey": true,
-          "notNull": true,
-          "autoincrement": false
-        },
-        "worktree": {
-          "name": "worktree",
-          "type": "text",
-          "primaryKey": false,
-          "notNull": true,
-          "autoincrement": false
-        },
-        "vcs": {
-          "name": "vcs",
-          "type": "text",
-          "primaryKey": false,
-          "notNull": false,
-          "autoincrement": false
-        },
-        "name": {
-          "name": "name",
-          "type": "text",
-          "primaryKey": false,
-          "notNull": false,
-          "autoincrement": false
-        },
-        "icon_url": {
-          "name": "icon_url",
-          "type": "text",
-          "primaryKey": false,
-          "notNull": false,
-          "autoincrement": false
-        },
-        "icon_color": {
-          "name": "icon_color",
-          "type": "text",
-          "primaryKey": false,
-          "notNull": false,
-          "autoincrement": false
-        },
-        "time_created": {
-          "name": "time_created",
-          "type": "integer",
-          "primaryKey": false,
-          "notNull": true,
-          "autoincrement": false
-        },
-        "time_updated": {
-          "name": "time_updated",
-          "type": "integer",
-          "primaryKey": false,
-          "notNull": true,
-          "autoincrement": false
-        },
-        "time_initialized": {
-          "name": "time_initialized",
-          "type": "integer",
-          "primaryKey": false,
-          "notNull": false,
-          "autoincrement": false
-        },
-        "sandboxes": {
-          "name": "sandboxes",
-          "type": "text",
-          "primaryKey": false,
-          "notNull": true,
-          "autoincrement": false
-        }
-      },
-      "indexes": {},
-      "foreignKeys": {},
-      "compositePrimaryKeys": {},
-      "uniqueConstraints": {},
-      "checkConstraints": {}
-    },
-    "message": {
-      "name": "message",
-      "columns": {
-        "id": {
-          "name": "id",
-          "type": "text",
-          "primaryKey": true,
-          "notNull": true,
-          "autoincrement": false
-        },
-        "session_id": {
-          "name": "session_id",
-          "type": "text",
-          "primaryKey": false,
-          "notNull": true,
-          "autoincrement": false
-        },
-        "created_at": {
-          "name": "created_at",
-          "type": "integer",
-          "primaryKey": false,
-          "notNull": true,
-          "autoincrement": false
-        },
-        "data": {
-          "name": "data",
-          "type": "text",
-          "primaryKey": false,
-          "notNull": true,
-          "autoincrement": false
-        }
-      },
-      "indexes": {
-        "message_session_idx": {
-          "name": "message_session_idx",
-          "columns": [
-            "session_id"
-          ],
-          "isUnique": false
-        }
-      },
-      "foreignKeys": {
-        "message_session_id_session_id_fk": {
-          "name": "message_session_id_session_id_fk",
-          "tableFrom": "message",
-          "tableTo": "session",
-          "columnsFrom": [
-            "session_id"
-          ],
-          "columnsTo": [
-            "id"
-          ],
-          "onDelete": "cascade",
-          "onUpdate": "no action"
-        }
-      },
-      "compositePrimaryKeys": {},
-      "uniqueConstraints": {},
-      "checkConstraints": {}
-    },
-    "part": {
-      "name": "part",
-      "columns": {
-        "id": {
-          "name": "id",
-          "type": "text",
-          "primaryKey": true,
-          "notNull": true,
-          "autoincrement": false
-        },
-        "message_id": {
-          "name": "message_id",
-          "type": "text",
-          "primaryKey": false,
-          "notNull": true,
-          "autoincrement": false
-        },
-        "session_id": {
-          "name": "session_id",
-          "type": "text",
-          "primaryKey": false,
-          "notNull": true,
-          "autoincrement": false
-        },
-        "data": {
-          "name": "data",
-          "type": "text",
-          "primaryKey": false,
-          "notNull": true,
-          "autoincrement": false
-        }
-      },
-      "indexes": {
-        "part_message_idx": {
-          "name": "part_message_idx",
-          "columns": [
-            "message_id"
-          ],
-          "isUnique": false
-        },
-        "part_session_idx": {
-          "name": "part_session_idx",
-          "columns": [
-            "session_id"
-          ],
-          "isUnique": false
-        }
-      },
-      "foreignKeys": {
-        "part_message_id_message_id_fk": {
-          "name": "part_message_id_message_id_fk",
-          "tableFrom": "part",
-          "tableTo": "message",
-          "columnsFrom": [
-            "message_id"
-          ],
-          "columnsTo": [
-            "id"
-          ],
-          "onDelete": "cascade",
-          "onUpdate": "no action"
-        }
-      },
-      "compositePrimaryKeys": {},
-      "uniqueConstraints": {},
-      "checkConstraints": {}
-    },
-    "permission": {
-      "name": "permission",
-      "columns": {
-        "project_id": {
-          "name": "project_id",
-          "type": "text",
-          "primaryKey": true,
-          "notNull": true,
-          "autoincrement": false
-        },
-        "data": {
-          "name": "data",
-          "type": "text",
-          "primaryKey": false,
-          "notNull": true,
-          "autoincrement": false
-        }
-      },
-      "indexes": {},
-      "foreignKeys": {
-        "permission_project_id_project_id_fk": {
-          "name": "permission_project_id_project_id_fk",
-          "tableFrom": "permission",
-          "tableTo": "project",
-          "columnsFrom": [
-            "project_id"
-          ],
-          "columnsTo": [
-            "id"
-          ],
-          "onDelete": "cascade",
-          "onUpdate": "no action"
-        }
-      },
-      "compositePrimaryKeys": {},
-      "uniqueConstraints": {},
-      "checkConstraints": {}
-    },
-    "session_diff": {
-      "name": "session_diff",
-      "columns": {
-        "session_id": {
-          "name": "session_id",
-          "type": "text",
-          "primaryKey": true,
-          "notNull": true,
-          "autoincrement": false
-        },
-        "data": {
-          "name": "data",
-          "type": "text",
-          "primaryKey": false,
-          "notNull": true,
-          "autoincrement": false
-        }
-      },
-      "indexes": {},
-      "foreignKeys": {
-        "session_diff_session_id_session_id_fk": {
-          "name": "session_diff_session_id_session_id_fk",
-          "tableFrom": "session_diff",
-          "tableTo": "session",
-          "columnsFrom": [
-            "session_id"
-          ],
-          "columnsTo": [
-            "id"
-          ],
-          "onDelete": "cascade",
-          "onUpdate": "no action"
-        }
-      },
-      "compositePrimaryKeys": {},
-      "uniqueConstraints": {},
-      "checkConstraints": {}
-    },
-    "session": {
-      "name": "session",
-      "columns": {
-        "id": {
-          "name": "id",
-          "type": "text",
-          "primaryKey": true,
-          "notNull": true,
-          "autoincrement": false
-        },
-        "project_id": {
-          "name": "project_id",
-          "type": "text",
-          "primaryKey": false,
-          "notNull": true,
-          "autoincrement": false
-        },
-        "parent_id": {
-          "name": "parent_id",
-          "type": "text",
-          "primaryKey": false,
-          "notNull": false,
-          "autoincrement": false
-        },
-        "slug": {
-          "name": "slug",
-          "type": "text",
-          "primaryKey": false,
-          "notNull": true,
-          "autoincrement": false
-        },
-        "directory": {
-          "name": "directory",
-          "type": "text",
-          "primaryKey": false,
-          "notNull": true,
-          "autoincrement": false
-        },
-        "title": {
-          "name": "title",
-          "type": "text",
-          "primaryKey": false,
-          "notNull": true,
-          "autoincrement": false
-        },
-        "version": {
-          "name": "version",
-          "type": "text",
-          "primaryKey": false,
-          "notNull": true,
-          "autoincrement": false
-        },
-        "share_url": {
-          "name": "share_url",
-          "type": "text",
-          "primaryKey": false,
-          "notNull": false,
-          "autoincrement": false
-        },
-        "summary_additions": {
-          "name": "summary_additions",
-          "type": "integer",
-          "primaryKey": false,
-          "notNull": false,
-          "autoincrement": false
-        },
-        "summary_deletions": {
-          "name": "summary_deletions",
-          "type": "integer",
-          "primaryKey": false,
-          "notNull": false,
-          "autoincrement": false
-        },
-        "summary_files": {
-          "name": "summary_files",
-          "type": "integer",
-          "primaryKey": false,
-          "notNull": false,
-          "autoincrement": false
-        },
-        "summary_diffs": {
-          "name": "summary_diffs",
-          "type": "text",
-          "primaryKey": false,
-          "notNull": false,
-          "autoincrement": false
-        },
-        "revert_message_id": {
-          "name": "revert_message_id",
-          "type": "text",
-          "primaryKey": false,
-          "notNull": false,
-          "autoincrement": false
-        },
-        "revert_part_id": {
-          "name": "revert_part_id",
-          "type": "text",
-          "primaryKey": false,
-          "notNull": false,
-          "autoincrement": false
-        },
-        "revert_snapshot": {
-          "name": "revert_snapshot",
-          "type": "text",
-          "primaryKey": false,
-          "notNull": false,
-          "autoincrement": false
-        },
-        "revert_diff": {
-          "name": "revert_diff",
-          "type": "text",
-          "primaryKey": false,
-          "notNull": false,
-          "autoincrement": false
-        },
-        "permission": {
-          "name": "permission",
-          "type": "text",
-          "primaryKey": false,
-          "notNull": false,
-          "autoincrement": false
-        },
-        "time_created": {
-          "name": "time_created",
-          "type": "integer",
-          "primaryKey": false,
-          "notNull": true,
-          "autoincrement": false
-        },
-        "time_updated": {
-          "name": "time_updated",
-          "type": "integer",
-          "primaryKey": false,
-          "notNull": true,
-          "autoincrement": false
-        },
-        "time_compacting": {
-          "name": "time_compacting",
-          "type": "integer",
-          "primaryKey": false,
-          "notNull": false,
-          "autoincrement": false
-        },
-        "time_archived": {
-          "name": "time_archived",
-          "type": "integer",
-          "primaryKey": false,
-          "notNull": false,
-          "autoincrement": false
-        }
-      },
-      "indexes": {
-        "session_project_idx": {
-          "name": "session_project_idx",
-          "columns": [
-            "project_id"
-          ],
-          "isUnique": false
-        },
-        "session_parent_idx": {
-          "name": "session_parent_idx",
-          "columns": [
-            "parent_id"
-          ],
-          "isUnique": false
-        }
-      },
-      "foreignKeys": {
-        "session_project_id_project_id_fk": {
-          "name": "session_project_id_project_id_fk",
-          "tableFrom": "session",
-          "tableTo": "project",
-          "columnsFrom": [
-            "project_id"
-          ],
-          "columnsTo": [
-            "id"
-          ],
-          "onDelete": "cascade",
-          "onUpdate": "no action"
-        }
-      },
-      "compositePrimaryKeys": {},
-      "uniqueConstraints": {},
-      "checkConstraints": {}
-    },
-    "todo": {
-      "name": "todo",
-      "columns": {
-        "session_id": {
-          "name": "session_id",
-          "type": "text",
-          "primaryKey": true,
-          "notNull": true,
-          "autoincrement": false
-        },
-        "data": {
-          "name": "data",
-          "type": "text",
-          "primaryKey": false,
-          "notNull": true,
-          "autoincrement": false
-        }
-      },
-      "indexes": {},
-      "foreignKeys": {
-        "todo_session_id_session_id_fk": {
-          "name": "todo_session_id_session_id_fk",
-          "tableFrom": "todo",
-          "tableTo": "session",
-          "columnsFrom": [
-            "session_id"
-          ],
-          "columnsTo": [
-            "id"
-          ],
-          "onDelete": "cascade",
-          "onUpdate": "no action"
-        }
-      },
-      "compositePrimaryKeys": {},
-      "uniqueConstraints": {},
-      "checkConstraints": {}
-    },
-    "session_share": {
-      "name": "session_share",
-      "columns": {
-        "session_id": {
-          "name": "session_id",
-          "type": "text",
-          "primaryKey": true,
-          "notNull": true,
-          "autoincrement": false
-        },
-        "data": {
-          "name": "data",
-          "type": "text",
-          "primaryKey": false,
-          "notNull": true,
-          "autoincrement": false
-        }
-      },
-      "indexes": {},
-      "foreignKeys": {
-        "session_share_session_id_session_id_fk": {
-          "name": "session_share_session_id_session_id_fk",
-          "tableFrom": "session_share",
-          "tableTo": "session",
-          "columnsFrom": [
-            "session_id"
-          ],
-          "columnsTo": [
-            "id"
-          ],
-          "onDelete": "cascade",
-          "onUpdate": "no action"
-        }
-      },
-      "compositePrimaryKeys": {},
-      "uniqueConstraints": {},
-      "checkConstraints": {}
-    },
-    "share": {
-      "name": "share",
-      "columns": {
-        "session_id": {
-          "name": "session_id",
-          "type": "text",
-          "primaryKey": true,
-          "notNull": true,
-          "autoincrement": false
-        },
-        "data": {
-          "name": "data",
-          "type": "text",
-          "primaryKey": false,
-          "notNull": true,
-          "autoincrement": false
-        }
-      },
-      "indexes": {},
-      "foreignKeys": {},
-      "compositePrimaryKeys": {},
-      "uniqueConstraints": {},
-      "checkConstraints": {}
-    }
-  },
-  "views": {},
-  "enums": {},
-  "_meta": {
-    "schemas": {},
-    "tables": {},
-    "columns": {}
-  },
-  "internal": {
-    "indexes": {}
-  }
-}

+ 0 - 13
packages/opencode/migration/meta/_journal.json

@@ -1,13 +0,0 @@
-{
-  "version": "7",
-  "dialect": "sqlite",
-  "entries": [
-    {
-      "idx": 0,
-      "version": "6",
-      "when": 1769232577135,
-      "tag": "0000_magical_strong_guy",
-      "breakpoints": true
-    }
-  ]
-}

+ 5 - 1
packages/opencode/package.json

@@ -26,7 +26,6 @@
   },
   "devDependencies": {
     "@babel/core": "7.28.4",
-    "drizzle-kit": "1.0.0-beta.12-a5629fb",
     "@octokit/webhooks-types": "7.6.1",
     "@opencode-ai/script": "workspace:*",
     "@parcel/watcher-darwin-arm64": "2.5.1",
@@ -43,6 +42,8 @@
     "@types/turndown": "5.0.5",
     "@types/yargs": "17.0.33",
     "@typescript/native-preview": "catalog:",
+    "drizzle-kit": "1.0.0-beta.12-a5629fb",
+    "drizzle-orm": "1.0.0-beta.12-a5629fb",
     "typescript": "catalog:",
     "vscode-languageserver-types": "3.17.5",
     "why-is-node-running": "3.2.2",
@@ -122,5 +123,8 @@
     "yargs": "18.0.0",
     "zod": "catalog:",
     "zod-to-json-schema": "3.24.5"
+  },
+  "overrides": {
+    "drizzle-orm": "1.0.0-beta.12-a5629fb"
   }
 }

+ 0 - 144
packages/opencode/src/cli/cmd/database.ts

@@ -1,144 +0,0 @@
-import type { Argv } from "yargs"
-import { cmd } from "./cmd"
-import { bootstrap } from "../bootstrap"
-import { UI } from "../ui"
-import { Database } from "../../storage/db"
-import { ProjectTable } from "../../project/project.sql"
-import { Project } from "../../project/project"
-import {
-  SessionTable,
-  MessageTable,
-  PartTable,
-  SessionDiffTable,
-  TodoTable,
-  PermissionTable,
-} from "../../session/session.sql"
-import { Session } from "../../session"
-import { SessionShareTable, ShareTable } from "../../share/share.sql"
-import path from "path"
-import fs from "fs/promises"
-
-export const DatabaseCommand = cmd({
-  command: "database",
-  describe: "database management commands",
-  builder: (yargs) => yargs.command(ExportCommand).demandCommand(),
-  async handler() {},
-})
-
-const ExportCommand = cmd({
-  command: "export",
-  describe: "export database to JSON files",
-  builder: (yargs: Argv) => {
-    return yargs.option("output", {
-      alias: ["o"],
-      describe: "output directory",
-      type: "string",
-      demandOption: true,
-    })
-  },
-  handler: async (args) => {
-    await bootstrap(process.cwd(), async () => {
-      const outDir = path.resolve(args.output)
-      await fs.mkdir(outDir, { recursive: true })
-
-      const stats = {
-        projects: 0,
-        sessions: 0,
-        messages: 0,
-        parts: 0,
-        diffs: 0,
-        todos: 0,
-        permissions: 0,
-        sessionShares: 0,
-        shares: 0,
-      }
-
-      // Export projects
-      const projectDir = path.join(outDir, "project")
-      await fs.mkdir(projectDir, { recursive: true })
-      for (const row of Database.use((db) => db.select().from(ProjectTable).all())) {
-        const project = Project.fromRow(row)
-        await Bun.write(path.join(projectDir, `${row.id}.json`), JSON.stringify(project, null, 2))
-        stats.projects++
-      }
-
-      // Export sessions (organized by projectID)
-      const sessionDir = path.join(outDir, "session")
-      for (const row of Database.use((db) => db.select().from(SessionTable).all())) {
-        const dir = path.join(sessionDir, row.project_id)
-        await fs.mkdir(dir, { recursive: true })
-        await Bun.write(path.join(dir, `${row.id}.json`), JSON.stringify(Session.fromRow(row), null, 2))
-        stats.sessions++
-      }
-
-      // Export messages (organized by sessionID)
-      const messageDir = path.join(outDir, "message")
-      for (const row of Database.use((db) => db.select().from(MessageTable).all())) {
-        const dir = path.join(messageDir, row.session_id)
-        await fs.mkdir(dir, { recursive: true })
-        await Bun.write(path.join(dir, `${row.id}.json`), JSON.stringify(row.data, null, 2))
-        stats.messages++
-      }
-
-      // Export parts (organized by messageID)
-      const partDir = path.join(outDir, "part")
-      for (const row of Database.use((db) => db.select().from(PartTable).all())) {
-        const dir = path.join(partDir, row.message_id)
-        await fs.mkdir(dir, { recursive: true })
-        await Bun.write(path.join(dir, `${row.id}.json`), JSON.stringify(row.data, null, 2))
-        stats.parts++
-      }
-
-      // Export session diffs
-      const diffDir = path.join(outDir, "session_diff")
-      await fs.mkdir(diffDir, { recursive: true })
-      for (const row of Database.use((db) => db.select().from(SessionDiffTable).all())) {
-        await Bun.write(path.join(diffDir, `${row.session_id}.json`), JSON.stringify(row.data, null, 2))
-        stats.diffs++
-      }
-
-      // Export todos
-      const todoDir = path.join(outDir, "todo")
-      await fs.mkdir(todoDir, { recursive: true })
-      for (const row of Database.use((db) => db.select().from(TodoTable).all())) {
-        await Bun.write(path.join(todoDir, `${row.session_id}.json`), JSON.stringify(row.data, null, 2))
-        stats.todos++
-      }
-
-      // Export permissions
-      const permDir = path.join(outDir, "permission")
-      await fs.mkdir(permDir, { recursive: true })
-      for (const row of Database.use((db) => db.select().from(PermissionTable).all())) {
-        await Bun.write(path.join(permDir, `${row.project_id}.json`), JSON.stringify(row.data, null, 2))
-        stats.permissions++
-      }
-
-      // Export session shares
-      const sessionShareDir = path.join(outDir, "session_share")
-      await fs.mkdir(sessionShareDir, { recursive: true })
-      for (const row of Database.use((db) => db.select().from(SessionShareTable).all())) {
-        await Bun.write(path.join(sessionShareDir, `${row.session_id}.json`), JSON.stringify(row.data, null, 2))
-        stats.sessionShares++
-      }
-
-      // Export shares
-      const shareDir = path.join(outDir, "share")
-      await fs.mkdir(shareDir, { recursive: true })
-      for (const row of Database.use((db) => db.select().from(ShareTable).all())) {
-        await Bun.write(path.join(shareDir, `${row.session_id}.json`), JSON.stringify(row.data, null, 2))
-        stats.shares++
-      }
-
-      UI.println(`Exported to ${outDir}:`)
-      UI.println(`  ${stats.projects} projects`)
-      UI.println(`  ${stats.sessions} sessions`)
-      UI.println(`  ${stats.messages} messages`)
-      UI.println(`  ${stats.parts} parts`)
-      UI.println(`  ${stats.diffs} session diffs`)
-      UI.println(`  ${stats.todos} todos`)
-      UI.println(`  ${stats.permissions} permissions`)
-      UI.println(`  ${stats.sessionShares} session shares`)
-      UI.println(`  ${stats.shares} shares`)
-    })
-  },
-})

+ 11 - 2
packages/opencode/src/index.ts

@@ -26,7 +26,10 @@ import { EOL } from "os"
 import { WebCommand } from "./cli/cmd/web"
 import { PrCommand } from "./cli/cmd/pr"
 import { SessionCommand } from "./cli/cmd/session"
-import { DatabaseCommand } from "./cli/cmd/database"
+import path from "path"
+import { Global } from "./global"
+import { JsonMigration } from "./storage/json-migration"
+import { Database } from "./storage/db"
 
 process.on("unhandledRejection", (e) => {
   Log.Default.error("rejection", {
@@ -75,6 +78,13 @@ const cli = yargs(hideBin(process.argv))
       version: Installation.VERSION,
       args: process.argv.slice(2),
     })
+
+    const marker = path.join(Global.Path.data, "opencode.db")
+    if (!(await Bun.file(marker).exists())) {
+      console.log("Performing one time database migration, may take a few minutes...")
+      await JsonMigration.run(Database.Client().$client)
+      console.log("Database migration complete.")
+    }
   })
   .usage("\n" + UI.logo())
   .completion("completion", "generate shell completion script")
@@ -98,7 +108,6 @@ const cli = yargs(hideBin(process.argv))
   .command(GithubCommand)
   .command(PrCommand)
   .command(SessionCommand)
-  .command(DatabaseCommand)
   .fail((msg, err) => {
     if (
       msg?.startsWith("Unknown argument") ||

+ 0 - 2
packages/opencode/src/project/bootstrap.ts

@@ -1,5 +1,4 @@
 import { Plugin } from "../plugin"
-import { Share } from "../share/share"
 import { Format } from "../format"
 import { LSP } from "../lsp"
 import { FileWatcher } from "../file/watcher"
@@ -17,7 +16,6 @@ import { Truncate } from "../tool/truncation"
 export async function InstanceBootstrap() {
   Log.Default.info("bootstrapping", { directory: Instance.directory })
   await Plugin.init()
-  Share.init()
   ShareNext.init()
   Format.init()
   await LSP.init()

+ 7 - 21
packages/opencode/src/session/index.ts

@@ -11,8 +11,8 @@ import { Identifier } from "../id/id"
 import { Installation } from "../installation"
 
 import { Database, NotFoundError, eq } from "../storage/db"
-import { SessionTable, MessageTable, PartTable, SessionDiffTable } from "./session.sql"
-import { ShareTable } from "../share/share.sql"
+import { SessionTable, MessageTable, PartTable } from "./session.sql"
+import { Storage } from "@/storage/storage"
 import { Log } from "../util/log"
 import { MessageV2 } from "./message-v2"
 import { Instance } from "../project/instance"
@@ -153,16 +153,6 @@ export namespace Session {
     })
   export type Info = z.output<typeof Info>
 
-  export const ShareInfo = z
-    .object({
-      secret: z.string(),
-      url: z.string(),
-    })
-    .meta({
-      ref: "SessionShare",
-    })
-  export type ShareInfo = z.output<typeof ShareInfo>
-
   export const Event = {
     Created: BusEvent.define(
       "session.created",
@@ -323,11 +313,6 @@ export namespace Session {
     return fromRow(row)
   })
 
-  export const getShare = fn(Identifier.schema("session"), async (id) => {
-    const row = Database.use((db) => db.select().from(ShareTable).where(eq(ShareTable.session_id, id)).get())
-    return row?.data
-  })
-
   export const share = fn(Identifier.schema("session"), async (id) => {
     const cfg = await Config.get()
     if (cfg.share === "disabled") {
@@ -498,10 +483,11 @@ export namespace Session {
   )
 
   export const diff = fn(Identifier.schema("session"), async (sessionID) => {
-    const row = Database.use((db) =>
-      db.select().from(SessionDiffTable).where(eq(SessionDiffTable.session_id, sessionID)).get(),
-    )
-    return row?.data ?? []
+    try {
+      return await Storage.read<Snapshot.FileDiff[]>(["session_diff", sessionID])
+    } catch {
+      return []
+    }
   })
 
   export const messages = fn(

+ 3 - 8
packages/opencode/src/session/revert.ts

@@ -5,7 +5,8 @@ import { MessageV2 } from "./message-v2"
 import { Session } from "."
 import { Log } from "../util/log"
 import { Database, eq } from "../storage/db"
-import { SessionDiffTable, MessageTable, PartTable } from "./session.sql"
+import { MessageTable, PartTable } from "./session.sql"
+import { Storage } from "@/storage/storage"
 import { Bus } from "../bus"
 import { SessionPrompt } from "./prompt"
 import { SessionSummary } from "./summary"
@@ -60,13 +61,7 @@ export namespace SessionRevert {
       if (revert.snapshot) revert.diff = await Snapshot.diff(revert.snapshot)
       const rangeMessages = all.filter((msg) => msg.info.id >= revert!.messageID)
       const diffs = await SessionSummary.computeDiff({ messages: rangeMessages })
-      Database.use((db) =>
-        db
-          .insert(SessionDiffTable)
-          .values({ session_id: input.sessionID, data: diffs })
-          .onConflictDoUpdate({ target: SessionDiffTable.session_id, set: { data: diffs } })
-          .run(),
-      )
+      await Storage.write(["session_diff", input.sessionID], diffs)
       Bus.publish(Session.Event.Diff, {
         sessionID: input.sessionID,
         diff: diffs,

+ 15 - 15
packages/opencode/src/session/session.sql.ts

@@ -1,8 +1,7 @@
-import { sqliteTable, text, integer, index } from "drizzle-orm/sqlite-core"
+import { sqliteTable, text, integer, index, primaryKey } from "drizzle-orm/sqlite-core"
 import { ProjectTable } from "../project/project.sql"
 import type { MessageV2 } from "./message-v2"
 import type { Snapshot } from "@/snapshot"
-import type { Todo } from "./todo"
 import type { PermissionNext } from "@/permission/next"
 
 export const SessionTable = sqliteTable(
@@ -61,19 +60,20 @@ export const PartTable = sqliteTable(
   (table) => [index("part_message_idx").on(table.message_id), index("part_session_idx").on(table.session_id)],
 )
 
-export const SessionDiffTable = sqliteTable("session_diff", {
-  session_id: text()
-    .primaryKey()
-    .references(() => SessionTable.id, { onDelete: "cascade" }),
-  data: text({ mode: "json" }).notNull().$type<Snapshot.FileDiff[]>(),
-})
-
-export const TodoTable = sqliteTable("todo", {
-  session_id: text()
-    .primaryKey()
-    .references(() => SessionTable.id, { onDelete: "cascade" }),
-  data: text({ mode: "json" }).notNull().$type<Todo.Info[]>(),
-})
+export const TodoTable = sqliteTable(
+  "todo",
+  {
+    session_id: text()
+      .notNull()
+      .references(() => SessionTable.id, { onDelete: "cascade" }),
+    id: text().notNull(),
+    content: text().notNull(),
+    status: text().notNull(),
+    priority: text().notNull(),
+    position: integer().notNull(),
+  },
+  (table) => [primaryKey({ columns: [table.session_id, table.id] }), index("todo_session_idx").on(table.session_id)],
+)
 
 export const PermissionTable = sqliteTable("permission", {
   project_id: text()

+ 7 - 13
packages/opencode/src/session/summary.ts

@@ -11,8 +11,7 @@ import { Snapshot } from "@/snapshot"
 import { Log } from "@/util/log"
 import path from "path"
 import { Instance } from "@/project/instance"
-import { Database, eq } from "@/storage/db"
-import { SessionDiffTable } from "./session.sql"
+import { Storage } from "@/storage/storage"
 import { Bus } from "@/bus"
 
 import { LLM } from "./llm"
@@ -56,13 +55,7 @@ export namespace SessionSummary {
         files: diffs.length,
       },
     })
-    Database.use((db) =>
-      db
-        .insert(SessionDiffTable)
-        .values({ session_id: input.sessionID, data: diffs })
-        .onConflictDoUpdate({ target: SessionDiffTable.session_id, set: { data: diffs } })
-        .run(),
-    )
+    await Storage.write(["session_diff", input.sessionID], diffs)
     Bus.publish(Session.Event.Diff, {
       sessionID: input.sessionID,
       diff: diffs,
@@ -124,10 +117,11 @@ export namespace SessionSummary {
       messageID: Identifier.schema("message").optional(),
     }),
     async (input) => {
-      const row = Database.use((db) =>
-        db.select().from(SessionDiffTable).where(eq(SessionDiffTable.session_id, input.sessionID)).get(),
-      )
-      return row?.data ?? []
+      try {
+        return await Storage.read<Snapshot.FileDiff[]>(["session_diff", input.sessionID])
+      } catch {
+        return []
+      }
     },
   )
 

+ 26 - 10
packages/opencode/src/session/todo.ts

@@ -1,7 +1,7 @@
 import { BusEvent } from "@/bus/bus-event"
 import { Bus } from "@/bus"
 import z from "zod"
-import { Database, eq } from "../storage/db"
+import { Database, eq, asc } from "../storage/db"
 import { TodoTable } from "./session.sql"
 
 export namespace Todo {
@@ -26,18 +26,34 @@ export namespace Todo {
   }
 
   export function update(input: { sessionID: string; todos: Info[] }) {
-    Database.use((db) =>
-      db
-        .insert(TodoTable)
-        .values({ session_id: input.sessionID, data: input.todos })
-        .onConflictDoUpdate({ target: TodoTable.session_id, set: { data: input.todos } })
-        .run(),
-    )
+    Database.transaction((db) => {
+      db.delete(TodoTable).where(eq(TodoTable.session_id, input.sessionID)).run()
+      if (input.todos.length === 0) return
+      db.insert(TodoTable)
+        .values(
+          input.todos.map((todo, position) => ({
+            session_id: input.sessionID,
+            id: todo.id,
+            content: todo.content,
+            status: todo.status,
+            priority: todo.priority,
+            position,
+          })),
+        )
+        .run()
+    })
     Bus.publish(Event.Updated, input)
   }
 
   export function get(sessionID: string) {
-    const row = Database.use((db) => db.select().from(TodoTable).where(eq(TodoTable.session_id, sessionID)).get())
-    return row?.data ?? []
+    const rows = Database.use((db) =>
+      db.select().from(TodoTable).where(eq(TodoTable.session_id, sessionID)).orderBy(asc(TodoTable.position)).all(),
+    )
+    return rows.map((row) => ({
+      id: row.id,
+      content: row.content,
+      status: row.status,
+      priority: row.priority,
+    }))
   }
 }

+ 7 - 3
packages/opencode/src/share/share-next.ts

@@ -81,8 +81,11 @@ export namespace ShareNext {
     Database.use((db) =>
       db
         .insert(SessionShareTable)
-        .values({ session_id: sessionID, data: result })
-        .onConflictDoUpdate({ target: SessionShareTable.session_id, set: { data: result } })
+        .values({ session_id: sessionID, id: result.id, secret: result.secret, url: result.url })
+        .onConflictDoUpdate({
+          target: SessionShareTable.session_id,
+          set: { id: result.id, secret: result.secret, url: result.url },
+        })
         .run(),
     )
     fullSync(sessionID)
@@ -93,7 +96,8 @@ export namespace ShareNext {
     const row = Database.use((db) =>
       db.select().from(SessionShareTable).where(eq(SessionShareTable.session_id, sessionID)).get(),
     )
-    return row?.data
+    if (!row) return
+    return { id: row.id, secret: row.secret, url: row.url }
   }
 
   type Data =

+ 3 - 11
packages/opencode/src/share/share.sql.ts

@@ -1,19 +1,11 @@
 import { sqliteTable, text } from "drizzle-orm/sqlite-core"
 import { SessionTable } from "../session/session.sql"
-import type { Session } from "../session"
 
 export const SessionShareTable = sqliteTable("session_share", {
   session_id: text()
     .primaryKey()
     .references(() => SessionTable.id, { onDelete: "cascade" }),
-  data: text({ mode: "json" }).notNull().$type<{
-    id: string
-    secret: string
-    url: string
-  }>(),
-})
-
-export const ShareTable = sqliteTable("share", {
-  session_id: text().primaryKey(),
-  data: text({ mode: "json" }).notNull().$type<Session.ShareInfo>(),
+  id: text().notNull(),
+  secret: text().notNull(),
+  url: text().notNull(),
 })

+ 0 - 92
packages/opencode/src/share/share.ts

@@ -1,92 +0,0 @@
-import { Bus } from "../bus"
-import { Installation } from "../installation"
-import { Session } from "../session"
-import { MessageV2 } from "../session/message-v2"
-import { Log } from "../util/log"
-
-export namespace Share {
-  const log = Log.create({ service: "share" })
-
-  let queue: Promise<void> = Promise.resolve()
-  const pending = new Map<string, any>()
-
-  export async function sync(key: string, content: any) {
-    if (disabled) return
-    const [root, ...splits] = key.split("/")
-    if (root !== "session") return
-    const [sub, sessionID] = splits
-    if (sub === "share") return
-    const share = await Session.getShare(sessionID).catch(() => {})
-    if (!share) return
-    const { secret } = share
-    pending.set(key, content)
-    queue = queue
-      .then(async () => {
-        const content = pending.get(key)
-        if (content === undefined) return
-        pending.delete(key)
-
-        return fetch(`${URL}/share_sync`, {
-          method: "POST",
-          body: JSON.stringify({
-            sessionID: sessionID,
-            secret,
-            key: key,
-            content,
-          }),
-        })
-      })
-      .then((x) => {
-        if (x) {
-          log.info("synced", {
-            key: key,
-            status: x.status,
-          })
-        }
-      })
-  }
-
-  export function init() {
-    Bus.subscribe(Session.Event.Updated, async (evt) => {
-      await sync("session/info/" + evt.properties.info.id, evt.properties.info)
-    })
-    Bus.subscribe(MessageV2.Event.Updated, async (evt) => {
-      await sync("session/message/" + evt.properties.info.sessionID + "/" + evt.properties.info.id, evt.properties.info)
-    })
-    Bus.subscribe(MessageV2.Event.PartUpdated, async (evt) => {
-      await sync(
-        "session/part/" +
-          evt.properties.part.sessionID +
-          "/" +
-          evt.properties.part.messageID +
-          "/" +
-          evt.properties.part.id,
-        evt.properties.part,
-      )
-    })
-  }
-
-  export const URL =
-    process.env["OPENCODE_API"] ??
-    (Installation.isPreview() || Installation.isLocal() ? "https://api.dev.opencode.ai" : "https://api.opencode.ai")
-
-  const disabled = process.env["OPENCODE_DISABLE_SHARE"] === "true" || process.env["OPENCODE_DISABLE_SHARE"] === "1"
-
-  export async function create(sessionID: string) {
-    if (disabled) return { url: "", secret: "" }
-    return fetch(`${URL}/share_create`, {
-      method: "POST",
-      body: JSON.stringify({ sessionID: sessionID }),
-    })
-      .then((x) => x.json())
-      .then((x) => x as { url: string; secret: string })
-  }
-
-  export async function remove(sessionID: string, secret: string) {
-    if (disabled) return {}
-    return fetch(`${URL}/share_delete`, {
-      method: "POST",
-      body: JSON.stringify({ sessionID, secret }),
-    }).then((x) => x.json())
-  }
-}

+ 35 - 29
packages/opencode/src/storage/db.ts

@@ -7,11 +7,12 @@ import { Context } from "../util/context"
 import { lazy } from "../util/lazy"
 import { Global } from "../global"
 import { Log } from "../util/log"
-import { migrateFromJson } from "./json-migration"
 import { NamedError } from "@opencode-ai/util/error"
 import z from "zod"
 import path from "path"
-import { readFileSync } from "fs"
+import { readFileSync, readdirSync } from "fs"
+import fs from "fs/promises"
+import { Instance } from "@/project/instance"
 
 declare const OPENCODE_MIGRATIONS: { sql: string; timestamp: number }[] | undefined
 
@@ -31,21 +32,39 @@ export namespace Database {
 
   type Journal = { sql: string; timestamp: number }[]
 
-  function journal(dir: string): Journal {
-    const file = path.join(dir, "meta/_journal.json")
-    if (!Bun.file(file).size) return []
+  function time(tag: string) {
+    const match = /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/.exec(tag)
+    if (!match) return 0
+    return Date.UTC(
+      Number(match[1]),
+      Number(match[2]) - 1,
+      Number(match[3]),
+      Number(match[4]),
+      Number(match[5]),
+      Number(match[6]),
+    )
+  }
 
-    const data = JSON.parse(readFileSync(file, "utf-8")) as {
-      entries: { tag: string; when: number }[]
-    }
+  function migrations(dir: string): Journal {
+    const dirs = readdirSync(dir, { withFileTypes: true })
+      .filter((entry) => entry.isDirectory())
+      .map((entry) => entry.name)
+
+    const sql = dirs
+      .map((name) => {
+        const file = path.join(dir, name, "migration.sql")
+        if (!Bun.file(file).size) return
+        return {
+          sql: readFileSync(file, "utf-8"),
+          timestamp: time(name),
+        }
+      })
+      .filter(Boolean) as Journal
 
-    return data.entries.map((entry) => ({
-      sql: readFileSync(path.join(dir, `${entry.tag}.sql`), "utf-8"),
-      timestamp: entry.when,
-    }))
+    return sql.sort((a, b) => a.timestamp - b.timestamp)
   }
 
-  const client = lazy(() => {
+  export const Client = lazy(() => {
     log.info("opening database", { path: path.join(Global.Path.data, "opencode.db") })
 
     const sqlite = new BunDatabase(path.join(Global.Path.data, "opencode.db"), { create: true })
@@ -62,7 +81,7 @@ export namespace Database {
     const entries =
       typeof OPENCODE_MIGRATIONS !== "undefined"
         ? OPENCODE_MIGRATIONS
-        : journal(path.join(import.meta.dirname, "../../migration"))
+        : migrations(path.join(import.meta.dirname, "../../migration"))
     if (entries.length > 0) {
       log.info("applying migrations", {
         count: entries.length,
@@ -71,19 +90,6 @@ export namespace Database {
       migrate(db, entries)
     }
 
-    // Run json migration if not already done
-    if (!sqlite.prepare("SELECT 1 FROM __drizzle_migrations WHERE hash = 'json-migration'").get()) {
-      Bun.file(path.join(Global.Path.data, "storage/project"))
-        .exists()
-        .then((exists) => {
-          if (!exists) return
-          return migrateFromJson(sqlite).then(() => {
-            sqlite.run("INSERT INTO __drizzle_migrations (hash, created_at) VALUES ('json-migration', ?)", [Date.now()])
-          })
-        })
-        .catch((e) => log.error("json migration failed", { error: e }))
-    }
-
     return db
   })
 
@@ -100,7 +106,7 @@ export namespace Database {
     } catch (err) {
       if (err instanceof Context.NotFound) {
         const effects: (() => void | Promise<void>)[] = []
-        const result = ctx.provide({ effects, tx: client() }, () => callback(client()))
+        const result = ctx.provide({ effects, tx: Client() }, () => callback(Client()))
         for (const effect of effects) effect()
         return result
       }
@@ -122,7 +128,7 @@ export namespace Database {
     } catch (err) {
       if (err instanceof Context.NotFound) {
         const effects: (() => void | Promise<void>)[] = []
-        const result = client().transaction((tx) => {
+        const result = Client().transaction((tx) => {
           return ctx.provide({ tx, effects }, () => callback(tx))
         })
         for (const effect of effects) effect()

+ 259 - 218
packages/opencode/src/storage/json-migration.ts

@@ -1,51 +1,70 @@
 import { Database } from "bun:sqlite"
 import { drizzle } from "drizzle-orm/bun-sqlite"
-import { eq } from "drizzle-orm"
 import { Global } from "../global"
 import { Log } from "../util/log"
 import { ProjectTable } from "../project/project.sql"
-import {
-  SessionTable,
-  MessageTable,
-  PartTable,
-  SessionDiffTable,
-  TodoTable,
-  PermissionTable,
-} from "../session/session.sql"
-import { SessionShareTable, ShareTable } from "../share/share.sql"
+import { SessionTable, MessageTable, PartTable, TodoTable, PermissionTable } from "../session/session.sql"
+import { SessionShareTable } from "../share/share.sql"
 import path from "path"
 
-const log = Log.create({ service: "json-migration" })
+export namespace JsonMigration {
+  const log = Log.create({ service: "json-migration" })
 
-export async function migrateFromJson(sqlite: Database, customStorageDir?: string) {
-  const storageDir = customStorageDir ?? path.join(Global.Path.data, "storage")
+  export async function run(sqlite: Database) {
+    const storageDir = path.join(Global.Path.data, "storage")
 
-  log.info("starting json to sqlite migration", { storageDir })
+    log.info("starting json to sqlite migration", { storageDir })
 
-  const db = drizzle({ client: sqlite })
-  const stats = {
-    projects: 0,
-    sessions: 0,
-    messages: 0,
-    parts: 0,
-    diffs: 0,
-    todos: 0,
-    permissions: 0,
-    shares: 0,
-    errors: [] as string[],
-  }
+    const db = drizzle({ client: sqlite })
+    const stats = {
+      projects: 0,
+      sessions: 0,
+      messages: 0,
+      parts: 0,
+      todos: 0,
+      permissions: 0,
+      shares: 0,
+      errors: [] as string[],
+    }
+
+    const limit = 32
+
+    async function list(pattern: string) {
+      const items: string[] = []
+      const scan = new Bun.Glob(pattern)
+      for await (const file of scan.scan({ cwd: storageDir, absolute: true })) {
+        items.push(file)
+      }
+      return items
+    }
 
-  // Migrate projects first (no FK deps)
-  const projectGlob = new Bun.Glob("project/*.json")
-  for await (const file of projectGlob.scan({ cwd: storageDir, absolute: true })) {
-    try {
-      const data = await Bun.file(file).json()
-      if (!data.id) {
-        stats.errors.push(`project missing id: ${file}`)
-        continue
+    async function read(files: string[]) {
+      const results = await Promise.allSettled(files.map((file) => Bun.file(file).json()))
+      const items: { file: string; data: any }[] = []
+      for (let i = 0; i < results.length; i++) {
+        const result = results[i]
+        const file = files[i]
+        if (result.status === "fulfilled") {
+          items.push({ file, data: result.value })
+          continue
+        }
+        stats.errors.push(`failed to read ${file}: ${result.reason}`)
       }
-      db.insert(ProjectTable)
-        .values({
+      return items
+    }
+
+    // Migrate projects first (no FK deps)
+    const projectFiles = await list("project/*.json")
+    for (let i = 0; i < projectFiles.length; i += limit) {
+      const batch = await read(projectFiles.slice(i, i + limit))
+      const values = [] as any[]
+      for (const item of batch) {
+        const data = item.data
+        if (!data?.id) {
+          stats.errors.push(`project missing id: ${item.file}`)
+          continue
+        }
+        values.push({
           id: data.id,
           worktree: data.worktree ?? "/",
           vcs: data.vcs,
@@ -57,32 +76,36 @@ export async function migrateFromJson(sqlite: Database, customStorageDir?: strin
           time_initialized: data.time?.initialized,
           sandboxes: data.sandboxes ?? [],
         })
-        .onConflictDoNothing()
-        .run()
-      stats.projects++
-    } catch (e) {
-      stats.errors.push(`failed to migrate project ${file}: ${e}`)
-    }
-  }
-  log.info("migrated projects", { count: stats.projects })
-
-  // Migrate sessions (depends on projects)
-  const sessionGlob = new Bun.Glob("session/*/*.json")
-  for await (const file of sessionGlob.scan({ cwd: storageDir, absolute: true })) {
-    try {
-      const data = await Bun.file(file).json()
-      if (!data.id || !data.projectID) {
-        stats.errors.push(`session missing id or projectID: ${file}`)
-        continue
       }
-      // Check if project exists (skip orphaned sessions)
-      const project = db.select().from(ProjectTable).where(eq(ProjectTable.id, data.projectID)).get()
-      if (!project) {
-        log.warn("skipping orphaned session", { sessionID: data.id, projectID: data.projectID })
-        continue
+      if (values.length === 0) continue
+      try {
+        db.insert(ProjectTable).values(values).onConflictDoNothing().run()
+        stats.projects += values.length
+      } catch (e) {
+        stats.errors.push(`failed to migrate project batch: ${e}`)
       }
-      db.insert(SessionTable)
-        .values({
+    }
+    log.info("migrated projects", { count: stats.projects })
+
+    const projectRows = db.select({ id: ProjectTable.id }).from(ProjectTable).all()
+    const projectIds = new Set(projectRows.map((item) => item.id))
+
+    // Migrate sessions (depends on projects)
+    const sessionFiles = await list("session/*/*.json")
+    for (let i = 0; i < sessionFiles.length; i += limit) {
+      const batch = await read(sessionFiles.slice(i, i + limit))
+      const values = [] as any[]
+      for (const item of batch) {
+        const data = item.data
+        if (!data?.id || !data?.projectID) {
+          stats.errors.push(`session missing id or projectID: ${item.file}`)
+          continue
+        }
+        if (!projectIds.has(data.projectID)) {
+          log.warn("skipping orphaned session", { sessionID: data.id, projectID: data.projectID })
+          continue
+        }
+        values.push({
           id: data.id,
           project_id: data.projectID,
           parent_id: data.parentID ?? null,
@@ -105,181 +128,199 @@ export async function migrateFromJson(sqlite: Database, customStorageDir?: strin
           time_compacting: data.time?.compacting ?? null,
           time_archived: data.time?.archived ?? null,
         })
-        .onConflictDoNothing()
-        .run()
-      stats.sessions++
-    } catch (e) {
-      stats.errors.push(`failed to migrate session ${file}: ${e}`)
-    }
-  }
-  log.info("migrated sessions", { count: stats.sessions })
-
-  // Migrate messages (depends on sessions)
-  const messageGlob = new Bun.Glob("message/*/*.json")
-  for await (const file of messageGlob.scan({ cwd: storageDir, absolute: true })) {
-    try {
-      const data = await Bun.file(file).json()
-      if (!data.id || !data.sessionID) {
-        stats.errors.push(`message missing id or sessionID: ${file}`)
-        continue
       }
-      // Check if session exists
-      const session = db.select().from(SessionTable).where(eq(SessionTable.id, data.sessionID)).get()
-      if (!session) {
-        log.warn("skipping orphaned message", { messageID: data.id, sessionID: data.sessionID })
-        continue
+      if (values.length === 0) continue
+      try {
+        db.insert(SessionTable).values(values).onConflictDoNothing().run()
+        stats.sessions += values.length
+      } catch (e) {
+        stats.errors.push(`failed to migrate session batch: ${e}`)
       }
-      db.insert(MessageTable)
-        .values({
-          id: data.id,
-          session_id: data.sessionID,
-          created_at: data.time?.created ?? Date.now(),
-          data,
-        })
-        .onConflictDoNothing()
-        .run()
-      stats.messages++
-    } catch (e) {
-      stats.errors.push(`failed to migrate message ${file}: ${e}`)
     }
-  }
-  log.info("migrated messages", { count: stats.messages })
+    log.info("migrated sessions", { count: stats.sessions })
 
-  // Migrate parts (depends on messages)
-  const partGlob = new Bun.Glob("part/*/*.json")
-  for await (const file of partGlob.scan({ cwd: storageDir, absolute: true })) {
-    try {
-      const data = await Bun.file(file).json()
-      if (!data.id || !data.messageID || !data.sessionID) {
-        stats.errors.push(`part missing id, messageID, or sessionID: ${file}`)
-        continue
-      }
-      // Check if message exists
-      const message = db.select().from(MessageTable).where(eq(MessageTable.id, data.messageID)).get()
-      if (!message) {
-        log.warn("skipping orphaned part", { partID: data.id, messageID: data.messageID })
-        continue
-      }
-      db.insert(PartTable)
-        .values({
-          id: data.id,
-          message_id: data.messageID,
-          session_id: data.sessionID,
-          data,
-        })
-        .onConflictDoNothing()
-        .run()
-      stats.parts++
-    } catch (e) {
-      stats.errors.push(`failed to migrate part ${file}: ${e}`)
-    }
-  }
-  log.info("migrated parts", { count: stats.parts })
+    const sessionRows = db.select({ id: SessionTable.id }).from(SessionTable).all()
+    const sessionIds = new Set(sessionRows.map((item) => item.id))
 
-  // Migrate session diffs
-  const diffGlob = new Bun.Glob("session_diff/*.json")
-  for await (const file of diffGlob.scan({ cwd: storageDir, absolute: true })) {
-    try {
-      const data = await Bun.file(file).json()
-      const sessionID = path.basename(file, ".json")
-      // Check if session exists
-      const session = db.select().from(SessionTable).where(eq(SessionTable.id, sessionID)).get()
-      if (!session) {
-        log.warn("skipping orphaned session_diff", { sessionID })
-        continue
-      }
-      db.insert(SessionDiffTable).values({ session_id: sessionID, data }).onConflictDoNothing().run()
-      stats.diffs++
-    } catch (e) {
-      stats.errors.push(`failed to migrate session_diff ${file}: ${e}`)
+    // Migrate messages + parts per session
+    const sessionList = Array.from(sessionIds)
+    for (let i = 0; i < sessionList.length; i += limit) {
+      const batch = sessionList.slice(i, i + limit)
+      await Promise.allSettled(
+        batch.map(async (sessionID) => {
+          const messageFiles = await list(`message/${sessionID}/*.json`)
+          const messageIds = new Set<string>()
+          for (let j = 0; j < messageFiles.length; j += limit) {
+            const chunk = await read(messageFiles.slice(j, j + limit))
+            const values = [] as any[]
+            for (const item of chunk) {
+              const data = item.data
+              if (!data?.id) {
+                stats.errors.push(`message missing id: ${item.file}`)
+                continue
+              }
+              values.push({
+                id: data.id,
+                session_id: sessionID,
+                created_at: data.time?.created ?? Date.now(),
+                data,
+              })
+              messageIds.add(data.id)
+            }
+            if (values.length === 0) continue
+            try {
+              db.insert(MessageTable).values(values).onConflictDoNothing().run()
+              stats.messages += values.length
+            } catch (e) {
+              stats.errors.push(`failed to migrate message batch: ${e}`)
+            }
+          }
+
+          const messageList = Array.from(messageIds)
+          for (let j = 0; j < messageList.length; j += limit) {
+            const messageBatch = messageList.slice(j, j + limit)
+            await Promise.allSettled(
+              messageBatch.map(async (messageID) => {
+                const partFiles = await list(`part/${messageID}/*.json`)
+                for (let k = 0; k < partFiles.length; k += limit) {
+                  const chunk = await read(partFiles.slice(k, k + limit))
+                  const values = [] as any[]
+                  for (const item of chunk) {
+                    const data = item.data
+                    if (!data?.id || !data?.messageID) {
+                      stats.errors.push(`part missing id or messageID: ${item.file}`)
+                      continue
+                    }
+                    values.push({
+                      id: data.id,
+                      message_id: data.messageID,
+                      session_id: sessionID,
+                      data,
+                    })
+                  }
+                  if (values.length === 0) continue
+                  try {
+                    db.insert(PartTable).values(values).onConflictDoNothing().run()
+                    stats.parts += values.length
+                  } catch (e) {
+                    stats.errors.push(`failed to migrate part batch: ${e}`)
+                  }
+                }
+              }),
+            )
+          }
+        }),
+      )
     }
-  }
-  log.info("migrated session diffs", { count: stats.diffs })
+    log.info("migrated messages", { count: stats.messages })
+    log.info("migrated parts", { count: stats.parts })
 
-  // Migrate todos
-  const todoGlob = new Bun.Glob("todo/*.json")
-  for await (const file of todoGlob.scan({ cwd: storageDir, absolute: true })) {
-    try {
-      const data = await Bun.file(file).json()
-      const sessionID = path.basename(file, ".json")
-      const session = db.select().from(SessionTable).where(eq(SessionTable.id, sessionID)).get()
-      if (!session) {
-        log.warn("skipping orphaned todo", { sessionID })
-        continue
+    // Migrate todos
+    const todoFiles = await list("todo/*.json")
+    for (let i = 0; i < todoFiles.length; i += limit) {
+      const batch = await read(todoFiles.slice(i, i + limit))
+      const values = [] as any[]
+      for (const item of batch) {
+        const data = item.data
+        const sessionID = path.basename(item.file, ".json")
+        if (!sessionIds.has(sessionID)) {
+          log.warn("skipping orphaned todo", { sessionID })
+          continue
+        }
+        if (!Array.isArray(data)) {
+          stats.errors.push(`todo not an array: ${item.file}`)
+          continue
+        }
+        for (let position = 0; position < data.length; position++) {
+          const todo = data[position]
+          if (!todo?.id || !todo?.content || !todo?.status || !todo?.priority) continue
+          values.push({
+            session_id: sessionID,
+            id: todo.id,
+            content: todo.content,
+            status: todo.status,
+            priority: todo.priority,
+            position,
+          })
+        }
+      }
+      if (values.length === 0) continue
+      try {
+        db.insert(TodoTable).values(values).onConflictDoNothing().run()
+        stats.todos += values.length
+      } catch (e) {
+        stats.errors.push(`failed to migrate todo batch: ${e}`)
       }
-      db.insert(TodoTable).values({ session_id: sessionID, data }).onConflictDoNothing().run()
-      stats.todos++
-    } catch (e) {
-      stats.errors.push(`failed to migrate todo ${file}: ${e}`)
     }
-  }
-  log.info("migrated todos", { count: stats.todos })
+    log.info("migrated todos", { count: stats.todos })
 
-  // Migrate permissions
-  const permGlob = new Bun.Glob("permission/*.json")
-  for await (const file of permGlob.scan({ cwd: storageDir, absolute: true })) {
-    try {
-      const data = await Bun.file(file).json()
-      const projectID = path.basename(file, ".json")
-      const project = db.select().from(ProjectTable).where(eq(ProjectTable.id, projectID)).get()
-      if (!project) {
-        log.warn("skipping orphaned permission", { projectID })
-        continue
+    // Migrate permissions
+    const permFiles = await list("permission/*.json")
+    for (let i = 0; i < permFiles.length; i += limit) {
+      const batch = await read(permFiles.slice(i, i + limit))
+      const values = [] as any[]
+      for (const item of batch) {
+        const data = item.data
+        const projectID = path.basename(item.file, ".json")
+        if (!projectIds.has(projectID)) {
+          log.warn("skipping orphaned permission", { projectID })
+          continue
+        }
+        values.push({ project_id: projectID, data })
+      }
+      if (values.length === 0) continue
+      try {
+        db.insert(PermissionTable).values(values).onConflictDoNothing().run()
+        stats.permissions += values.length
+      } catch (e) {
+        stats.errors.push(`failed to migrate permission batch: ${e}`)
       }
-      db.insert(PermissionTable).values({ project_id: projectID, data }).onConflictDoNothing().run()
-      stats.permissions++
-    } catch (e) {
-      stats.errors.push(`failed to migrate permission ${file}: ${e}`)
     }
-  }
-  log.info("migrated permissions", { count: stats.permissions })
+    log.info("migrated permissions", { count: stats.permissions })
 
-  // Migrate session shares
-  const shareGlob = new Bun.Glob("session_share/*.json")
-  for await (const file of shareGlob.scan({ cwd: storageDir, absolute: true })) {
-    try {
-      const data = await Bun.file(file).json()
-      const sessionID = path.basename(file, ".json")
-      const session = db.select().from(SessionTable).where(eq(SessionTable.id, sessionID)).get()
-      if (!session) {
-        log.warn("skipping orphaned session_share", { sessionID })
-        continue
+    // Migrate session shares
+    const shareFiles = await list("session_share/*.json")
+    for (let i = 0; i < shareFiles.length; i += limit) {
+      const batch = await read(shareFiles.slice(i, i + limit))
+      const values = [] as any[]
+      for (const item of batch) {
+        const data = item.data
+        const sessionID = path.basename(item.file, ".json")
+        if (!sessionIds.has(sessionID)) {
+          log.warn("skipping orphaned session_share", { sessionID })
+          continue
+        }
+        if (!data?.id || !data?.secret || !data?.url) {
+          stats.errors.push(`session_share missing id/secret/url: ${item.file}`)
+          continue
+        }
+        values.push({ session_id: sessionID, id: data.id, secret: data.secret, url: data.url })
+      }
+      if (values.length === 0) continue
+      try {
+        db.insert(SessionShareTable).values(values).onConflictDoNothing().run()
+        stats.shares += values.length
+      } catch (e) {
+        stats.errors.push(`failed to migrate session_share batch: ${e}`)
       }
-      db.insert(SessionShareTable).values({ session_id: sessionID, data }).onConflictDoNothing().run()
-      stats.shares++
-    } catch (e) {
-      stats.errors.push(`failed to migrate session_share ${file}: ${e}`)
     }
-  }
-  log.info("migrated session shares", { count: stats.shares })
+    log.info("migrated session shares", { count: stats.shares })
 
-  // Migrate shares (downloaded shared sessions, no FK)
-  const share2Glob = new Bun.Glob("share/*.json")
-  for await (const file of share2Glob.scan({ cwd: storageDir, absolute: true })) {
-    try {
-      const data = await Bun.file(file).json()
-      const sessionID = path.basename(file, ".json")
-      db.insert(ShareTable).values({ session_id: sessionID, data }).onConflictDoNothing().run()
-    } catch (e) {
-      stats.errors.push(`failed to migrate share ${file}: ${e}`)
-    }
-  }
+    log.info("json migration complete", {
+      projects: stats.projects,
+      sessions: stats.sessions,
+      messages: stats.messages,
+      parts: stats.parts,
+      todos: stats.todos,
+      permissions: stats.permissions,
+      shares: stats.shares,
+      errorCount: stats.errors.length,
+    })
 
-  log.info("json migration complete", {
-    projects: stats.projects,
-    sessions: stats.sessions,
-    messages: stats.messages,
-    parts: stats.parts,
-    diffs: stats.diffs,
-    todos: stats.todos,
-    permissions: stats.permissions,
-    shares: stats.shares,
-    errorCount: stats.errors.length,
-  })
+    if (stats.errors.length > 0) {
+      log.warn("migration errors", { errors: stats.errors.slice(0, 20) })
+    }
 
-  if (stats.errors.length > 0) {
-    log.warn("migration errors", { errors: stats.errors.slice(0, 20) })
+    return stats
   }
-
-  return stats
 }

+ 227 - 0
packages/opencode/src/storage/storage.ts

@@ -0,0 +1,227 @@
+import { Log } from "../util/log"
+import path from "path"
+import fs from "fs/promises"
+import { Global } from "../global"
+import { Filesystem } from "../util/filesystem"
+import { lazy } from "../util/lazy"
+import { Lock } from "../util/lock"
+import { $ } from "bun"
+import { NamedError } from "@opencode-ai/util/error"
+import z from "zod"
+
+export namespace Storage {
+  const log = Log.create({ service: "storage" })
+
+  type Migration = (dir: string) => Promise<void>
+
+  export const NotFoundError = NamedError.create(
+    "NotFoundError",
+    z.object({
+      message: z.string(),
+    }),
+  )
+
+  const MIGRATIONS: Migration[] = [
+    async (dir) => {
+      const project = path.resolve(dir, "../project")
+      if (!(await Filesystem.isDir(project))) return
+      for await (const projectDir of new Bun.Glob("*").scan({
+        cwd: project,
+        onlyFiles: false,
+      })) {
+        log.info(`migrating project ${projectDir}`)
+        let projectID = projectDir
+        const fullProjectDir = path.join(project, projectDir)
+        let worktree = "/"
+
+        if (projectID !== "global") {
+          for await (const msgFile of new Bun.Glob("storage/session/message/*/*.json").scan({
+            cwd: path.join(project, projectDir),
+            absolute: true,
+          })) {
+            const json = await Bun.file(msgFile).json()
+            worktree = json.path?.root
+            if (worktree) break
+          }
+          if (!worktree) continue
+          if (!(await Filesystem.isDir(worktree))) continue
+          const [id] = await $`git rev-list --max-parents=0 --all`
+            .quiet()
+            .nothrow()
+            .cwd(worktree)
+            .text()
+            .then((x) =>
+              x
+                .split("\n")
+                .filter(Boolean)
+                .map((x) => x.trim())
+                .toSorted(),
+            )
+          if (!id) continue
+          projectID = id
+
+          await Bun.write(
+            path.join(dir, "project", projectID + ".json"),
+            JSON.stringify({
+              id,
+              vcs: "git",
+              worktree,
+              time: {
+                created: Date.now(),
+                initialized: Date.now(),
+              },
+            }),
+          )
+
+          log.info(`migrating sessions for project ${projectID}`)
+          for await (const sessionFile of new Bun.Glob("storage/session/info/*.json").scan({
+            cwd: fullProjectDir,
+            absolute: true,
+          })) {
+            const dest = path.join(dir, "session", projectID, path.basename(sessionFile))
+            log.info("copying", {
+              sessionFile,
+              dest,
+            })
+            const session = await Bun.file(sessionFile).json()
+            await Bun.write(dest, JSON.stringify(session))
+            log.info(`migrating messages for session ${session.id}`)
+            for await (const msgFile of new Bun.Glob(`storage/session/message/${session.id}/*.json`).scan({
+              cwd: fullProjectDir,
+              absolute: true,
+            })) {
+              const dest = path.join(dir, "message", session.id, path.basename(msgFile))
+              log.info("copying", {
+                msgFile,
+                dest,
+              })
+              const message = await Bun.file(msgFile).json()
+              await Bun.write(dest, JSON.stringify(message))
+
+              log.info(`migrating parts for message ${message.id}`)
+              for await (const partFile of new Bun.Glob(`storage/session/part/${session.id}/${message.id}/*.json`).scan(
+                {
+                  cwd: fullProjectDir,
+                  absolute: true,
+                },
+              )) {
+                const dest = path.join(dir, "part", message.id, path.basename(partFile))
+                const part = await Bun.file(partFile).json()
+                log.info("copying", {
+                  partFile,
+                  dest,
+                })
+                await Bun.write(dest, JSON.stringify(part))
+              }
+            }
+          }
+        }
+      }
+    },
+    async (dir) => {
+      for await (const item of new Bun.Glob("session/*/*.json").scan({
+        cwd: dir,
+        absolute: true,
+      })) {
+        const session = await Bun.file(item).json()
+        if (!session.projectID) continue
+        if (!session.summary?.diffs) continue
+        const { diffs } = session.summary
+        await Bun.file(path.join(dir, "session_diff", session.id + ".json")).write(JSON.stringify(diffs))
+        await Bun.file(path.join(dir, "session", session.projectID, session.id + ".json")).write(
+          JSON.stringify({
+            ...session,
+            summary: {
+              additions: diffs.reduce((sum: any, x: any) => sum + x.additions, 0),
+              deletions: diffs.reduce((sum: any, x: any) => sum + x.deletions, 0),
+            },
+          }),
+        )
+      }
+    },
+  ]
+
+  const state = lazy(async () => {
+    const dir = path.join(Global.Path.data, "storage")
+    const migration = await Bun.file(path.join(dir, "migration"))
+      .json()
+      .then((x) => parseInt(x))
+      .catch(() => 0)
+    for (let index = migration; index < MIGRATIONS.length; index++) {
+      log.info("running migration", { index })
+      const migration = MIGRATIONS[index]
+      await migration(dir).catch(() => log.error("failed to run migration", { index }))
+      await Bun.write(path.join(dir, "migration"), (index + 1).toString())
+    }
+    return {
+      dir,
+    }
+  })
+
+  export async function remove(key: string[]) {
+    const dir = await state().then((x) => x.dir)
+    const target = path.join(dir, ...key) + ".json"
+    return withErrorHandling(async () => {
+      await fs.unlink(target).catch(() => {})
+    })
+  }
+
+  export async function read<T>(key: string[]) {
+    const dir = await state().then((x) => x.dir)
+    const target = path.join(dir, ...key) + ".json"
+    return withErrorHandling(async () => {
+      using _ = await Lock.read(target)
+      const result = await Bun.file(target).json()
+      return result as T
+    })
+  }
+
+  export async function update<T>(key: string[], fn: (draft: T) => void) {
+    const dir = await state().then((x) => x.dir)
+    const target = path.join(dir, ...key) + ".json"
+    return withErrorHandling(async () => {
+      using _ = await Lock.write(target)
+      const content = await Bun.file(target).json()
+      fn(content)
+      await Bun.write(target, JSON.stringify(content, null, 2))
+      return content as T
+    })
+  }
+
+  export async function write<T>(key: string[], content: T) {
+    const dir = await state().then((x) => x.dir)
+    const target = path.join(dir, ...key) + ".json"
+    return withErrorHandling(async () => {
+      using _ = await Lock.write(target)
+      await Bun.write(target, JSON.stringify(content, null, 2))
+    })
+  }
+
+  async function withErrorHandling<T>(body: () => Promise<T>) {
+    return body().catch((e) => {
+      if (!(e instanceof Error)) throw e
+      const errnoException = e as NodeJS.ErrnoException
+      if (errnoException.code === "ENOENT") {
+        throw new NotFoundError({ message: `Resource not found: ${errnoException.path}` })
+      }
+      throw e
+    })
+  }
+
+  const glob = new Bun.Glob("**/*")
+  export async function list(prefix: string[]) {
+    const dir = await state().then((x) => x.dir)
+    try {
+      const result = await Array.fromAsync(
+        glob.scan({
+          cwd: path.join(dir, ...prefix),
+          onlyFiles: true,
+        }),
+      ).then((results) => results.map((x) => [...prefix, ...x.slice(0, -5).split(path.sep)]))
+      result.sort()
+      return result
+    } catch {
+      return []
+    }
+  }
+}

+ 23 - 33
packages/opencode/test/storage/json-migration.test.ts

@@ -5,20 +5,12 @@ import { migrate } from "drizzle-orm/bun-sqlite/migrator"
 import { eq } from "drizzle-orm"
 import path from "path"
 import fs from "fs/promises"
-import { readFileSync } from "fs"
-import os from "os"
-import { migrateFromJson } from "../../src/storage/json-migration"
+import { readFileSync, readdirSync } from "fs"
+import { JsonMigration } from "../../src/storage/json-migration"
+import { Global } from "../../src/global"
 import { ProjectTable } from "../../src/project/project.sql"
 import { Project } from "../../src/project/project"
-import {
-  SessionTable,
-  MessageTable,
-  PartTable,
-  SessionDiffTable,
-  TodoTable,
-  PermissionTable,
-} from "../../src/session/session.sql"
-import { SessionShareTable, ShareTable } from "../../src/share/share.sql"
+import { SessionTable, MessageTable, PartTable, TodoTable, PermissionTable } from "../../src/session/session.sql"
 
 // Test fixtures
 const fixtures = {
@@ -56,8 +48,9 @@ const fixtures = {
 }
 
 // Helper to create test storage directory structure
-async function setupStorageDir(baseDir: string) {
-  const storageDir = path.join(baseDir, "storage")
+async function setupStorageDir() {
+  const storageDir = path.join(Global.Path.data, "storage")
+  await fs.rm(storageDir, { recursive: true, force: true })
   await fs.mkdir(path.join(storageDir, "project"), { recursive: true })
   await fs.mkdir(path.join(storageDir, "session", "proj_test123abc"), { recursive: true })
   await fs.mkdir(path.join(storageDir, "message", "ses_test456def"), { recursive: true })
@@ -66,7 +59,6 @@ async function setupStorageDir(baseDir: string) {
   await fs.mkdir(path.join(storageDir, "todo"), { recursive: true })
   await fs.mkdir(path.join(storageDir, "permission"), { recursive: true })
   await fs.mkdir(path.join(storageDir, "session_share"), { recursive: true })
-  await fs.mkdir(path.join(storageDir, "share"), { recursive: true })
   // Create legacy marker to indicate JSON storage exists
   await Bun.write(path.join(storageDir, "migration"), "1")
   return storageDir
@@ -79,33 +71,31 @@ function createTestDb() {
 
   // Apply schema migrations using drizzle migrate
   const dir = path.join(import.meta.dirname, "../../migration")
-  const journal = JSON.parse(readFileSync(path.join(dir, "meta/_journal.json"), "utf-8")) as {
-    entries: { tag: string; when: number }[]
-  }
-  const migrations = journal.entries.map((entry) => ({
-    sql: readFileSync(path.join(dir, `${entry.tag}.sql`), "utf-8"),
-    timestamp: entry.when,
-  }))
+  const entries = readdirSync(dir, { withFileTypes: true })
+  const migrations = entries
+    .filter((entry) => entry.isDirectory())
+    .map((entry) => ({
+      sql: readFileSync(path.join(dir, entry.name, "migration.sql"), "utf-8"),
+      timestamp: Number(entry.name.split("_")[0]),
+    }))
+    .sort((a, b) => a.timestamp - b.timestamp)
   migrate(drizzle({ client: sqlite }), migrations)
 
   return sqlite
 }
 
 describe("JSON to SQLite migration", () => {
-  let tmpDir: string
   let storageDir: string
   let sqlite: Database
 
   beforeEach(async () => {
-    tmpDir = path.join(os.tmpdir(), "opencode-migration-test-" + Math.random().toString(36).slice(2))
-    await fs.mkdir(tmpDir, { recursive: true })
-    storageDir = await setupStorageDir(tmpDir)
+    storageDir = await setupStorageDir()
     sqlite = createTestDb()
   })
 
   afterEach(async () => {
     sqlite.close()
-    await fs.rm(tmpDir, { recursive: true, force: true })
+    await fs.rm(storageDir, { recursive: true, force: true })
   })
 
   test("migrates project", async () => {
@@ -121,7 +111,7 @@ describe("JSON to SQLite migration", () => {
       }),
     )
 
-    const stats = await migrateFromJson(sqlite, storageDir)
+    const stats = await JsonMigration.run(sqlite)
 
     expect(stats?.projects).toBe(1)
 
@@ -161,7 +151,7 @@ describe("JSON to SQLite migration", () => {
       }),
     )
 
-    await migrateFromJson(sqlite, storageDir)
+    await JsonMigration.run(sqlite)
 
     const db = drizzle({ client: sqlite })
     const sessions = db.select().from(SessionTable).all()
@@ -198,7 +188,7 @@ describe("JSON to SQLite migration", () => {
       JSON.stringify({ ...fixtures.part }),
     )
 
-    const stats = await migrateFromJson(sqlite, storageDir)
+    const stats = await JsonMigration.run(sqlite)
 
     expect(stats?.messages).toBe(1)
     expect(stats?.parts).toBe(1)
@@ -227,7 +217,7 @@ describe("JSON to SQLite migration", () => {
       }),
     )
 
-    const stats = await migrateFromJson(sqlite, storageDir)
+    const stats = await JsonMigration.run(sqlite)
 
     expect(stats?.sessions).toBe(0)
   })
@@ -243,8 +233,8 @@ describe("JSON to SQLite migration", () => {
       }),
     )
 
-    await migrateFromJson(sqlite, storageDir)
-    await migrateFromJson(sqlite, storageDir)
+    await JsonMigration.run(sqlite)
+    await JsonMigration.run(sqlite)
 
     const db = drizzle({ client: sqlite })
     const projects = db.select().from(ProjectTable).all()