remcodex 0.1.0-beta.10 β 0.1.0-beta.11
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 +215 -209
- package/dist/server/src/app.js +4 -29
- package/dist/server/src/cli.js +63 -6
- package/dist/server/src/db/migrations.js +30 -1
- package/dist/server/src/db/schema.sql +48 -0
- package/dist/server/src/services/session-manager.js +11 -11
- package/dist/server/src/utils/runtime-paths.js +31 -0
- package/docs/assets/approval-flow.png +0 -0
- package/docs/assets/hero-desktop.png +0 -0
- package/docs/assets/imported-session.png +0 -0
- package/docs/assets/mobile-session.png +0 -0
- package/package.json +13 -5
- package/scripts/check-node-version.js +14 -0
- package/LICENSE +0 -21
package/README.md
CHANGED
|
@@ -1,104 +1,45 @@
|
|
|
1
|
-
#
|
|
1
|
+
# RemCodex
|
|
2
2
|
|
|
3
|
-
>
|
|
4
|
-
> From your browser and phone.
|
|
3
|
+
> Control Codex from anywhere. Even on your phone.
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
Monitor, approve, and control the same session from another.
|
|
5
|
+
RemCodex is a local-first web UI for running, reviewing, approving, and resuming Codex sessions from your browser.
|
|
8
6
|
|
|
9
|
-
|
|
7
|
+
It is built for the real workflow: long-running sessions, mobile check-ins, approval prompts, imported rollout history, and timeline-style execution flow.
|
|
10
8
|
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
- Watch live Codex runs without staying in the terminal
|
|
10
|
+
- Approve sensitive actions from a cleaner UI
|
|
11
|
+
- Pick up the same session again after refresh, sleep, or reconnect
|
|
13
12
|
|
|
14
|
-
|
|
13
|
+
## Status
|
|
15
14
|
|
|
16
|
-
|
|
15
|
+
This project is currently a **beta / developer preview**.
|
|
17
16
|
|
|
18
|
-
|
|
17
|
+
It is already usable for local and internal workflows, but it is not yet packaged as a one-click desktop app.
|
|
19
18
|
|
|
20
|
-
|
|
19
|
+
## Why People Use It
|
|
21
20
|
|
|
22
|
-
|
|
23
|
-
interruptible, and controllable from another.
|
|
21
|
+
Codex is powerful in the terminal, but many real workflows need a better control surface:
|
|
24
22
|
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
-
|
|
29
|
-
-
|
|
23
|
+
- checking progress from your phone
|
|
24
|
+
- watching a long run without babysitting a terminal window
|
|
25
|
+
- approving writes from a cleaner interface
|
|
26
|
+
- reopening a session after refresh, sleep, or reconnect
|
|
27
|
+
- reviewing imported rollout history next to native sessions
|
|
30
28
|
|
|
31
|
-
|
|
29
|
+
RemCodex turns Codex's event stream into a browser-based workspace that is easier to follow, easier to resume, and easier to operate.
|
|
32
30
|
|
|
33
|
-
|
|
31
|
+
## What It Does
|
|
34
32
|
|
|
35
|
-
|
|
33
|
+
- Run Codex sessions from a browser
|
|
34
|
+
- View sessions in a single-page workspace with a sidebar and execution timeline
|
|
35
|
+
- Follow streaming assistant output, commands, patches, and approvals
|
|
36
|
+
- Approve or reject file-system actions from the UI
|
|
37
|
+
- Import existing Codex rollout sessions from `~/.codex/sessions/...`
|
|
38
|
+
- Keep imported sessions in sync while they are still active
|
|
39
|
+
- Resume stale sessions after the page comes back from background
|
|
40
|
+
- Work well on desktop and on mobile
|
|
36
41
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
Then you leave your desk.
|
|
40
|
-
|
|
41
|
-
On your phone:
|
|
42
|
-
|
|
43
|
-
- you see progress in real time
|
|
44
|
-
- you receive an approval request
|
|
45
|
-
- you approve it
|
|
46
|
-
|
|
47
|
-
The session continues instantly.
|
|
48
|
-
|
|
49
|
-
> Everything else can disconnect β your session wonβt.
|
|
50
|
-
|
|
51
|
-
---
|
|
52
|
-
|
|
53
|
-
## π₯ Why RemCodex exists
|
|
54
|
-
|
|
55
|
-
AI coding agents are powerful.
|
|
56
|
-
But today, they run like a black box.
|
|
57
|
-
|
|
58
|
-
You either:
|
|
59
|
-
|
|
60
|
-
- trust everything blindly
|
|
61
|
-
- or sit in front of your terminal watching it
|
|
62
|
-
|
|
63
|
-
RemCodex fixes that.
|
|
64
|
-
|
|
65
|
-
> AI is no longer a black box.
|
|
66
|
-
|
|
67
|
-
---
|
|
68
|
-
|
|
69
|
-
## β‘ What it does
|
|
70
|
-
|
|
71
|
-
- Real-time execution timeline (messages, commands, approvals)
|
|
72
|
-
- Human-in-the-loop command approval
|
|
73
|
-
- Multi-device access to the same live session
|
|
74
|
-
- Resume after refresh, sleep, or reconnect
|
|
75
|
-
- Browser-based UI β **no extra client required**
|
|
76
|
-
- Works with Codex CLI
|
|
77
|
-
|
|
78
|
-
> No extra client install. Just open a browser.
|
|
79
|
-
> Your code never leaves your machine.
|
|
80
|
-
|
|
81
|
-
---
|
|
82
|
-
|
|
83
|
-
## π Quick start
|
|
84
|
-
|
|
85
|
-
```bash
|
|
86
|
-
npx remcodex
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
Then open:
|
|
90
|
-
|
|
91
|
-
http://127.0.0.1:18840
|
|
92
|
-
|
|
93
|
-
Access from another device:
|
|
94
|
-
|
|
95
|
-
http://<your-ip>:18840
|
|
96
|
-
|
|
97
|
-
> Runs entirely on your local machine. No cloud, no data upload.
|
|
98
|
-
|
|
99
|
-
---
|
|
100
|
-
|
|
101
|
-
## π₯ Screenshots
|
|
42
|
+
## Screenshots
|
|
102
43
|
|
|
103
44
|

|
|
104
45
|
|
|
@@ -114,203 +55,268 @@ http://<your-ip>:18840
|
|
|
114
55
|
|
|
115
56
|

|
|
116
57
|
|
|
117
|
-
> Bring imported Codex rollouts into the same workspace.
|
|
58
|
+
> Bring imported Codex rollouts into the same workspace and keep them easy to review.
|
|
118
59
|
|
|
119
|
-
|
|
60
|
+
## Who It Is For
|
|
120
61
|
|
|
121
|
-
|
|
62
|
+
- developers who already use Codex locally
|
|
63
|
+
- people who want a browser-based control surface instead of raw terminal watching
|
|
64
|
+
- teams who want to review or monitor runs from another device on the same network
|
|
65
|
+
- anyone who wants approvals, timeline view, and imported rollout history in one place
|
|
122
66
|
|
|
123
|
-
|
|
67
|
+
## Screens It Aims To Replace
|
|
124
68
|
|
|
125
|
-
|
|
69
|
+
- terminal-only session watching
|
|
70
|
+
- ad-hoc mobile remote desktop checks
|
|
71
|
+
- raw log scrolling for approvals and command progress
|
|
72
|
+
- fragmented session history between local and imported rollouts
|
|
126
73
|
|
|
127
|
-
|
|
128
|
-
- mobile check-ins
|
|
129
|
-
- approval prompts
|
|
130
|
-
- imported rollout history
|
|
131
|
-
- timeline-style execution flow
|
|
132
|
-
|
|
133
|
-
Instead of raw terminal logs, you get a structured, visual timeline you can follow and control.
|
|
134
|
-
|
|
135
|
-
---
|
|
136
|
-
|
|
137
|
-
## π§© Current product shape
|
|
74
|
+
## Current Product Shape
|
|
138
75
|
|
|
139
76
|
- Single-page workspace UI
|
|
140
77
|
- Left sidebar for session navigation
|
|
141
|
-
- Right-side execution
|
|
142
|
-
- Fixed
|
|
78
|
+
- Right-side timeline / execution flow for the active session
|
|
79
|
+
- Fixed composer at the bottom
|
|
143
80
|
- Semantic timeline rendering for:
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
---
|
|
81
|
+
- user messages
|
|
82
|
+
- assistant commentary / final messages
|
|
83
|
+
- thinking
|
|
84
|
+
- commands
|
|
85
|
+
- patches
|
|
86
|
+
- approvals
|
|
87
|
+
- system events
|
|
153
88
|
|
|
154
|
-
##
|
|
89
|
+
## Tech Stack
|
|
155
90
|
|
|
156
|
-
|
|
91
|
+
- Backend: Node.js + TypeScript + Express + WebSocket
|
|
92
|
+
- Database: SQLite via `better-sqlite3`
|
|
93
|
+
- Terminal/runtime integration: `node-pty` + Codex app-server
|
|
94
|
+
- Frontend: zero-build static web app (`web/`)
|
|
95
|
+
- Markdown rendering: `markdown-it`
|
|
157
96
|
|
|
158
|
-
|
|
159
|
-
- Writes outside β require approval
|
|
160
|
-
- `Allow once` / `Allow for this turn` supported
|
|
161
|
-
- Approval history stays visible in timeline
|
|
97
|
+
## Requirements
|
|
162
98
|
|
|
163
|
-
|
|
99
|
+
Before running this project, you should have:
|
|
164
100
|
|
|
165
|
-
|
|
101
|
+
- Node.js 20.x installed
|
|
102
|
+
- Codex CLI installed and already working locally
|
|
103
|
+
- A machine where this app can access your local Codex data and working directories
|
|
166
104
|
|
|
167
|
-
|
|
168
|
-
- Commands grouped into readable activity blocks
|
|
169
|
-
- Running / failed states clearly visible
|
|
170
|
-
- Smooth streaming + recovery after refresh
|
|
105
|
+
This project is currently developed primarily around a local macOS workflow.
|
|
171
106
|
|
|
172
|
-
|
|
107
|
+
## Quick Start
|
|
173
108
|
|
|
174
|
-
|
|
109
|
+
For the current developer preview, the recommended local install path is:
|
|
175
110
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
111
|
+
```bash
|
|
112
|
+
npm install
|
|
113
|
+
npm run build
|
|
114
|
+
npm link
|
|
115
|
+
remcodex start
|
|
116
|
+
```
|
|
179
117
|
|
|
180
|
-
|
|
118
|
+
If you switch Node.js versions later, reinstall dependencies and relink or reinstall `remcodex`.
|
|
181
119
|
|
|
182
|
-
|
|
120
|
+
Then open:
|
|
183
121
|
|
|
184
|
-
```
|
|
185
|
-
|
|
122
|
+
```text
|
|
123
|
+
http://127.0.0.1:3000
|
|
186
124
|
```
|
|
187
125
|
|
|
188
|
-
|
|
126
|
+
If you want to make it reachable from your phone, expose the local machine on your LAN and open the same URL with your host IP.
|
|
189
127
|
|
|
190
|
-
##
|
|
128
|
+
## Local CLI
|
|
191
129
|
|
|
192
|
-
|
|
193
|
-
- Codex CLI (already working locally)
|
|
130
|
+
RemCodex already ships with a local CLI entrypoint, even though the npm package is not published yet.
|
|
194
131
|
|
|
195
|
-
|
|
132
|
+
If you do not want to run `npm link`, you can call the built CLI directly:
|
|
196
133
|
|
|
197
|
-
|
|
134
|
+
```bash
|
|
135
|
+
node dist/server/src/cli.js start --no-open
|
|
136
|
+
```
|
|
198
137
|
|
|
199
|
-
|
|
138
|
+
Useful commands:
|
|
200
139
|
|
|
201
140
|
```bash
|
|
202
|
-
|
|
141
|
+
node dist/server/src/cli.js doctor
|
|
142
|
+
node dist/server/src/cli.js start --no-open
|
|
143
|
+
node dist/server/src/cli.js version
|
|
203
144
|
```
|
|
204
145
|
|
|
205
|
-
|
|
146
|
+
Published package note:
|
|
206
147
|
|
|
207
|
-
|
|
148
|
+
- the npm package currently expects Node.js 20.x
|
|
149
|
+
- if `better-sqlite3` or `node-pty` reports a native module / `NODE_MODULE_VERSION` error, reinstall `remcodex` under the same Node.js version you will use to run it
|
|
208
150
|
|
|
209
|
-
|
|
151
|
+
Use a specific database:
|
|
210
152
|
|
|
211
|
-
|
|
153
|
+
```bash
|
|
154
|
+
node dist/server/src/cli.js start --db ~/.remcodex/remcodex-demo.db --no-open
|
|
155
|
+
node dist/server/src/cli.js doctor --db ~/.remcodex/remcodex-demo.db
|
|
156
|
+
```
|
|
212
157
|
|
|
213
|
-
|
|
214
|
-
- `node-pty`
|
|
158
|
+
Planned install target after the npm package is published:
|
|
215
159
|
|
|
216
|
-
|
|
160
|
+
```bash
|
|
161
|
+
npx remcodex
|
|
162
|
+
```
|
|
217
163
|
|
|
218
|
-
|
|
219
|
-
- `make`
|
|
220
|
-
- `g++`
|
|
164
|
+
## Development
|
|
221
165
|
|
|
222
|
-
|
|
166
|
+
```bash
|
|
167
|
+
npm install
|
|
168
|
+
npm run dev
|
|
169
|
+
```
|
|
223
170
|
|
|
224
|
-
|
|
171
|
+
Smoke-test the published-package shape locally:
|
|
225
172
|
|
|
226
173
|
```bash
|
|
227
|
-
npm
|
|
228
|
-
remcodex doctor
|
|
229
|
-
remcodex start
|
|
174
|
+
npm run smoke:tarball
|
|
230
175
|
```
|
|
231
176
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
### Headless mode
|
|
177
|
+
Smoke-test a real isolated startup and `/health` check:
|
|
235
178
|
|
|
236
179
|
```bash
|
|
237
|
-
|
|
180
|
+
npm run smoke:start
|
|
238
181
|
```
|
|
239
182
|
|
|
240
|
-
|
|
183
|
+
## How It Works
|
|
184
|
+
|
|
185
|
+
The app uses `codex app-server` as the primary runtime path.
|
|
241
186
|
|
|
242
|
-
|
|
187
|
+
At a high level:
|
|
243
188
|
|
|
244
|
-
1. Codex emits events
|
|
245
|
-
2.
|
|
246
|
-
3.
|
|
247
|
-
4. Live updates
|
|
189
|
+
1. Codex emits semantic events
|
|
190
|
+
2. The backend stores them in SQLite
|
|
191
|
+
3. The frontend reads an aggregated timeline view for initial load
|
|
192
|
+
4. Live updates continue over WebSocket with catch-up after refresh
|
|
248
193
|
|
|
249
|
-
|
|
194
|
+
This gives the UI:
|
|
250
195
|
|
|
196
|
+
- smooth streaming
|
|
251
197
|
- recoverable sessions
|
|
252
|
-
-
|
|
253
|
-
- consistent execution
|
|
198
|
+
- imported rollout support
|
|
199
|
+
- a consistent execution timeline instead of raw terminal logs
|
|
254
200
|
|
|
255
|
-
|
|
201
|
+
## Key Behaviors
|
|
256
202
|
|
|
257
|
-
|
|
203
|
+
### Approvals
|
|
258
204
|
|
|
259
|
-
-
|
|
260
|
-
-
|
|
261
|
-
-
|
|
205
|
+
- Writes inside the working area usually pass directly
|
|
206
|
+
- Writes outside the working area trigger approval
|
|
207
|
+
- `Allow once` approves only the current request
|
|
208
|
+
- `Allow for this turn` expands writable roots for the active turn
|
|
209
|
+
- Historical approval records stay visible in the timeline
|
|
210
|
+
- Only live approvals stay actionable in the bottom approval bar
|
|
262
211
|
|
|
263
|
-
|
|
212
|
+
### Imported Codex Sessions
|
|
264
213
|
|
|
265
|
-
|
|
214
|
+
- Existing Codex rollouts can be imported from local session history
|
|
215
|
+
- Imported sessions keep their own source metadata
|
|
216
|
+
- Imported sessions can continue syncing after you open them
|
|
217
|
+
- Native sessions are excluded from the import picker
|
|
266
218
|
|
|
267
|
-
|
|
219
|
+
### Timeline and Execution Flow
|
|
268
220
|
|
|
269
|
-
-
|
|
270
|
-
-
|
|
221
|
+
- The UI renders semantic timeline items, not raw logs
|
|
222
|
+
- Commands and patches can be grouped into lighter activity summaries
|
|
223
|
+
- Running and failed commands remain visually important
|
|
224
|
+
- The final thinking placeholder appears only at the end of the active flow
|
|
271
225
|
|
|
272
|
-
|
|
226
|
+
## Configuration
|
|
273
227
|
|
|
274
|
-
|
|
275
|
-
- safer execution
|
|
228
|
+
Supported environment variables:
|
|
276
229
|
|
|
277
|
-
|
|
230
|
+
- `PORT`
|
|
231
|
+
- `DATABASE_PATH`
|
|
232
|
+
- `PROJECT_ROOTS`
|
|
233
|
+
- `CODEX_COMMAND`
|
|
234
|
+
- `CODEX_MODE`
|
|
278
235
|
|
|
279
|
-
|
|
280
|
-
- stable long runs
|
|
236
|
+
For launch screenshots or demo data, you can rebuild a clean demo database with:
|
|
281
237
|
|
|
282
|
-
|
|
238
|
+
```bash
|
|
239
|
+
DATABASE_PATH="$HOME/.remcodex/remcodex-demo.db" ~/.nvm/versions/node/v20.19.5/bin/node scripts/seed-launch-demo-data.js --clean
|
|
240
|
+
```
|
|
241
|
+
- `REMOTE_HOSTS`
|
|
242
|
+
- `ACTIVE_REMOTE_HOST`
|
|
243
|
+
|
|
244
|
+
Notes:
|
|
245
|
+
|
|
246
|
+
- The default runtime mode is `app-server`
|
|
247
|
+
- `exec-json` is kept only as a fallback compatibility path
|
|
248
|
+
- If `PROJECT_ROOTS` is not set, the app falls back to a broad local browsing root
|
|
249
|
+
|
|
250
|
+
## Project Structure
|
|
251
|
+
|
|
252
|
+
```text
|
|
253
|
+
server/
|
|
254
|
+
src/
|
|
255
|
+
app.ts
|
|
256
|
+
controllers/
|
|
257
|
+
db/
|
|
258
|
+
gateways/
|
|
259
|
+
services/
|
|
260
|
+
types/
|
|
261
|
+
utils/
|
|
262
|
+
web/
|
|
263
|
+
index.html
|
|
264
|
+
styles.css
|
|
265
|
+
api.js
|
|
266
|
+
session-ws.js
|
|
267
|
+
app.js
|
|
268
|
+
scripts/
|
|
269
|
+
fix-node-pty-helper.js
|
|
270
|
+
import-codex-rollout.js
|
|
271
|
+
reset-semantic-demo-data.js
|
|
272
|
+
```
|
|
283
273
|
|
|
284
|
-
|
|
274
|
+
## Main Endpoints
|
|
285
275
|
|
|
286
|
-
|
|
276
|
+
- `GET /health`
|
|
277
|
+
- `GET /api/codex/mode`
|
|
278
|
+
- `GET /api/codex/status`
|
|
279
|
+
- `GET /api/codex/quota`
|
|
280
|
+
- `GET /api/sessions`
|
|
281
|
+
- `GET /api/sessions/:sessionId`
|
|
282
|
+
- `GET /api/sessions/:sessionId/timeline`
|
|
283
|
+
- `GET /api/sessions/:sessionId/events`
|
|
284
|
+
- `POST /api/sessions`
|
|
285
|
+
- `POST /api/sessions/:sessionId/messages`
|
|
286
|
+
- `POST /api/sessions/:sessionId/stop`
|
|
287
|
+
- `POST /api/sessions/:sessionId/approvals/:requestId`
|
|
288
|
+
- `WS /ws/sessions/:sessionId`
|
|
287
289
|
|
|
288
|
-
|
|
289
|
-
- optional sharing
|
|
290
|
+
## What Is Not Finished Yet
|
|
290
291
|
|
|
291
|
-
|
|
292
|
+
This is the honest list:
|
|
292
293
|
|
|
293
|
-
|
|
294
|
+
- no polished installer yet
|
|
295
|
+
- no desktop packaging yet
|
|
296
|
+
- no full automated test suite yet
|
|
297
|
+
- no production-grade auth / multi-user hardening yet
|
|
298
|
+
- no release pipeline yet
|
|
294
299
|
|
|
295
|
-
|
|
296
|
-
- people tired of terminal-only workflows
|
|
297
|
-
- anyone who wants **control, not just output**
|
|
298
|
-
- multi-device workflows
|
|
300
|
+
If you are comfortable cloning a repo and running a local Node app, you can use it today.
|
|
299
301
|
|
|
300
|
-
|
|
302
|
+
## Roadmap
|
|
301
303
|
|
|
302
|
-
|
|
304
|
+
Near-term:
|
|
303
305
|
|
|
304
|
-
-
|
|
305
|
-
-
|
|
306
|
-
-
|
|
307
|
-
-
|
|
306
|
+
- improve onboarding and installation
|
|
307
|
+
- ship a cleaner public README and screenshots
|
|
308
|
+
- add stronger regression coverage
|
|
309
|
+
- harden long-running session recovery
|
|
310
|
+
- continue refining the execution timeline UI
|
|
311
|
+
|
|
312
|
+
Later:
|
|
308
313
|
|
|
309
|
-
|
|
310
|
-
|
|
314
|
+
- package for easier local install
|
|
315
|
+
- optional sync / multi-device helpers
|
|
316
|
+
- stronger sharing, auditing, and team workflows
|
|
311
317
|
|
|
312
|
-
|
|
318
|
+
## License
|
|
313
319
|
|
|
314
|
-
|
|
320
|
+
No license has been added yet.
|
|
315
321
|
|
|
316
|
-
|
|
322
|
+
Until a license is added, assume this repository is **source-available for review only**, not open source for reuse.
|
package/dist/server/src/app.js
CHANGED
|
@@ -3,13 +3,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.DEFAULT_PORT = void 0;
|
|
7
|
-
exports.resolvePackageRoot = resolvePackageRoot;
|
|
8
|
-
exports.resolveDefaultDatabasePath = resolveDefaultDatabasePath;
|
|
9
6
|
exports.startRemCodexServer = startRemCodexServer;
|
|
10
7
|
const node_fs_1 = require("node:fs");
|
|
11
8
|
const node_http_1 = __importDefault(require("node:http"));
|
|
12
|
-
const node_os_1 = require("node:os");
|
|
13
9
|
const node_path_1 = __importDefault(require("node:path"));
|
|
14
10
|
const express_1 = __importDefault(require("express"));
|
|
15
11
|
const codex_options_controller_1 = require("./controllers/codex-options.controller");
|
|
@@ -24,36 +20,15 @@ const codex_rollout_sync_1 = require("./services/codex-rollout-sync");
|
|
|
24
20
|
const project_manager_1 = require("./services/project-manager");
|
|
25
21
|
const session_manager_1 = require("./services/session-manager");
|
|
26
22
|
const session_timeline_service_1 = require("./services/session-timeline-service");
|
|
23
|
+
const runtime_paths_1 = require("./utils/runtime-paths");
|
|
27
24
|
const command_1 = require("./utils/command");
|
|
28
25
|
const errors_1 = require("./utils/errors");
|
|
29
|
-
function isPackageRoot(root) {
|
|
30
|
-
return ((0, node_fs_1.existsSync)(node_path_1.default.join(root, "package.json")) &&
|
|
31
|
-
(0, node_fs_1.existsSync)(node_path_1.default.join(root, "web", "index.html")));
|
|
32
|
-
}
|
|
33
|
-
function resolvePackageRoot(startDir = __dirname) {
|
|
34
|
-
let current = node_path_1.default.resolve(startDir);
|
|
35
|
-
while (true) {
|
|
36
|
-
if (isPackageRoot(current)) {
|
|
37
|
-
return current;
|
|
38
|
-
}
|
|
39
|
-
const parent = node_path_1.default.dirname(current);
|
|
40
|
-
if (parent === current) {
|
|
41
|
-
break;
|
|
42
|
-
}
|
|
43
|
-
current = parent;
|
|
44
|
-
}
|
|
45
|
-
return process.cwd();
|
|
46
|
-
}
|
|
47
|
-
function resolveDefaultDatabasePath() {
|
|
48
|
-
return node_path_1.default.join((0, node_os_1.homedir)(), ".remcodex", "remcodex.db");
|
|
49
|
-
}
|
|
50
|
-
exports.DEFAULT_PORT = 18840;
|
|
51
26
|
function buildRemCodexServer(options = {}) {
|
|
52
|
-
const repoRoot = options.repoRoot ? node_path_1.default.resolve(options.repoRoot) : resolvePackageRoot();
|
|
53
|
-
const port = options.port ?? Number.parseInt(process.env.PORT ??
|
|
27
|
+
const repoRoot = options.repoRoot ? node_path_1.default.resolve(options.repoRoot) : (0, runtime_paths_1.resolvePackageRoot)();
|
|
28
|
+
const port = options.port ?? Number.parseInt(process.env.PORT ?? "3000", 10);
|
|
54
29
|
const databasePath = options.databasePath ??
|
|
55
30
|
process.env.DATABASE_PATH ??
|
|
56
|
-
resolveDefaultDatabasePath();
|
|
31
|
+
(0, runtime_paths_1.resolveDefaultDatabasePath)();
|
|
57
32
|
const codexCommand = (0, command_1.resolveExecutable)(options.codexCommand ?? process.env.CODEX_COMMAND ?? "codex");
|
|
58
33
|
const codexMode = options.codexMode ?? (process.env.CODEX_MODE === "exec-json" ? "exec-json" : "app-server");
|
|
59
34
|
const projectRootsEnv = options.projectRootsEnv ?? process.env.PROJECT_ROOTS;
|
package/dist/server/src/cli.js
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
3
36
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
37
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
38
|
};
|
|
@@ -8,7 +41,7 @@ const node_fs_1 = require("node:fs");
|
|
|
8
41
|
const node_child_process_1 = require("node:child_process");
|
|
9
42
|
const node_os_1 = require("node:os");
|
|
10
43
|
const node_path_1 = __importDefault(require("node:path"));
|
|
11
|
-
const
|
|
44
|
+
const runtime_paths_1 = require("./utils/runtime-paths");
|
|
12
45
|
const command_1 = require("./utils/command");
|
|
13
46
|
function print(message = "") {
|
|
14
47
|
process.stdout.write(`${message}\n`);
|
|
@@ -18,7 +51,7 @@ function printError(message = "") {
|
|
|
18
51
|
}
|
|
19
52
|
function readPackageVersion() {
|
|
20
53
|
try {
|
|
21
|
-
const packageRoot = (0,
|
|
54
|
+
const packageRoot = (0, runtime_paths_1.resolvePackageRoot)();
|
|
22
55
|
const packageJson = JSON.parse((0, node_fs_1.readFileSync)(node_path_1.default.join(packageRoot, "package.json"), "utf8"));
|
|
23
56
|
return typeof packageJson.version === "string" ? packageJson.version : "0.0.0";
|
|
24
57
|
}
|
|
@@ -123,12 +156,24 @@ function usage() {
|
|
|
123
156
|
print(" --db <path> Use a specific SQLite database path");
|
|
124
157
|
print(" --no-open Do not open a browser automatically");
|
|
125
158
|
}
|
|
159
|
+
function formatNativeModuleError(error) {
|
|
160
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
161
|
+
if (!message.includes("NODE_MODULE_VERSION")) {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
return [
|
|
165
|
+
"Native module failed to load. This usually means RemCodex was installed with a different Node.js version.",
|
|
166
|
+
`Current Node: ${process.version}`,
|
|
167
|
+
"Reinstall remcodex with the same Node.js version you will use to run it.",
|
|
168
|
+
"If you use nvm/fnm/asdf, switch to the target Node version first, then reinstall.",
|
|
169
|
+
].join("\n");
|
|
170
|
+
}
|
|
126
171
|
async function runDoctor(flags) {
|
|
127
172
|
const version = readPackageVersion();
|
|
128
173
|
const rawCodexCommand = process.env.CODEX_COMMAND ?? "codex";
|
|
129
174
|
const codex = commandExists(rawCodexCommand);
|
|
130
|
-
const packageRoot = (0,
|
|
131
|
-
const databasePath = flags.databasePath ?? process.env.DATABASE_PATH ?? (0,
|
|
175
|
+
const packageRoot = (0, runtime_paths_1.resolvePackageRoot)();
|
|
176
|
+
const databasePath = flags.databasePath ?? process.env.DATABASE_PATH ?? (0, runtime_paths_1.resolveDefaultDatabasePath)();
|
|
132
177
|
const databaseDir = node_path_1.default.dirname(databasePath);
|
|
133
178
|
const databaseDirExists = (0, node_fs_1.existsSync)(databaseDir);
|
|
134
179
|
const databaseDirWritable = databaseDirExists && (() => {
|
|
@@ -161,6 +206,18 @@ async function runDoctor(flags) {
|
|
|
161
206
|
return 0;
|
|
162
207
|
}
|
|
163
208
|
async function runStart(flags) {
|
|
209
|
+
let startRemCodexServer;
|
|
210
|
+
try {
|
|
211
|
+
({ startRemCodexServer } = await Promise.resolve().then(() => __importStar(require("./app"))));
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
const nativeModuleMessage = formatNativeModuleError(error);
|
|
215
|
+
if (nativeModuleMessage) {
|
|
216
|
+
printError(nativeModuleMessage);
|
|
217
|
+
return 1;
|
|
218
|
+
}
|
|
219
|
+
throw error;
|
|
220
|
+
}
|
|
164
221
|
const version = readPackageVersion();
|
|
165
222
|
const rawCodexCommand = process.env.CODEX_COMMAND ?? "codex";
|
|
166
223
|
const codex = commandExists(rawCodexCommand);
|
|
@@ -169,14 +226,14 @@ async function runStart(flags) {
|
|
|
169
226
|
printError("Install Codex first, or set CODEX_COMMAND to the correct executable.");
|
|
170
227
|
return 1;
|
|
171
228
|
}
|
|
172
|
-
const preferredPort = flags.port ?? Number.parseInt(process.env.PORT ??
|
|
229
|
+
const preferredPort = flags.port ?? Number.parseInt(process.env.PORT ?? "3000", 10);
|
|
173
230
|
const codexMode = process.env.CODEX_MODE === "exec-json" ? "exec-json" : "app-server";
|
|
174
231
|
let started = null;
|
|
175
232
|
let activePort = preferredPort;
|
|
176
233
|
for (let offset = 0; offset < 20; offset += 1) {
|
|
177
234
|
const candidate = preferredPort + offset;
|
|
178
235
|
try {
|
|
179
|
-
started = await
|
|
236
|
+
started = await startRemCodexServer({
|
|
180
237
|
port: candidate,
|
|
181
238
|
databasePath: flags.databasePath,
|
|
182
239
|
codexCommand: rawCodexCommand,
|
|
@@ -6,6 +6,35 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.runMigrations = runMigrations;
|
|
7
7
|
const node_fs_1 = require("node:fs");
|
|
8
8
|
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
function isPackageRoot(root) {
|
|
10
|
+
return (0, node_fs_1.existsSync)(node_path_1.default.join(root, "package.json")) && (0, node_fs_1.existsSync)(node_path_1.default.join(root, "web", "index.html"));
|
|
11
|
+
}
|
|
12
|
+
function resolvePackageRoot(startDir = __dirname) {
|
|
13
|
+
let current = node_path_1.default.resolve(startDir);
|
|
14
|
+
while (true) {
|
|
15
|
+
if (isPackageRoot(current)) {
|
|
16
|
+
return current;
|
|
17
|
+
}
|
|
18
|
+
const parent = node_path_1.default.dirname(current);
|
|
19
|
+
if (parent === current) {
|
|
20
|
+
break;
|
|
21
|
+
}
|
|
22
|
+
current = parent;
|
|
23
|
+
}
|
|
24
|
+
return process.cwd();
|
|
25
|
+
}
|
|
26
|
+
function resolveSchemaFile() {
|
|
27
|
+
const packageRoot = resolvePackageRoot();
|
|
28
|
+
const candidates = [
|
|
29
|
+
node_path_1.default.join(packageRoot, "server", "src", "db", "schema.sql"),
|
|
30
|
+
node_path_1.default.join(packageRoot, "dist", "server", "src", "db", "schema.sql"),
|
|
31
|
+
];
|
|
32
|
+
const resolved = candidates.find((candidate) => (0, node_fs_1.existsSync)(candidate));
|
|
33
|
+
if (!resolved) {
|
|
34
|
+
throw new Error(`Database schema file not found. Tried: ${candidates.join(", ")}`);
|
|
35
|
+
}
|
|
36
|
+
return resolved;
|
|
37
|
+
}
|
|
9
38
|
function ensureColumn(db, table, column, definition) {
|
|
10
39
|
const rows = db.prepare(`PRAGMA table_info(${table})`).all();
|
|
11
40
|
if (rows.some((row) => row.name === column)) {
|
|
@@ -14,7 +43,7 @@ function ensureColumn(db, table, column, definition) {
|
|
|
14
43
|
db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
|
|
15
44
|
}
|
|
16
45
|
function runMigrations(db) {
|
|
17
|
-
const schemaFile =
|
|
46
|
+
const schemaFile = resolveSchemaFile();
|
|
18
47
|
const schema = (0, node_fs_1.readFileSync)(schemaFile, "utf8");
|
|
19
48
|
db.exec(schema);
|
|
20
49
|
ensureColumn(db, "sessions", "source_kind", "TEXT NOT NULL DEFAULT 'native'");
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS projects (
|
|
2
|
+
id TEXT PRIMARY KEY,
|
|
3
|
+
name TEXT NOT NULL,
|
|
4
|
+
path TEXT NOT NULL,
|
|
5
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
|
6
|
+
);
|
|
7
|
+
|
|
8
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
9
|
+
id TEXT PRIMARY KEY,
|
|
10
|
+
title TEXT,
|
|
11
|
+
project_id TEXT NOT NULL,
|
|
12
|
+
status TEXT NOT NULL,
|
|
13
|
+
pid INTEGER,
|
|
14
|
+
codex_thread_id TEXT,
|
|
15
|
+
source_kind TEXT NOT NULL DEFAULT 'native',
|
|
16
|
+
source_rollout_path TEXT,
|
|
17
|
+
source_thread_id TEXT,
|
|
18
|
+
source_sync_cursor INTEGER,
|
|
19
|
+
source_last_synced_at TEXT,
|
|
20
|
+
source_rollout_has_open_turn INTEGER NOT NULL DEFAULT 0,
|
|
21
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
22
|
+
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
23
|
+
FOREIGN KEY (project_id) REFERENCES projects(id)
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
CREATE TABLE IF NOT EXISTS session_events (
|
|
27
|
+
id TEXT PRIMARY KEY,
|
|
28
|
+
session_id TEXT NOT NULL,
|
|
29
|
+
turn_id TEXT,
|
|
30
|
+
seq INTEGER NOT NULL,
|
|
31
|
+
event_type TEXT NOT NULL,
|
|
32
|
+
message_id TEXT,
|
|
33
|
+
call_id TEXT,
|
|
34
|
+
request_id TEXT,
|
|
35
|
+
phase TEXT,
|
|
36
|
+
stream TEXT,
|
|
37
|
+
payload_json TEXT NOT NULL,
|
|
38
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
39
|
+
FOREIGN KEY (session_id) REFERENCES sessions(id),
|
|
40
|
+
UNIQUE (session_id, seq)
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_project_id ON sessions(project_id);
|
|
44
|
+
CREATE INDEX IF NOT EXISTS idx_session_events_session_seq ON session_events(session_id, seq);
|
|
45
|
+
CREATE INDEX IF NOT EXISTS idx_session_events_type_seq ON session_events(session_id, event_type, seq);
|
|
46
|
+
CREATE INDEX IF NOT EXISTS idx_session_events_message_id ON session_events(session_id, message_id, seq);
|
|
47
|
+
CREATE INDEX IF NOT EXISTS idx_session_events_call_id ON session_events(session_id, call_id, seq);
|
|
48
|
+
CREATE INDEX IF NOT EXISTS idx_session_events_request_id ON session_events(session_id, request_id, seq);
|
|
@@ -279,17 +279,6 @@ class SessionManager {
|
|
|
279
279
|
seq: event.seq,
|
|
280
280
|
};
|
|
281
281
|
}
|
|
282
|
-
stopSession(sessionId) {
|
|
283
|
-
const runtime = this.runners.get(sessionId);
|
|
284
|
-
if (!runtime || !runtime.runner.isAlive()) {
|
|
285
|
-
this.setStatus(sessionId, "idle");
|
|
286
|
-
return { accepted: true };
|
|
287
|
-
}
|
|
288
|
-
runtime.stopRequested = true;
|
|
289
|
-
this.setStatus(sessionId, "stopping");
|
|
290
|
-
runtime.runner.stop();
|
|
291
|
-
return { accepted: true };
|
|
292
|
-
}
|
|
293
282
|
retryApprovalRequest(sessionId, requestId, codexLaunch) {
|
|
294
283
|
const session = this.getSessionOrThrow(sessionId);
|
|
295
284
|
const project = this.options.projectManager.getProject(session.project_id);
|
|
@@ -314,6 +303,17 @@ class SessionManager {
|
|
|
314
303
|
turnId,
|
|
315
304
|
};
|
|
316
305
|
}
|
|
306
|
+
stopSession(sessionId) {
|
|
307
|
+
const runtime = this.runners.get(sessionId);
|
|
308
|
+
if (!runtime || !runtime.runner.isAlive()) {
|
|
309
|
+
this.setStatus(sessionId, "idle");
|
|
310
|
+
return { accepted: true };
|
|
311
|
+
}
|
|
312
|
+
runtime.stopRequested = true;
|
|
313
|
+
this.setStatus(sessionId, "stopping");
|
|
314
|
+
runtime.runner.stop();
|
|
315
|
+
return { accepted: true };
|
|
316
|
+
}
|
|
317
317
|
resolveApproval(sessionId, requestId, decision) {
|
|
318
318
|
const runtime = this.runners.get(sessionId);
|
|
319
319
|
if (!runtime?.runner.isAlive()) {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.resolvePackageRoot = resolvePackageRoot;
|
|
7
|
+
exports.resolveDefaultDatabasePath = resolveDefaultDatabasePath;
|
|
8
|
+
const node_fs_1 = require("node:fs");
|
|
9
|
+
const node_os_1 = require("node:os");
|
|
10
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
11
|
+
function isPackageRoot(root) {
|
|
12
|
+
return ((0, node_fs_1.existsSync)(node_path_1.default.join(root, "package.json")) &&
|
|
13
|
+
(0, node_fs_1.existsSync)(node_path_1.default.join(root, "web", "index.html")));
|
|
14
|
+
}
|
|
15
|
+
function resolvePackageRoot(startDir = __dirname) {
|
|
16
|
+
let current = node_path_1.default.resolve(startDir);
|
|
17
|
+
while (true) {
|
|
18
|
+
if (isPackageRoot(current)) {
|
|
19
|
+
return current;
|
|
20
|
+
}
|
|
21
|
+
const parent = node_path_1.default.dirname(current);
|
|
22
|
+
if (parent === current) {
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
current = parent;
|
|
26
|
+
}
|
|
27
|
+
return process.cwd();
|
|
28
|
+
}
|
|
29
|
+
function resolveDefaultDatabasePath() {
|
|
30
|
+
return node_path_1.default.join((0, node_os_1.homedir)(), ".remcodex", "remcodex.db");
|
|
31
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,23 +1,31 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "remcodex",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.11",
|
|
4
4
|
"description": "Control Codex from anywhere. Even on your phone.",
|
|
5
|
-
"license": "
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": "20.x"
|
|
8
|
+
},
|
|
6
9
|
"bin": {
|
|
7
10
|
"remcodex": "dist/server/src/cli.js"
|
|
8
11
|
},
|
|
9
12
|
"files": [
|
|
10
13
|
"dist",
|
|
11
14
|
"web",
|
|
15
|
+
"scripts/check-node-version.js",
|
|
12
16
|
"scripts/fix-node-pty-helper.js",
|
|
13
|
-
"README.md"
|
|
17
|
+
"README.md",
|
|
18
|
+
"docs/assets"
|
|
14
19
|
],
|
|
15
20
|
"scripts": {
|
|
21
|
+
"preinstall": "node scripts/check-node-version.js",
|
|
16
22
|
"postinstall": "node scripts/fix-node-pty-helper.js",
|
|
17
23
|
"dev": "tsx watch server/src/app.ts",
|
|
18
24
|
"start": "tsx server/src/cli.ts --no-open",
|
|
19
|
-
"build": "tsc -p tsconfig.json",
|
|
20
|
-
"cli": "tsx server/src/cli.ts --no-open"
|
|
25
|
+
"build": "tsc -p tsconfig.json && node scripts/copy-db-assets.js",
|
|
26
|
+
"cli": "tsx server/src/cli.ts --no-open",
|
|
27
|
+
"smoke:tarball": "node scripts/smoke-test-tarball.js",
|
|
28
|
+
"smoke:start": "node scripts/smoke-start-tarball.js"
|
|
21
29
|
},
|
|
22
30
|
"dependencies": {
|
|
23
31
|
"better-sqlite3": "^11.8.1",
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const supportedMajor = 20;
|
|
2
|
+
const currentVersion = process.versions.node;
|
|
3
|
+
const currentMajor = Number.parseInt(currentVersion.split(".")[0] || "", 10);
|
|
4
|
+
|
|
5
|
+
if (currentMajor !== supportedMajor) {
|
|
6
|
+
console.error(
|
|
7
|
+
[
|
|
8
|
+
`remcodex requires Node.js ${supportedMajor}.x for the published package.`,
|
|
9
|
+
`Current Node.js: ${currentVersion}`,
|
|
10
|
+
"Switch Node versions first, then reinstall remcodex so native modules are built against the same runtime.",
|
|
11
|
+
].join("\n"),
|
|
12
|
+
);
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 RemCodex contributors
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|