remote-codex 0.1.2 → 0.1.4

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 CHANGED
@@ -43,8 +43,8 @@ remote-codex stop
43
43
 
44
44
  The global CLI starts the production API and web service:
45
45
 
46
- - API: `http://127.0.0.1:8787`
47
- - Web: `http://127.0.0.1:4173`
46
+ - API: `http://127.0.0.1:45674`
47
+ - Web: `http://127.0.0.1:45673`
48
48
  - service logs/state: `~/.remote-codex/service/`
49
49
  - production database: `~/.remote-codex/supervisor.sqlite`
50
50
 
@@ -6097,7 +6097,10 @@ function getDefaultHostRecord() {
6097
6097
  // ../../packages/db/src/migrate.ts
6098
6098
  import fs2 from "fs";
6099
6099
  import path3 from "path";
6100
- function resolveRepoRoot(start = process.cwd()) {
6100
+ function resolvePackageRoot(start = process.cwd()) {
6101
+ if (process.env.REMOTE_CODEX_PACKAGE_ROOT) {
6102
+ return path3.resolve(process.env.REMOTE_CODEX_PACKAGE_ROOT);
6103
+ }
6101
6104
  let current = path3.resolve(start);
6102
6105
  while (current !== path3.dirname(current)) {
6103
6106
  if (fs2.existsSync(path3.join(current, "pnpm-workspace.yaml"))) {
@@ -6105,10 +6108,10 @@ function resolveRepoRoot(start = process.cwd()) {
6105
6108
  }
6106
6109
  current = path3.dirname(current);
6107
6110
  }
6108
- throw new Error("Unable to locate repository root from current working directory.");
6111
+ throw new Error("Unable to locate package root from current working directory.");
6109
6112
  }
6110
6113
  function getMigrationsDir() {
6111
- return path3.join(resolveRepoRoot(), "packages", "db", "migrations");
6114
+ return path3.join(resolvePackageRoot(), "packages", "db", "migrations");
6112
6115
  }
6113
6116
  function runMigrations(databaseUrl) {
6114
6117
  const { sqlite } = createDatabase(databaseUrl);
@@ -8262,7 +8265,10 @@ import fs7 from "fs";
8262
8265
  import path7 from "path";
8263
8266
  var TOKEN_PRICE_DENOMINATOR = 1e6;
8264
8267
  var cachedPricingConfig = null;
8265
- function resolveRepoRoot2(start = process.cwd()) {
8268
+ function resolvePackageRoot2(start = process.cwd()) {
8269
+ if (process.env.REMOTE_CODEX_PACKAGE_ROOT) {
8270
+ return path7.resolve(process.env.REMOTE_CODEX_PACKAGE_ROOT);
8271
+ }
8266
8272
  let current = path7.resolve(start);
8267
8273
  while (current !== path7.dirname(current)) {
8268
8274
  if (fs7.existsSync(path7.join(current, "pnpm-workspace.yaml"))) {
@@ -8270,10 +8276,10 @@ function resolveRepoRoot2(start = process.cwd()) {
8270
8276
  }
8271
8277
  current = path7.dirname(current);
8272
8278
  }
8273
- throw new Error("Unable to locate repository root for Codex pricing config.");
8279
+ throw new Error("Unable to locate package root for Codex pricing config.");
8274
8280
  }
8275
8281
  function getPricingConfigPath() {
8276
- return path7.join(resolveRepoRoot2(), "config", "codex-model-pricing.json");
8282
+ return path7.join(resolvePackageRoot2(), "config", "codex-model-pricing.json");
8277
8283
  }
8278
8284
  function isPositiveNumber(value) {
8279
8285
  return typeof value === "number" && Number.isFinite(value) && value >= 0;
@@ -9,6 +9,11 @@ const binDir = path.dirname(fileURLToPath(import.meta.url));
9
9
  const packageRoot = path.resolve(binDir, '..');
10
10
  const packageJsonPath = path.join(packageRoot, 'package.json');
11
11
  const serviceManagerPath = path.join(packageRoot, 'scripts', 'service-manager.mjs');
12
+ const sourceCheckout =
13
+ fs.existsSync(path.join(packageRoot, 'pnpm-workspace.yaml')) &&
14
+ fs.existsSync(path.join(packageRoot, 'scripts', 'service-restart.mjs'));
15
+ const defaultServicePort = sourceCheckout ? 4173 : 45673;
16
+ const defaultApiPort = sourceCheckout ? 8787 : 45674;
12
17
 
13
18
  const aliases = new Map([
14
19
  ['service:start', 'start'],
@@ -84,9 +89,9 @@ Usage:
84
89
 
85
90
  Environment:
86
91
  SERVICE_HOST Web listen host, default 127.0.0.1
87
- SERVICE_PORT Web listen port, default 4173
92
+ SERVICE_PORT Web listen port, default ${defaultServicePort}
88
93
  SERVICE_API_HOST API listen host, default 127.0.0.1
89
- SERVICE_API_PORT API listen port, default 8787
94
+ SERVICE_API_PORT API listen port, default ${defaultApiPort}
90
95
  REMOTE_CODEX_SERVICE_DIR Service state and log directory, default ~/.remote-codex/service
91
96
  `);
92
97
  }
@@ -0,0 +1,57 @@
1
+ {
2
+ "currency": "USD",
3
+ "tiers": {
4
+ "standard": {
5
+ "multiplier": 1
6
+ },
7
+ "fast": {
8
+ "multiplier": 2
9
+ }
10
+ },
11
+ "models": {
12
+ "gpt-5.5": {
13
+ "inputUsdPerMillion": 5,
14
+ "cachedInputUsdPerMillion": 0.5,
15
+ "outputUsdPerMillion": 30,
16
+ "supportsFastMode": true,
17
+ "fastMultiplier": 2.5,
18
+ "contextWindowTokens": 272000
19
+ },
20
+ "gpt-5.3-codex": {
21
+ "inputUsdPerMillion": 1.75,
22
+ "cachedInputUsdPerMillion": 0.175,
23
+ "outputUsdPerMillion": 14,
24
+ "supportsFastMode": true
25
+ },
26
+ "gpt-5.4": {
27
+ "inputUsdPerMillion": 2.5,
28
+ "cachedInputUsdPerMillion": 0.25,
29
+ "outputUsdPerMillion": 15,
30
+ "supportsFastMode": true
31
+ },
32
+ "gpt-5.2-codex": {
33
+ "inputUsdPerMillion": 1.75,
34
+ "cachedInputUsdPerMillion": 0.175,
35
+ "outputUsdPerMillion": 14,
36
+ "supportsFastMode": false
37
+ },
38
+ "gpt-5.1-codex-max": {
39
+ "inputUsdPerMillion": 1.25,
40
+ "cachedInputUsdPerMillion": 0.125,
41
+ "outputUsdPerMillion": 10,
42
+ "supportsFastMode": false
43
+ },
44
+ "gpt-5.2": {
45
+ "inputUsdPerMillion": 1.75,
46
+ "cachedInputUsdPerMillion": 0.175,
47
+ "outputUsdPerMillion": 14,
48
+ "supportsFastMode": false
49
+ },
50
+ "gpt-5.1-codex-mini": {
51
+ "inputUsdPerMillion": 0.25,
52
+ "cachedInputUsdPerMillion": 0.025,
53
+ "outputUsdPerMillion": 2,
54
+ "supportsFastMode": false
55
+ }
56
+ }
57
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "remote-codex",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Local web supervisor for Codex workspaces and threads.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -13,6 +13,8 @@
13
13
  "scripts/run-web-service.mjs",
14
14
  "apps/supervisor-api/dist/",
15
15
  "apps/supervisor-web/dist/",
16
+ "config/codex-model-pricing.json",
17
+ "packages/db/migrations/*.sql",
16
18
  "README.md"
17
19
  ],
18
20
  "packageManager": "pnpm@10.11.1",
@@ -0,0 +1,74 @@
1
+ CREATE TABLE IF NOT EXISTS hosts (
2
+ id TEXT PRIMARY KEY NOT NULL,
3
+ hostname TEXT NOT NULL,
4
+ platform TEXT NOT NULL,
5
+ tailscale_name TEXT,
6
+ created_at TEXT NOT NULL,
7
+ last_seen_at TEXT NOT NULL
8
+ );
9
+
10
+ CREATE TABLE IF NOT EXISTS workspaces (
11
+ id TEXT PRIMARY KEY NOT NULL,
12
+ host_id TEXT NOT NULL,
13
+ label TEXT NOT NULL,
14
+ abs_path TEXT NOT NULL UNIQUE,
15
+ is_favorite INTEGER NOT NULL DEFAULT 0,
16
+ created_at TEXT NOT NULL,
17
+ last_opened_at TEXT
18
+ );
19
+
20
+ CREATE TABLE IF NOT EXISTS threads (
21
+ id TEXT PRIMARY KEY NOT NULL,
22
+ workspace_id TEXT NOT NULL,
23
+ codex_thread_id TEXT,
24
+ title TEXT NOT NULL,
25
+ model TEXT,
26
+ approval_mode TEXT,
27
+ status TEXT,
28
+ created_at TEXT NOT NULL,
29
+ updated_at TEXT NOT NULL,
30
+ last_turn_started_at TEXT,
31
+ last_turn_completed_at TEXT,
32
+ last_viewed_at TEXT,
33
+ is_pinned INTEGER NOT NULL DEFAULT 0
34
+ );
35
+
36
+ CREATE TABLE IF NOT EXISTS shell_sessions (
37
+ id TEXT PRIMARY KEY NOT NULL,
38
+ workspace_id TEXT NOT NULL,
39
+ thread_id TEXT,
40
+ tmux_session_name TEXT,
41
+ cwd TEXT NOT NULL,
42
+ status TEXT,
43
+ created_at TEXT NOT NULL,
44
+ updated_at TEXT NOT NULL,
45
+ last_activity_at TEXT
46
+ );
47
+
48
+ CREATE TABLE IF NOT EXISTS viewer_sessions (
49
+ id TEXT PRIMARY KEY NOT NULL,
50
+ thread_id TEXT,
51
+ shell_id TEXT,
52
+ connected_at TEXT NOT NULL,
53
+ last_heartbeat_at TEXT,
54
+ active_tab TEXT
55
+ );
56
+
57
+ CREATE TABLE IF NOT EXISTS notifications (
58
+ id TEXT PRIMARY KEY NOT NULL,
59
+ thread_id TEXT,
60
+ kind TEXT NOT NULL,
61
+ severity TEXT NOT NULL,
62
+ title TEXT NOT NULL,
63
+ body TEXT NOT NULL,
64
+ is_read INTEGER NOT NULL DEFAULT 0,
65
+ created_at TEXT NOT NULL
66
+ );
67
+
68
+ CREATE TABLE IF NOT EXISTS policies (
69
+ id TEXT PRIMARY KEY NOT NULL,
70
+ key TEXT NOT NULL UNIQUE,
71
+ value_json TEXT NOT NULL,
72
+ created_at TEXT NOT NULL,
73
+ updated_at TEXT NOT NULL
74
+ );
@@ -0,0 +1,3 @@
1
+ ALTER TABLE threads ADD COLUMN codex_turn_id TEXT;
2
+ ALTER TABLE threads ADD COLUMN summary_text TEXT;
3
+ ALTER TABLE threads ADD COLUMN last_error TEXT;
@@ -0,0 +1 @@
1
+ ALTER TABLE threads ADD COLUMN source TEXT NOT NULL DEFAULT 'supervisor';
@@ -0,0 +1,2 @@
1
+ ALTER TABLE threads ADD COLUMN reasoning_effort TEXT;
2
+ ALTER TABLE threads ADD COLUMN collaboration_mode TEXT NOT NULL DEFAULT 'default';
@@ -0,0 +1 @@
1
+ ALTER TABLE threads ADD COLUMN is_connected INTEGER NOT NULL DEFAULT 1;
@@ -0,0 +1,13 @@
1
+ CREATE TABLE IF NOT EXISTS thread_turn_metadata (
2
+ id TEXT PRIMARY KEY NOT NULL,
3
+ thread_id TEXT NOT NULL,
4
+ turn_id TEXT NOT NULL,
5
+ model TEXT,
6
+ reasoning_effort TEXT,
7
+ reasoning_effort_available INTEGER,
8
+ created_at TEXT NOT NULL,
9
+ updated_at TEXT NOT NULL
10
+ );
11
+
12
+ CREATE UNIQUE INDEX IF NOT EXISTS thread_turn_metadata_thread_turn_idx
13
+ ON thread_turn_metadata(thread_id, turn_id);
@@ -0,0 +1 @@
1
+ ALTER TABLE threads ADD COLUMN sandbox_mode TEXT;
@@ -0,0 +1,13 @@
1
+ CREATE TABLE IF NOT EXISTS thread_pending_steers (
2
+ id TEXT PRIMARY KEY,
3
+ thread_id TEXT NOT NULL,
4
+ turn_id TEXT NOT NULL,
5
+ client_request_id TEXT,
6
+ display_prompt TEXT NOT NULL,
7
+ submitted_prompt TEXT NOT NULL,
8
+ created_at TEXT NOT NULL,
9
+ updated_at TEXT NOT NULL
10
+ );
11
+
12
+ CREATE INDEX IF NOT EXISTS thread_pending_steers_thread_created_idx
13
+ ON thread_pending_steers(thread_id, created_at);
@@ -0,0 +1,19 @@
1
+ ALTER TABLE threads
2
+ ADD COLUMN fast_mode INTEGER NOT NULL DEFAULT 0;
3
+
4
+ ALTER TABLE threads
5
+ ADD COLUMN fast_base_model TEXT;
6
+
7
+ ALTER TABLE threads
8
+ ADD COLUMN fast_base_reasoning_effort TEXT;
9
+
10
+ CREATE TABLE IF NOT EXISTS thread_activity_notes (
11
+ id TEXT PRIMARY KEY,
12
+ thread_id TEXT NOT NULL,
13
+ kind TEXT NOT NULL,
14
+ text TEXT NOT NULL,
15
+ created_at TEXT NOT NULL
16
+ );
17
+
18
+ CREATE INDEX IF NOT EXISTS thread_activity_notes_thread_created_idx
19
+ ON thread_activity_notes(thread_id, created_at);
@@ -0,0 +1,2 @@
1
+ ALTER TABLE thread_turn_metadata
2
+ ADD COLUMN token_usage_json TEXT;
@@ -0,0 +1,5 @@
1
+ ALTER TABLE thread_turn_metadata
2
+ ADD COLUMN pricing_model_key TEXT;
3
+
4
+ ALTER TABLE thread_turn_metadata
5
+ ADD COLUMN pricing_tier_key TEXT;
@@ -0,0 +1,8 @@
1
+ CREATE TABLE IF NOT EXISTS thread_forks (
2
+ id TEXT PRIMARY KEY NOT NULL,
3
+ source_thread_id TEXT NOT NULL,
4
+ source_turn_id TEXT,
5
+ source_turn_index INTEGER,
6
+ forked_thread_id TEXT NOT NULL,
7
+ created_at TEXT NOT NULL
8
+ );
@@ -0,0 +1,17 @@
1
+ CREATE TABLE IF NOT EXISTS thread_goals (
2
+ id TEXT PRIMARY KEY NOT NULL,
3
+ thread_id TEXT NOT NULL,
4
+ codex_thread_id TEXT NOT NULL,
5
+ objective TEXT NOT NULL,
6
+ status TEXT NOT NULL,
7
+ token_budget INTEGER,
8
+ tokens_used INTEGER NOT NULL DEFAULT 0,
9
+ time_used_seconds INTEGER NOT NULL DEFAULT 0,
10
+ started_at TEXT NOT NULL,
11
+ completed_at TEXT,
12
+ created_at TEXT NOT NULL,
13
+ updated_at TEXT NOT NULL
14
+ );
15
+
16
+ CREATE INDEX IF NOT EXISTS thread_goals_thread_updated_idx
17
+ ON thread_goals(thread_id, updated_at);
@@ -0,0 +1,2 @@
1
+ ALTER TABLE thread_activity_notes
2
+ ADD COLUMN anchor_turn_id TEXT;
@@ -7,11 +7,16 @@ import { fileURLToPath } from 'node:url';
7
7
 
8
8
  const scriptDir = path.dirname(fileURLToPath(import.meta.url));
9
9
  const repoRoot = path.resolve(scriptDir, '..');
10
+ const sourceCheckout =
11
+ fs.existsSync(path.join(repoRoot, 'pnpm-workspace.yaml')) &&
12
+ fs.existsSync(path.join(repoRoot, 'scripts', 'service-restart.mjs'));
13
+ const defaultServicePort = sourceCheckout ? 4173 : 45673;
14
+ const defaultApiPort = sourceCheckout ? 8787 : 45674;
10
15
 
11
16
  const serviceHost = process.env.SERVICE_HOST ?? '127.0.0.1';
12
- const servicePort = parsePort(process.env.SERVICE_PORT, 4173);
17
+ const servicePort = parsePort(process.env.SERVICE_PORT, defaultServicePort);
13
18
  const apiHost = process.env.SERVICE_API_HOST ?? '127.0.0.1';
14
- const apiPort = parsePort(process.env.SERVICE_API_PORT, 8787);
19
+ const apiPort = parsePort(process.env.SERVICE_API_PORT, defaultApiPort);
15
20
  const distDir = path.resolve(
16
21
  process.env.SERVICE_WEB_DIST_DIR ?? path.join(repoRoot, 'apps/supervisor-web/dist')
17
22
  );
@@ -4,6 +4,7 @@ import os from 'node:os';
4
4
  import path from 'node:path';
5
5
  import { spawn } from 'node:child_process';
6
6
  import { fileURLToPath } from 'node:url';
7
+ import net from 'node:net';
7
8
 
8
9
  const scriptDir = path.dirname(fileURLToPath(import.meta.url));
9
10
  const repoRoot = path.resolve(scriptDir, '..');
@@ -17,11 +18,13 @@ const webIndex = path.join(repoRoot, 'apps', 'supervisor-web', 'dist', 'index.ht
17
18
  const supportsSourceRestart =
18
19
  fs.existsSync(path.join(repoRoot, 'pnpm-workspace.yaml')) &&
19
20
  fs.existsSync(path.join(repoRoot, 'scripts', 'service-restart.mjs'));
21
+ const defaultServicePort = supportsSourceRestart ? 4173 : 45673;
22
+ const defaultApiPort = supportsSourceRestart ? 8787 : 45674;
20
23
 
21
24
  const serviceHost = process.env.SERVICE_HOST ?? '127.0.0.1';
22
- const servicePort = parsePort(process.env.SERVICE_PORT, 4173);
25
+ const servicePort = parsePort(process.env.SERVICE_PORT, defaultServicePort);
23
26
  const apiHost = process.env.SERVICE_API_HOST ?? '127.0.0.1';
24
- const apiPort = parsePort(process.env.SERVICE_API_PORT, 8787);
27
+ const apiPort = parsePort(process.env.SERVICE_API_PORT, defaultApiPort);
25
28
 
26
29
  const command = process.argv[2];
27
30
 
@@ -58,6 +61,10 @@ async function startService() {
58
61
  const webLogPath = path.join(serviceDir, 'web.log');
59
62
  prepareLogFile(apiLogPath);
60
63
  prepareLogFile(webLogPath);
64
+
65
+ await assertTcpPortAvailable(apiHost, apiPort, 'API');
66
+ await assertTcpPortAvailable(serviceHost, servicePort, 'Web');
67
+
61
68
  const apiPid = spawnDetached(process.execPath, [apiEntry], apiLogPath, {
62
69
  NODE_ENV: 'production',
63
70
  HOST: apiHost,
@@ -73,7 +80,7 @@ async function startService() {
73
80
  await waitForHttp(`http://${apiHost}:${apiPort}/healthz`, apiPid, 15_000);
74
81
  } catch (error) {
75
82
  stopPid(apiPid);
76
- throw error;
83
+ throw appendLogTail(error, apiLogPath, 'API');
77
84
  }
78
85
 
79
86
  const webPid = spawnDetached(process.execPath, [webEntry], webLogPath, {
@@ -89,7 +96,7 @@ async function startService() {
89
96
  } catch (error) {
90
97
  stopPid(webPid);
91
98
  stopPid(apiPid);
92
- throw error;
99
+ throw appendLogTail(error, webLogPath, 'Web');
93
100
  }
94
101
 
95
102
  const state = {
@@ -214,6 +221,54 @@ async function waitForHttp(url, pid, timeoutMs) {
214
221
  throw new Error(`Timed out waiting for ${url}.`);
215
222
  }
216
223
 
224
+ async function assertTcpPortAvailable(host, port, label) {
225
+ await new Promise((resolve, reject) => {
226
+ const server = net.createServer();
227
+ server.unref();
228
+
229
+ server.once('error', (error) => {
230
+ const code = typeof error === 'object' && error !== null ? error.code : undefined;
231
+ if (code === 'EADDRINUSE') {
232
+ reject(
233
+ new Error(
234
+ `${label} port ${host}:${port} is already in use. Set ${label === 'API' ? 'SERVICE_API_PORT' : 'SERVICE_PORT'} to another port, or stop the process currently using it.`
235
+ )
236
+ );
237
+ return;
238
+ }
239
+
240
+ reject(error);
241
+ });
242
+
243
+ server.listen(port, host, () => {
244
+ server.close(resolve);
245
+ });
246
+ });
247
+ }
248
+
249
+ function appendLogTail(error, logPath, label) {
250
+ const message = error instanceof Error ? error.message : String(error);
251
+ const tail = readLogTail(logPath, 80);
252
+ if (!tail) {
253
+ return new Error(`${message}\n${label} log: ${logPath}`);
254
+ }
255
+
256
+ return new Error(`${message}\n${label} log: ${logPath}\n\nLast ${label} log lines:\n${tail}`);
257
+ }
258
+
259
+ function readLogTail(logPath, maxLines) {
260
+ try {
261
+ const content = fs.readFileSync(logPath, 'utf8').trimEnd();
262
+ if (!content) {
263
+ return '';
264
+ }
265
+
266
+ return content.split(/\r?\n/).slice(-maxLines).join('\n');
267
+ } catch {
268
+ return '';
269
+ }
270
+ }
271
+
217
272
  async function probeHttp(url) {
218
273
  try {
219
274
  const controller = new AbortController();