xedoc-cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +111 -0
  2. package/bin/xedoc.mjs +233 -0
  3. package/build/client/assets/api.accounts-CHoT6sYP.js +0 -0
  4. package/build/client/assets/api.accounts._accountId-BixDYyx8.js +0 -0
  5. package/build/client/assets/api.accounts._accountId.authenticate-CLzgv3py.js +0 -0
  6. package/build/client/assets/api.accounts._accountId.authenticate.callback-Bqhz8sDl.js +0 -0
  7. package/build/client/assets/api.accounts._accountId.models-VIVpVZrj.js +0 -0
  8. package/build/client/assets/api.accounts._accountId.rate-limits-C9iUvNFt.js +0 -0
  9. package/build/client/assets/api.accounts._accountId.runtime-settings-6XNKTx--.js +0 -0
  10. package/build/client/assets/api.accounts.export-romutOT6.js +0 -0
  11. package/build/client/assets/api.accounts.import-BiWq2jPz.js +0 -0
  12. package/build/client/assets/api.auth.exchange-CkPWDiGV.js +0 -0
  13. package/build/client/assets/api.auth.session-DOeXMy_2.js +0 -0
  14. package/build/client/assets/api.auth.status-FGYJfLVM.js +0 -0
  15. package/build/client/assets/api.chats-C6-gVaXw.js +0 -0
  16. package/build/client/assets/api.chats._chatId-DaXHa-ln.js +0 -0
  17. package/build/client/assets/api.chats._chatId.context-DlBgjpCD.js +0 -0
  18. package/build/client/assets/api.chats._chatId.files-DVZKcoNV.js +0 -0
  19. package/build/client/assets/api.chats._chatId.git._operation-8Xtyz4hF.js +0 -0
  20. package/build/client/assets/api.chats._chatId.interrupt-B71lrxkc.js +0 -0
  21. package/build/client/assets/api.chats._chatId.messages-iVzJcgvw.js +0 -0
  22. package/build/client/assets/api.chats._chatId.server-requests._requestId.respond-BLhOUK8A.js +0 -0
  23. package/build/client/assets/api.users-BLvMDkJE.js +0 -0
  24. package/build/client/assets/api.workspaces.directories-B0tllRDq.js +0 -0
  25. package/build/client/assets/app-layout-C1QtSnIO.js +1 -0
  26. package/build/client/assets/app-shell-Bcxy4tS4.js +1 -0
  27. package/build/client/assets/app-shell-DdKuH37F.css +1 -0
  28. package/build/client/assets/chat-DrelwLpW.js +1 -0
  29. package/build/client/assets/connect-0oYrfAJT.js +1 -0
  30. package/build/client/assets/document-title-Dn4sU16M.js +1 -0
  31. package/build/client/assets/entry.client-CM3vH2bv.js +1 -0
  32. package/build/client/assets/favicon.ico-BGeA4faG.js +0 -0
  33. package/build/client/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
  34. package/build/client/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
  35. package/build/client/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
  36. package/build/client/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  37. package/build/client/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
  38. package/build/client/assets/health-OjIoaxn7.js +0 -0
  39. package/build/client/assets/home-DSWMwIrE.js +1 -0
  40. package/build/client/assets/jsx-runtime-B9QlDcuB.js +1 -0
  41. package/build/client/assets/label-FUU4pCWu.js +1 -0
  42. package/build/client/assets/manifest-9479dd15.js +1 -0
  43. package/build/client/assets/react-dom-KYDDPtOx.js +1 -0
  44. package/build/client/assets/root-Bh_1OXF0.css +2 -0
  45. package/build/client/assets/root-DKEnPIjJ.js +1 -0
  46. package/build/client/assets/session-provider-FKGj36EG.js +1 -0
  47. package/build/client/favicon.svg +1 -0
  48. package/build/client/icons.svg +24 -0
  49. package/build/server/index.js +1 -0
  50. package/package.json +88 -0
  51. package/prisma/schema.prisma +171 -0
  52. package/server/index.mjs +261 -0
  53. package/server/sqlite-setup.mjs +155 -0
package/package.json ADDED
@@ -0,0 +1,88 @@
1
+ {
2
+ "name": "xedoc-cli",
3
+ "version": "0.1.0",
4
+ "description": "Local web UI for Codex account, chat, execution, and workspace management.",
5
+ "author": "Edward Nguyen <monokaijs@gmail.com>",
6
+ "type": "module",
7
+ "packageManager": "pnpm@10.33.0",
8
+ "license": "UNLICENSED",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/monokaijs/xedoc.git"
12
+ },
13
+ "bin": {
14
+ "xedoc": "bin/xedoc.mjs"
15
+ },
16
+ "files": [
17
+ "bin/xedoc.mjs",
18
+ "build/client/",
19
+ "build/server/index.js",
20
+ "prisma/schema.prisma",
21
+ "server/index.mjs",
22
+ "server/sqlite-setup.mjs",
23
+ "README.md"
24
+ ],
25
+ "engines": {
26
+ "node": ">=20"
27
+ },
28
+ "publishConfig": {
29
+ "access": "public"
30
+ },
31
+ "scripts": {
32
+ "dev": "react-router dev --host 0.0.0.0",
33
+ "build": "react-router build --mode production --minify terser",
34
+ "start": "node ./server/index.mjs",
35
+ "typecheck": "tsc --noEmit",
36
+ "clean:build": "node -e \"import('node:fs/promises').then(({ rm }) => rm('build', { recursive: true, force: true }))\"",
37
+ "prepublishOnly": "pnpm typecheck",
38
+ "prepack": "pnpm clean:build && pnpm build",
39
+ "publish": "npm publish --access public",
40
+ "publish:dry-run": "npm publish --access public --dry-run",
41
+ "prisma:generate": "prisma generate --schema prisma/schema.prisma",
42
+ "db:setup": "node -e \"import('./server/sqlite-setup.mjs').then((m)=>m.setupSqliteDatabase())\""
43
+ },
44
+ "dependencies": {
45
+ "@base-ui/react": "^1.4.1",
46
+ "@fontsource-variable/geist": "^5.2.9",
47
+ "@openai/codex": "^0.130.0",
48
+ "@prisma/client": "^6.19.3",
49
+ "@react-router/node": "^7.15.1",
50
+ "@tanstack/react-query": "^5.100.10",
51
+ "class-variance-authority": "^0.7.1",
52
+ "clsx": "^2.1.1",
53
+ "cmdk": "^1.1.1",
54
+ "dotenv": "16.6.1",
55
+ "highlight.js": "^11.11.1",
56
+ "isbot": "^5.1.32",
57
+ "lucide-react": "^1.16.0",
58
+ "next-themes": "^0.4.6",
59
+ "prisma": "^6.19.3",
60
+ "react": "^19.2.6",
61
+ "react-dom": "^19.2.6",
62
+ "react-markdown": "^10.1.0",
63
+ "react-router": "^7.15.1",
64
+ "remark-gfm": "^4.0.1",
65
+ "socket.io": "^4.8.3",
66
+ "socket.io-client": "^4.8.3",
67
+ "sonner": "^2.0.7",
68
+ "tailwind-merge": "^3.6.0",
69
+ "tw-animate-css": "^1.4.0"
70
+ },
71
+ "devDependencies": {
72
+ "@react-router/dev": "^7.15.1",
73
+ "@tailwindcss/vite": "^4.3.0",
74
+ "@types/node": "^24.12.4",
75
+ "@types/react": "^19.2.14",
76
+ "@types/react-dom": "^19.2.3",
77
+ "shadcn": "^4.7.0",
78
+ "tailwindcss": "^4.3.0",
79
+ "terser": "^5.47.1",
80
+ "typescript": "5.9.3",
81
+ "vite": "^8.0.12"
82
+ },
83
+ "pnpm": {
84
+ "overrides": {
85
+ "lightningcss": "1.30.1"
86
+ }
87
+ }
88
+ }
@@ -0,0 +1,171 @@
1
+ generator client {
2
+ provider = "prisma-client-js"
3
+ }
4
+
5
+ datasource db {
6
+ provider = "sqlite"
7
+ url = env("DATABASE_URL")
8
+ }
9
+
10
+ enum AccountStatus {
11
+ DISCONNECTED
12
+ AUTHENTICATING
13
+ CONNECTED
14
+ ERROR
15
+ }
16
+
17
+ enum ChatStatus {
18
+ IDLE
19
+ RUNNING
20
+ ARCHIVED
21
+ }
22
+
23
+ enum MessageRole {
24
+ USER
25
+ ASSISTANT
26
+ SYSTEM
27
+ TOOL
28
+ }
29
+
30
+ enum MessageStatus {
31
+ PENDING
32
+ STREAMING
33
+ COMPLETED
34
+ FAILED
35
+ }
36
+
37
+ enum MessageKind {
38
+ CHAT
39
+ THINKING
40
+ TOOL_ACTIVITY
41
+ COMMAND_EXECUTION
42
+ FILE_CHANGE
43
+ PLAN
44
+ APPROVAL
45
+ USER_INPUT_PROMPT
46
+ ERROR
47
+ }
48
+
49
+ enum RunStatus {
50
+ QUEUED
51
+ RUNNING
52
+ COMPLETED
53
+ FAILED
54
+ CANCELLED
55
+ }
56
+
57
+ model User {
58
+ id String @id @default(uuid())
59
+ externalId String? @unique
60
+ name String?
61
+ createdAt DateTime @default(now())
62
+ updatedAt DateTime @updatedAt
63
+ accounts CodexAccount[]
64
+ chats Chat[]
65
+ }
66
+
67
+ model ServerAuth {
68
+ id String @id @default("server")
69
+ passwordHash String
70
+ tokenSecret String
71
+ createdAt DateTime @default(now())
72
+ updatedAt DateTime @updatedAt
73
+ }
74
+
75
+ model CodexAccount {
76
+ id String @id @default(uuid())
77
+ userId String
78
+ displayName String
79
+ status AccountStatus @default(DISCONNECTED)
80
+ command String @default("codex")
81
+ args Json @default("[\"app-server\"]")
82
+ environment Json?
83
+ defaultModel String?
84
+ defaultPermissionMode String?
85
+ defaultReasoningEffort String?
86
+ defaultServiceTier String?
87
+ lastAuthUrl String?
88
+ lastError String?
89
+ createdAt DateTime @default(now())
90
+ updatedAt DateTime @updatedAt
91
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
92
+ chats Chat[]
93
+ runs ChatRun[]
94
+
95
+ @@index([userId])
96
+ }
97
+
98
+ model Chat {
99
+ id String @id @default(uuid())
100
+ userId String
101
+ accountId String?
102
+ title String
103
+ workingDirectory String?
104
+ model String?
105
+ reasoningEffort String?
106
+ serviceTier String?
107
+ collaborationMode String @default("default")
108
+ permissionMode String @default("default")
109
+ status ChatStatus @default(IDLE)
110
+ externalThreadId String?
111
+ lastActivityAt DateTime @default(now())
112
+ createdAt DateTime @default(now())
113
+ updatedAt DateTime @updatedAt
114
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
115
+ account CodexAccount? @relation(fields: [accountId], references: [id], onDelete: SetNull)
116
+ messages ChatMessage[]
117
+ runs ChatRun[]
118
+
119
+ @@index([userId, updatedAt])
120
+ @@index([userId, lastActivityAt])
121
+ @@index([accountId])
122
+ }
123
+
124
+ model ChatMessage {
125
+ id String @id @default(uuid())
126
+ chatId String
127
+ runId String?
128
+ sequence Int
129
+ role MessageRole
130
+ kind MessageKind @default(CHAT)
131
+ status MessageStatus @default(COMPLETED)
132
+ turnId String?
133
+ itemId String?
134
+ requestId String?
135
+ content String
136
+ metadata Json?
137
+ rawPayload Json?
138
+ createdAt DateTime @default(now())
139
+ completedAt DateTime?
140
+ chat Chat @relation(fields: [chatId], references: [id], onDelete: Cascade)
141
+ run ChatRun? @relation(fields: [runId], references: [id], onDelete: SetNull)
142
+
143
+ @@unique([chatId, sequence])
144
+ @@index([chatId, sequence])
145
+ @@index([chatId, turnId])
146
+ @@index([chatId, itemId])
147
+ @@index([requestId])
148
+ @@index([runId])
149
+ }
150
+
151
+ model ChatRun {
152
+ id String @id @default(uuid())
153
+ chatId String
154
+ accountId String
155
+ status RunStatus @default(QUEUED)
156
+ request Json
157
+ error String?
158
+ externalTurnId String?
159
+ interruptRequestedAt DateTime?
160
+ startedAt DateTime?
161
+ endedAt DateTime?
162
+ createdAt DateTime @default(now())
163
+ updatedAt DateTime @updatedAt
164
+ chat Chat @relation(fields: [chatId], references: [id], onDelete: Cascade)
165
+ account CodexAccount @relation(fields: [accountId], references: [id], onDelete: Restrict)
166
+ messages ChatMessage[]
167
+
168
+ @@index([chatId, createdAt])
169
+ @@index([accountId])
170
+ @@index([externalTurnId])
171
+ }
@@ -0,0 +1,261 @@
1
+ import "dotenv/config"
2
+ import { createHmac, createHash, timingSafeEqual } from "node:crypto"
3
+ import { createReadStream, statSync } from "node:fs"
4
+ import { createServer } from "node:http"
5
+ import { dirname, extname, join, normalize, resolve } from "node:path"
6
+ import { fileURLToPath } from "node:url"
7
+ import { PrismaClient } from "@prisma/client"
8
+ import { createRequestListener } from "@react-router/node"
9
+ import { Server as SocketServer } from "socket.io"
10
+
11
+ process.env.NODE_ENV = process.env.NODE_ENV ?? "production"
12
+
13
+ const DEFAULT_PORT = "6354"
14
+ const SERVER_AUTH_ID = "server"
15
+ const serverRoot = dirname(fileURLToPath(import.meta.url))
16
+ const packageRoot = resolve(serverRoot, "..")
17
+ const prisma = new PrismaClient()
18
+ const options = parseArgs(process.argv.slice(2))
19
+ const build = await import(join(packageRoot, "build/server/index.js"))
20
+ const clientRoot = join(packageRoot, "build/client")
21
+ const requestListener = createRequestListener({
22
+ build,
23
+ mode: process.env.NODE_ENV,
24
+ })
25
+
26
+ const server = createServer((request, response) => {
27
+ if (serveStaticAsset(request, response)) {
28
+ return
29
+ }
30
+ requestListener(request, response)
31
+ })
32
+
33
+ installSocketServer(server)
34
+
35
+ const port = Number.parseInt(options.port ?? process.env.PORT ?? DEFAULT_PORT, 10)
36
+ const host = options.host ?? process.env.HOST ?? "0.0.0.0"
37
+
38
+ server.listen(port, host, () => {
39
+ console.log(`xedoc listening on http://${host}:${port}`)
40
+ })
41
+
42
+ function parseArgs(argv) {
43
+ const parsed = {}
44
+ for (let index = 0; index < argv.length; index += 1) {
45
+ const arg = argv[index]
46
+ if (arg.startsWith("--")) {
47
+ const [name, inlineValue] = arg.split("=", 2)
48
+ const value = inlineValue ?? argv[++index]
49
+ if (!value || value.startsWith("--")) {
50
+ fail(`${name} requires a value.`)
51
+ }
52
+ assignOption(parsed, name, value)
53
+ } else {
54
+ fail(`Unknown argument: ${arg}`)
55
+ }
56
+ }
57
+ return parsed
58
+ }
59
+
60
+ function assignOption(parsed, name, value) {
61
+ switch (name) {
62
+ case "--host":
63
+ parsed.host = value
64
+ return
65
+ case "--port":
66
+ parsed.port = value
67
+ return
68
+ default:
69
+ fail(`Unknown option: ${name}`)
70
+ }
71
+ }
72
+
73
+ function fail(message) {
74
+ console.error(message)
75
+ process.exit(1)
76
+ }
77
+
78
+ function installSocketServer(httpServer) {
79
+ const io = new SocketServer(httpServer, {
80
+ path: "/socket.io",
81
+ serveClient: false,
82
+ })
83
+
84
+ io.use((socket, next) => {
85
+ const token = readSocketToken(socket.handshake.auth)
86
+ if (!token) {
87
+ next(new Error("Missing auth token."))
88
+ return
89
+ }
90
+ void verifyToken(token)
91
+ .then(() => next())
92
+ .catch((error) => {
93
+ next(error instanceof Error ? error : new Error("Invalid auth token."))
94
+ })
95
+ })
96
+
97
+ io.on("connection", (socket) => {
98
+ socket.on("chat:join", (chatId, ack) => {
99
+ if (!isValidChatId(chatId)) {
100
+ ack?.({ ok: false, message: "Invalid chat id." })
101
+ return
102
+ }
103
+ socket.join(chatRoomName(chatId))
104
+ ack?.({ ok: true })
105
+ socket.emit("chat:connected", { chatId })
106
+ })
107
+
108
+ socket.on("chat:leave", (chatId, ack) => {
109
+ if (!isValidChatId(chatId)) {
110
+ ack?.({ ok: false, message: "Invalid chat id." })
111
+ return
112
+ }
113
+ socket.leave(chatRoomName(chatId))
114
+ ack?.({ ok: true })
115
+ })
116
+ })
117
+
118
+ const state = getRealtimeState()
119
+ const handler = (event) => {
120
+ io.to(chatRoomName(event.chatId)).emit("chat:event", event)
121
+ }
122
+ state.handlers.add(handler)
123
+ httpServer.once("close", () => {
124
+ state.handlers.delete(handler)
125
+ io.close()
126
+ })
127
+ }
128
+
129
+ function serveStaticAsset(request, response) {
130
+ if (request.method !== "GET" && request.method !== "HEAD") {
131
+ return false
132
+ }
133
+
134
+ const host = request.headers.host ?? "localhost"
135
+ const url = new URL(request.url ?? "/", `http://${host}`)
136
+ const pathname = decodeURIComponent(url.pathname)
137
+ if (pathname === "/" || pathname.includes("\0")) {
138
+ return false
139
+ }
140
+
141
+ const candidate = resolve(clientRoot, `.${normalize(pathname)}`)
142
+ if (!candidate.startsWith(`${clientRoot}/`) && candidate !== clientRoot) {
143
+ return false
144
+ }
145
+
146
+ let stats
147
+ try {
148
+ stats = statSync(candidate)
149
+ } catch {
150
+ return false
151
+ }
152
+ if (!stats.isFile()) {
153
+ return false
154
+ }
155
+
156
+ response.statusCode = 200
157
+ response.setHeader("Content-Type", contentType(candidate))
158
+ response.setHeader(
159
+ "Cache-Control",
160
+ pathname.startsWith("/assets/")
161
+ ? "public, max-age=31536000, immutable"
162
+ : "no-cache",
163
+ )
164
+ if (request.method === "HEAD") {
165
+ response.end()
166
+ return true
167
+ }
168
+ createReadStream(candidate).pipe(response)
169
+ return true
170
+ }
171
+
172
+ function readSocketToken(auth) {
173
+ if (!auth || typeof auth !== "object") {
174
+ return undefined
175
+ }
176
+ const token = auth.token
177
+ return typeof token === "string" && token.trim() ? token.trim() : undefined
178
+ }
179
+
180
+ async function verifyToken(token) {
181
+ const auth = await requireServerAuth()
182
+ const [encodedPayload, signature, extra] = token.split(".")
183
+ if (!encodedPayload || !signature || extra !== undefined) {
184
+ throw new Error("Invalid auth token.")
185
+ }
186
+ if (!constantTimeEqual(signature, sign(encodedPayload, auth.tokenSecret))) {
187
+ throw new Error("Invalid auth token.")
188
+ }
189
+
190
+ const payload = JSON.parse(
191
+ Buffer.from(encodedPayload, "base64url").toString("utf8"),
192
+ )
193
+ if (payload?.authHash !== hash(auth.passwordHash)) {
194
+ throw new Error("Auth token has been revoked.")
195
+ }
196
+ }
197
+
198
+ function sign(value, tokenSecret) {
199
+ return createHmac("sha256", tokenSecret).update(value).digest("base64url")
200
+ }
201
+
202
+ function hash(value) {
203
+ return createHash("sha256").update(value).digest("hex")
204
+ }
205
+
206
+ async function requireServerAuth() {
207
+ const auth = await prisma.serverAuth.findUnique({
208
+ where: { id: SERVER_AUTH_ID },
209
+ })
210
+ if (!auth) {
211
+ throw new Error("Server password has not been configured.")
212
+ }
213
+ return auth
214
+ }
215
+
216
+ function constantTimeEqual(left, right) {
217
+ const leftBuffer = Buffer.from(left)
218
+ const rightBuffer = Buffer.from(right)
219
+ return (
220
+ leftBuffer.length === rightBuffer.length &&
221
+ timingSafeEqual(leftBuffer, rightBuffer)
222
+ )
223
+ }
224
+
225
+ function getRealtimeState() {
226
+ globalThis.__xedocRealtimeState__ ??= { handlers: new Set() }
227
+ return globalThis.__xedocRealtimeState__
228
+ }
229
+
230
+ function chatRoomName(chatId) {
231
+ return `chat:${chatId}`
232
+ }
233
+
234
+ function isValidChatId(value) {
235
+ return typeof value === "string" && value.trim().length > 0 && value.length <= 128
236
+ }
237
+
238
+ function contentType(pathname) {
239
+ switch (extname(pathname)) {
240
+ case ".css":
241
+ return "text/css; charset=utf-8"
242
+ case ".html":
243
+ return "text/html; charset=utf-8"
244
+ case ".js":
245
+ return "text/javascript; charset=utf-8"
246
+ case ".json":
247
+ return "application/json; charset=utf-8"
248
+ case ".map":
249
+ return "application/json; charset=utf-8"
250
+ case ".png":
251
+ return "image/png"
252
+ case ".svg":
253
+ return "image/svg+xml"
254
+ case ".webp":
255
+ return "image/webp"
256
+ case ".woff2":
257
+ return "font/woff2"
258
+ default:
259
+ return "application/octet-stream"
260
+ }
261
+ }
@@ -0,0 +1,155 @@
1
+ import "dotenv/config"
2
+ import { mkdir } from "node:fs/promises"
3
+ import { dirname, isAbsolute, resolve } from "node:path"
4
+ import { fileURLToPath } from "node:url"
5
+
6
+ const schemaDirectory = dirname(
7
+ fileURLToPath(new URL("../prisma/schema.prisma", import.meta.url)),
8
+ )
9
+
10
+ export async function setupSqliteDatabase() {
11
+ await ensureSqliteDirectory()
12
+ const { PrismaClient } = await import("@prisma/client")
13
+ const prisma = new PrismaClient()
14
+ try {
15
+ await prisma.$executeRawUnsafe("PRAGMA foreign_keys = ON")
16
+ for (const statement of schemaStatements) {
17
+ try {
18
+ await prisma.$executeRawUnsafe(statement)
19
+ } catch (error) {
20
+ if (!isDuplicateColumnError(error)) {
21
+ throw error
22
+ }
23
+ }
24
+ }
25
+ } finally {
26
+ await prisma.$disconnect()
27
+ }
28
+ }
29
+
30
+ function isDuplicateColumnError(error) {
31
+ const message = error instanceof Error ? error.message : String(error)
32
+ return message.toLowerCase().includes("duplicate column name")
33
+ }
34
+
35
+ async function ensureSqliteDirectory() {
36
+ const databaseUrl = process.env.DATABASE_URL
37
+ if (!databaseUrl?.startsWith("file:")) {
38
+ return
39
+ }
40
+ const filePath = databaseUrl.slice("file:".length)
41
+ if (!filePath || filePath === ":memory:" || filePath.includes("?")) {
42
+ return
43
+ }
44
+ const resolvedPath = isAbsolute(filePath)
45
+ ? filePath
46
+ : resolve(schemaDirectory, filePath)
47
+ await mkdir(dirname(resolvedPath), { recursive: true })
48
+ }
49
+
50
+ const schemaStatements = [
51
+ `CREATE TABLE IF NOT EXISTS "User" (
52
+ "id" TEXT NOT NULL PRIMARY KEY,
53
+ "externalId" TEXT,
54
+ "name" TEXT,
55
+ "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
56
+ "updatedAt" DATETIME NOT NULL
57
+ )`,
58
+ `CREATE TABLE IF NOT EXISTS "ServerAuth" (
59
+ "id" TEXT NOT NULL PRIMARY KEY DEFAULT 'server',
60
+ "passwordHash" TEXT NOT NULL,
61
+ "tokenSecret" TEXT NOT NULL,
62
+ "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
63
+ "updatedAt" DATETIME NOT NULL
64
+ )`,
65
+ `CREATE TABLE IF NOT EXISTS "CodexAccount" (
66
+ "id" TEXT NOT NULL PRIMARY KEY,
67
+ "userId" TEXT NOT NULL,
68
+ "displayName" TEXT NOT NULL,
69
+ "status" TEXT NOT NULL DEFAULT 'DISCONNECTED',
70
+ "command" TEXT NOT NULL DEFAULT 'codex',
71
+ "args" JSONB NOT NULL DEFAULT '["app-server"]',
72
+ "environment" JSONB,
73
+ "defaultModel" TEXT,
74
+ "defaultPermissionMode" TEXT,
75
+ "defaultReasoningEffort" TEXT,
76
+ "defaultServiceTier" TEXT,
77
+ "lastAuthUrl" TEXT,
78
+ "lastError" TEXT,
79
+ "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
80
+ "updatedAt" DATETIME NOT NULL,
81
+ CONSTRAINT "CodexAccount_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
82
+ )`,
83
+ `CREATE TABLE IF NOT EXISTS "Chat" (
84
+ "id" TEXT NOT NULL PRIMARY KEY,
85
+ "userId" TEXT NOT NULL,
86
+ "accountId" TEXT,
87
+ "title" TEXT NOT NULL,
88
+ "workingDirectory" TEXT,
89
+ "model" TEXT,
90
+ "reasoningEffort" TEXT,
91
+ "serviceTier" TEXT,
92
+ "collaborationMode" TEXT NOT NULL DEFAULT 'default',
93
+ "permissionMode" TEXT NOT NULL DEFAULT 'default',
94
+ "status" TEXT NOT NULL DEFAULT 'IDLE',
95
+ "externalThreadId" TEXT,
96
+ "lastActivityAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
97
+ "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
98
+ "updatedAt" DATETIME NOT NULL,
99
+ CONSTRAINT "Chat_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
100
+ CONSTRAINT "Chat_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES "CodexAccount" ("id") ON DELETE SET NULL ON UPDATE CASCADE
101
+ )`,
102
+ `CREATE TABLE IF NOT EXISTS "ChatMessage" (
103
+ "id" TEXT NOT NULL PRIMARY KEY,
104
+ "chatId" TEXT NOT NULL,
105
+ "runId" TEXT,
106
+ "sequence" INTEGER NOT NULL,
107
+ "role" TEXT NOT NULL,
108
+ "kind" TEXT NOT NULL DEFAULT 'CHAT',
109
+ "status" TEXT NOT NULL DEFAULT 'COMPLETED',
110
+ "turnId" TEXT,
111
+ "itemId" TEXT,
112
+ "requestId" TEXT,
113
+ "content" TEXT NOT NULL,
114
+ "metadata" JSONB,
115
+ "rawPayload" JSONB,
116
+ "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
117
+ "completedAt" DATETIME,
118
+ CONSTRAINT "ChatMessage_chatId_fkey" FOREIGN KEY ("chatId") REFERENCES "Chat" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
119
+ CONSTRAINT "ChatMessage_runId_fkey" FOREIGN KEY ("runId") REFERENCES "ChatRun" ("id") ON DELETE SET NULL ON UPDATE CASCADE
120
+ )`,
121
+ `CREATE TABLE IF NOT EXISTS "ChatRun" (
122
+ "id" TEXT NOT NULL PRIMARY KEY,
123
+ "chatId" TEXT NOT NULL,
124
+ "accountId" TEXT NOT NULL,
125
+ "status" TEXT NOT NULL DEFAULT 'QUEUED',
126
+ "request" JSONB NOT NULL,
127
+ "error" TEXT,
128
+ "externalTurnId" TEXT,
129
+ "interruptRequestedAt" DATETIME,
130
+ "startedAt" DATETIME,
131
+ "endedAt" DATETIME,
132
+ "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
133
+ "updatedAt" DATETIME NOT NULL,
134
+ CONSTRAINT "ChatRun_chatId_fkey" FOREIGN KEY ("chatId") REFERENCES "Chat" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
135
+ CONSTRAINT "ChatRun_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES "CodexAccount" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
136
+ )`,
137
+ 'CREATE UNIQUE INDEX IF NOT EXISTS "User_externalId_key" ON "User"("externalId")',
138
+ 'ALTER TABLE "CodexAccount" ADD COLUMN "defaultModel" TEXT',
139
+ 'ALTER TABLE "CodexAccount" ADD COLUMN "defaultPermissionMode" TEXT',
140
+ 'ALTER TABLE "CodexAccount" ADD COLUMN "defaultReasoningEffort" TEXT',
141
+ 'ALTER TABLE "CodexAccount" ADD COLUMN "defaultServiceTier" TEXT',
142
+ 'CREATE INDEX IF NOT EXISTS "CodexAccount_userId_idx" ON "CodexAccount"("userId")',
143
+ 'CREATE INDEX IF NOT EXISTS "Chat_userId_updatedAt_idx" ON "Chat"("userId", "updatedAt")',
144
+ 'CREATE INDEX IF NOT EXISTS "Chat_userId_lastActivityAt_idx" ON "Chat"("userId", "lastActivityAt")',
145
+ 'CREATE INDEX IF NOT EXISTS "Chat_accountId_idx" ON "Chat"("accountId")',
146
+ 'CREATE INDEX IF NOT EXISTS "ChatMessage_chatId_sequence_idx" ON "ChatMessage"("chatId", "sequence")',
147
+ 'CREATE INDEX IF NOT EXISTS "ChatMessage_chatId_turnId_idx" ON "ChatMessage"("chatId", "turnId")',
148
+ 'CREATE INDEX IF NOT EXISTS "ChatMessage_chatId_itemId_idx" ON "ChatMessage"("chatId", "itemId")',
149
+ 'CREATE INDEX IF NOT EXISTS "ChatMessage_requestId_idx" ON "ChatMessage"("requestId")',
150
+ 'CREATE INDEX IF NOT EXISTS "ChatMessage_runId_idx" ON "ChatMessage"("runId")',
151
+ 'CREATE UNIQUE INDEX IF NOT EXISTS "ChatMessage_chatId_sequence_key" ON "ChatMessage"("chatId", "sequence")',
152
+ 'CREATE INDEX IF NOT EXISTS "ChatRun_chatId_createdAt_idx" ON "ChatRun"("chatId", "createdAt")',
153
+ 'CREATE INDEX IF NOT EXISTS "ChatRun_accountId_idx" ON "ChatRun"("accountId")',
154
+ 'CREATE INDEX IF NOT EXISTS "ChatRun_externalTurnId_idx" ON "ChatRun"("externalTurnId")',
155
+ ]