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.
- package/README.md +111 -0
- package/bin/xedoc.mjs +233 -0
- package/build/client/assets/api.accounts-CHoT6sYP.js +0 -0
- package/build/client/assets/api.accounts._accountId-BixDYyx8.js +0 -0
- package/build/client/assets/api.accounts._accountId.authenticate-CLzgv3py.js +0 -0
- package/build/client/assets/api.accounts._accountId.authenticate.callback-Bqhz8sDl.js +0 -0
- package/build/client/assets/api.accounts._accountId.models-VIVpVZrj.js +0 -0
- package/build/client/assets/api.accounts._accountId.rate-limits-C9iUvNFt.js +0 -0
- package/build/client/assets/api.accounts._accountId.runtime-settings-6XNKTx--.js +0 -0
- package/build/client/assets/api.accounts.export-romutOT6.js +0 -0
- package/build/client/assets/api.accounts.import-BiWq2jPz.js +0 -0
- package/build/client/assets/api.auth.exchange-CkPWDiGV.js +0 -0
- package/build/client/assets/api.auth.session-DOeXMy_2.js +0 -0
- package/build/client/assets/api.auth.status-FGYJfLVM.js +0 -0
- package/build/client/assets/api.chats-C6-gVaXw.js +0 -0
- package/build/client/assets/api.chats._chatId-DaXHa-ln.js +0 -0
- package/build/client/assets/api.chats._chatId.context-DlBgjpCD.js +0 -0
- package/build/client/assets/api.chats._chatId.files-DVZKcoNV.js +0 -0
- package/build/client/assets/api.chats._chatId.git._operation-8Xtyz4hF.js +0 -0
- package/build/client/assets/api.chats._chatId.interrupt-B71lrxkc.js +0 -0
- package/build/client/assets/api.chats._chatId.messages-iVzJcgvw.js +0 -0
- package/build/client/assets/api.chats._chatId.server-requests._requestId.respond-BLhOUK8A.js +0 -0
- package/build/client/assets/api.users-BLvMDkJE.js +0 -0
- package/build/client/assets/api.workspaces.directories-B0tllRDq.js +0 -0
- package/build/client/assets/app-layout-C1QtSnIO.js +1 -0
- package/build/client/assets/app-shell-Bcxy4tS4.js +1 -0
- package/build/client/assets/app-shell-DdKuH37F.css +1 -0
- package/build/client/assets/chat-DrelwLpW.js +1 -0
- package/build/client/assets/connect-0oYrfAJT.js +1 -0
- package/build/client/assets/document-title-Dn4sU16M.js +1 -0
- package/build/client/assets/entry.client-CM3vH2bv.js +1 -0
- package/build/client/assets/favicon.ico-BGeA4faG.js +0 -0
- package/build/client/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/build/client/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/build/client/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/build/client/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/build/client/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/build/client/assets/health-OjIoaxn7.js +0 -0
- package/build/client/assets/home-DSWMwIrE.js +1 -0
- package/build/client/assets/jsx-runtime-B9QlDcuB.js +1 -0
- package/build/client/assets/label-FUU4pCWu.js +1 -0
- package/build/client/assets/manifest-9479dd15.js +1 -0
- package/build/client/assets/react-dom-KYDDPtOx.js +1 -0
- package/build/client/assets/root-Bh_1OXF0.css +2 -0
- package/build/client/assets/root-DKEnPIjJ.js +1 -0
- package/build/client/assets/session-provider-FKGj36EG.js +1 -0
- package/build/client/favicon.svg +1 -0
- package/build/client/icons.svg +24 -0
- package/build/server/index.js +1 -0
- package/package.json +88 -0
- package/prisma/schema.prisma +171 -0
- package/server/index.mjs +261 -0
- 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
|
+
}
|
package/server/index.mjs
ADDED
|
@@ -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
|
+
]
|