@openchamber/web 1.4.5 → 1.4.6

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/dist/index.html CHANGED
@@ -160,10 +160,10 @@
160
160
  pointer-events: none;
161
161
  }
162
162
  </style>
163
- <script type="module" crossorigin src="/assets/index-g64tpi5J.js"></script>
164
- <link rel="modulepreload" crossorigin href="/assets/vendor-.bun-B2HtLj-d.js">
163
+ <script type="module" crossorigin src="/assets/index-_QJSNcFo.js"></script>
164
+ <link rel="modulepreload" crossorigin href="/assets/vendor-.bun-C07YQe9X.js">
165
165
  <link rel="stylesheet" crossorigin href="/assets/vendor--Jn2c0Clh.css">
166
- <link rel="stylesheet" crossorigin href="/assets/index-hru9kOov.css">
166
+ <link rel="stylesheet" crossorigin href="/assets/index-Cxzt1pIT.css">
167
167
  </head>
168
168
  <body class="h-full bg-background text-foreground">
169
169
  <div id="root" class="h-full">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openchamber/web",
3
- "version": "1.4.5",
3
+ "version": "1.4.6",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "./server/index.js",
@@ -26,7 +26,7 @@
26
26
  "@fontsource/ibm-plex-mono": "^5.2.7",
27
27
  "@fontsource/ibm-plex-sans": "^5.1.1",
28
28
  "@ibm/plex": "^6.4.1",
29
- "@opencode-ai/sdk": "^1.1.6",
29
+ "@opencode-ai/sdk": "^1.1.8",
30
30
  "@radix-ui/react-collapsible": "^1.1.12",
31
31
  "@radix-ui/react-dialog": "^1.1.15",
32
32
  "@radix-ui/react-dropdown-menu": "^2.1.16",
package/server/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import express from 'express';
2
2
  import { createProxyMiddleware } from 'http-proxy-middleware';
3
3
  import path from 'path';
4
- import { spawn, spawnSync } from 'child_process';
4
+ import { spawn } from 'child_process';
5
5
  import fs from 'fs';
6
6
  import http from 'http';
7
7
  import { fileURLToPath } from 'url';
@@ -9,12 +9,12 @@ import os from 'os';
9
9
  import crypto from 'crypto';
10
10
  import { createUiAuth } from './lib/ui-auth.js';
11
11
  import { startCloudflareTunnel, printTunnelWarning, checkCloudflaredAvailable } from './lib/cloudflare-tunnel.js';
12
+ import { createOpencodeServer } from '@opencode-ai/sdk/server';
12
13
 
13
14
  const __filename = fileURLToPath(import.meta.url);
14
15
  const __dirname = path.dirname(__filename);
15
16
 
16
17
  const DEFAULT_PORT = 3000;
17
- const DEFAULT_OPENCODE_PORT = 0;
18
18
  const HEALTH_CHECK_INTERVAL = 30000;
19
19
  const SHUTDOWN_TIMEOUT = 10000;
20
20
  const MODELS_DEV_API_URL = 'https://models.dev/api.json';
@@ -848,7 +848,6 @@ let openCodeApiDetectionTimer = null;
848
848
  let isDetectingApiPrefix = false;
849
849
  let openCodeApiDetectionPromise = null;
850
850
  let lastOpenCodeError = null;
851
- let openCodePortWaiters = [];
852
851
  let isOpenCodeReady = false;
853
852
  let openCodeNotReadySince = 0;
854
853
  let exitOnShutdown = true;
@@ -890,12 +889,7 @@ async function isOpenCodeProcessHealthy() {
890
889
  return false;
891
890
  }
892
891
 
893
- // Check if process is still running
894
- if (openCodeProcess.exitCode !== null || openCodeProcess.signalCode !== null) {
895
- return false;
896
- }
897
-
898
- // Health check via HTTP
892
+ // Health check via HTTP since SDK object doesn't expose exitCode
899
893
  try {
900
894
  const response = await fetch(`http://127.0.0.1:${openCodePort}/session`, {
901
895
  method: 'GET',
@@ -907,105 +901,6 @@ async function isOpenCodeProcessHealthy() {
907
901
  }
908
902
  }
909
903
 
910
- const OPENCODE_BINARY_ENV =
911
- process.env.OPENCODE_BINARY ||
912
- process.env.OPENCHAMBER_BINARY ||
913
- process.env.OPENCODE_PATH ||
914
- process.env.OPENCHAMBER_OPENCODE_PATH ||
915
- null;
916
-
917
- function buildAugmentedPath() {
918
- const augmented = new Set();
919
-
920
- const loginShellPath = getLoginShellPath();
921
- if (loginShellPath) {
922
- for (const segment of loginShellPath.split(path.delimiter)) {
923
- if (segment) {
924
- augmented.add(segment);
925
- }
926
- }
927
- }
928
-
929
- const current = (process.env.PATH || '').split(path.delimiter).filter(Boolean);
930
- for (const segment of current) {
931
- augmented.add(segment);
932
- }
933
-
934
- return Array.from(augmented).join(path.delimiter);
935
- }
936
-
937
- function getLoginShellPath() {
938
- if (process.platform === 'win32') {
939
- return null;
940
- }
941
-
942
- const shell = process.env.SHELL || '/bin/zsh';
943
- const shellName = path.basename(shell);
944
-
945
- // Nushell requires different flag syntax and PATH access
946
- const isNushell = shellName === 'nu' || shellName === 'nushell';
947
- const args = isNushell
948
- ? ['-l', '-i', '-c', '$env.PATH | str join (char esep)']
949
- : ['-lic', 'echo -n "$PATH"'];
950
-
951
- try {
952
- const result = spawnSync(shell, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
953
- if (result.status === 0 && typeof result.stdout === 'string') {
954
- const value = result.stdout.trim();
955
- if (value) {
956
- return value;
957
- }
958
- } else if (result.stderr) {
959
- console.warn(`Failed to read PATH from login shell (${shell}): ${result.stderr}`);
960
- }
961
- } catch (error) {
962
- console.warn(`Error executing login shell (${shell}) for PATH detection: ${error.message}`);
963
- }
964
- return null;
965
- }
966
-
967
- function resolveBinaryFromPath(binaryName, searchPath) {
968
- if (!binaryName) {
969
- return null;
970
- }
971
- if (path.isAbsolute(binaryName)) {
972
- return fs.existsSync(binaryName) ? binaryName : null;
973
- }
974
- const directories = searchPath.split(path.delimiter).filter(Boolean);
975
- for (const directory of directories) {
976
- try {
977
- const candidate = path.join(directory, binaryName);
978
- if (fs.existsSync(candidate)) {
979
- const stats = fs.statSync(candidate);
980
- if (stats.isFile()) {
981
- return candidate;
982
- }
983
- }
984
- } catch {
985
- // Ignore resolution errors, continue searching
986
- }
987
- }
988
- return null;
989
- }
990
-
991
- function getOpencodeSpawnConfig() {
992
- const envPath = buildAugmentedPath();
993
- const resolvedEnv = { ...process.env, PATH: envPath };
994
-
995
- if (OPENCODE_BINARY_ENV) {
996
- const explicit = resolveBinaryFromPath(OPENCODE_BINARY_ENV, envPath);
997
- if (explicit) {
998
- console.log(`Using OpenCode binary from OPENCODE_BINARY: ${explicit}`);
999
- return { command: explicit, env: resolvedEnv };
1000
- }
1001
- console.warn(
1002
- `OPENCODE_BINARY path "${OPENCODE_BINARY_ENV}" not found. Falling back to search.`
1003
- );
1004
- }
1005
-
1006
- return { command: 'opencode', env: resolvedEnv };
1007
- }
1008
-
1009
904
  const ENV_CONFIGURED_OPENCODE_PORT = (() => {
1010
905
  const raw =
1011
906
  process.env.OPENCODE_PORT ||
@@ -1048,42 +943,31 @@ function setOpenCodePort(port) {
1048
943
  }
1049
944
 
1050
945
  lastOpenCodeError = null;
1051
-
1052
- if (openCodePortWaiters.length > 0) {
1053
- const waiters = openCodePortWaiters;
1054
- openCodePortWaiters = [];
1055
- for (const notify of waiters) {
1056
- try {
1057
- notify(numericPort);
1058
- } catch (error) {
1059
- console.warn('Failed to notify OpenCode port waiter:', error);
1060
- }
1061
- }
1062
- }
1063
946
  }
1064
947
 
1065
- async function waitForOpenCodePort(timeoutMs = 15000) {
1066
- if (openCodePort !== null) {
1067
- return openCodePort;
1068
- }
948
+ const API_PREFIX_CANDIDATES = ['', '/api']; // Simplified - only check root and /api
1069
949
 
1070
- return new Promise((resolve, reject) => {
1071
- const onPortDetected = (port) => {
950
+ async function waitForReady(url, timeoutMs = 10000) {
951
+ const start = Date.now();
952
+ while (Date.now() - start < timeoutMs) {
953
+ try {
954
+ const controller = new AbortController();
955
+ const timeout = setTimeout(() => controller.abort(), 3000);
956
+ const res = await fetch(`${url.replace(/\/+$/, '')}/config`, {
957
+ method: 'GET',
958
+ headers: { Accept: 'application/json' },
959
+ signal: controller.signal
960
+ });
1072
961
  clearTimeout(timeout);
1073
- resolve(port);
1074
- };
1075
-
1076
- const timeout = setTimeout(() => {
1077
- openCodePortWaiters = openCodePortWaiters.filter((cb) => cb !== onPortDetected);
1078
- reject(new Error('Timed out waiting for OpenCode port'));
1079
- }, timeoutMs);
1080
-
1081
- openCodePortWaiters.push(onPortDetected);
1082
- });
962
+ if (res.ok) return true;
963
+ } catch {
964
+ // ignore
965
+ }
966
+ await new Promise(r => setTimeout(r, 100));
967
+ }
968
+ return false;
1083
969
  }
1084
970
 
1085
- const API_PREFIX_CANDIDATES = ['', '/api']; // Simplified - only check root and /api
1086
-
1087
971
  function normalizeApiPrefix(prefix) {
1088
972
  if (!prefix) {
1089
973
  return '';
@@ -1119,51 +1003,6 @@ function setDetectedOpenCodeApiPrefix(prefix) {
1119
1003
  }
1120
1004
  }
1121
1005
 
1122
- function detectPortFromLogMessage(message) {
1123
- if (openCodePort && ENV_CONFIGURED_OPENCODE_PORT) {
1124
- return;
1125
- }
1126
-
1127
- const regex = /https?:\/\/[^:\s]+:(\d+)/gi;
1128
- let match;
1129
- while ((match = regex.exec(message)) !== null) {
1130
- const port = parseInt(match[1], 10);
1131
- if (Number.isFinite(port) && port > 0) {
1132
- setOpenCodePort(port);
1133
- return;
1134
- }
1135
- }
1136
-
1137
- const fallbackMatch = /(?:^|\s)(?:127\.0\.0\.1|localhost):(\d+)/i.exec(message);
1138
- if (fallbackMatch) {
1139
- const port = parseInt(fallbackMatch[1], 10);
1140
- if (Number.isFinite(port) && port > 0) {
1141
- setOpenCodePort(port);
1142
- }
1143
- }
1144
- }
1145
-
1146
- function detectPrefixFromLogMessage(message) {
1147
- if (!openCodePort) {
1148
- return;
1149
- }
1150
-
1151
- const urlRegex = /https?:\/\/[^:\s]+:(\d+)(\/[^\s"']*)?/gi;
1152
- let match;
1153
-
1154
- while ((match = urlRegex.exec(message)) !== null) {
1155
- const portMatch = parseInt(match[1], 10);
1156
- if (portMatch !== openCodePort) {
1157
- continue;
1158
- }
1159
-
1160
- const path = match[2] || '';
1161
- const normalized = normalizeApiPrefix(path);
1162
- setDetectedOpenCodeApiPrefix(normalized);
1163
- return;
1164
- }
1165
- }
1166
-
1167
1006
  function getCandidateApiPrefixes() {
1168
1007
  if (openCodeApiPrefixDetected) {
1169
1008
  return [openCodeApiPrefix];
@@ -1495,136 +1334,57 @@ function parseArgs(argv = process.argv.slice(2)) {
1495
1334
  }
1496
1335
 
1497
1336
  async function startOpenCode() {
1498
- const desiredPort = ENV_CONFIGURED_OPENCODE_PORT ?? DEFAULT_OPENCODE_PORT;
1337
+ const desiredPort = ENV_CONFIGURED_OPENCODE_PORT ?? 0;
1499
1338
  console.log(
1500
- desiredPort
1339
+ desiredPort > 0
1501
1340
  ? `Starting OpenCode on requested port ${desiredPort}...`
1502
1341
  : 'Starting OpenCode with dynamic port assignment...'
1503
1342
  );
1504
- console.log(`Starting OpenCode in working directory: ${openCodeWorkingDirectory}`);
1343
+ // Note: SDK starts in current process CWD. openCodeWorkingDirectory is tracked but not used for spawn in SDK.
1505
1344
 
1506
- const { command, env } = getOpencodeSpawnConfig();
1507
- const args = ['serve', '--port', desiredPort.toString()];
1508
- console.log(`Launching OpenCode via "${command}" with args ${args.join(' ')}`);
1509
-
1510
- const child = spawn(command, args, {
1511
- stdio: 'pipe',
1512
- env,
1513
- cwd: openCodeWorkingDirectory
1514
- });
1515
- isOpenCodeReady = false;
1516
- openCodeNotReadySince = Date.now();
1345
+ try {
1346
+ const serverInstance = await createOpencodeServer({
1347
+ hostname: '127.0.0.1',
1348
+ port: desiredPort,
1349
+ timeout: 30000,
1350
+ env: {
1351
+ ...process.env,
1352
+ // Pass minimal config to avoid pollution, but inherit PATH etc
1353
+ }
1354
+ });
1517
1355
 
1518
- let firstSignalResolver;
1519
- const firstSignalPromise = new Promise((resolve) => {
1520
- firstSignalResolver = resolve;
1521
- });
1522
- let firstSignalSettled = false;
1523
- const settleFirstSignal = () => {
1524
- if (firstSignalSettled) {
1525
- return;
1526
- }
1527
- firstSignalSettled = true;
1528
- clearTimeout(firstSignalTimer);
1529
- child.stdout.off('data', settleFirstSignal);
1530
- child.stderr.off('data', settleFirstSignal);
1531
- child.off('exit', settleFirstSignal);
1532
- if (firstSignalResolver) {
1533
- firstSignalResolver();
1356
+ if (!serverInstance || !serverInstance.url) {
1357
+ throw new Error('OpenCode server started but URL is missing');
1534
1358
  }
1535
- };
1536
- const firstSignalTimer = setTimeout(settleFirstSignal, 750);
1537
-
1538
- child.stdout.once('data', settleFirstSignal);
1539
- child.stderr.once('data', settleFirstSignal);
1540
- child.once('exit', settleFirstSignal);
1541
-
1542
- child.stdout.on('data', (data) => {
1543
- const text = data.toString();
1544
- console.log(`OpenCode: ${text.trim()}`);
1545
- detectPortFromLogMessage(text);
1546
- detectPrefixFromLogMessage(text);
1547
- settleFirstSignal();
1548
- });
1549
1359
 
1550
- child.stderr.on('data', (data) => {
1551
- const text = data.toString();
1552
- lastOpenCodeError = text.trim();
1553
- console.error(`OpenCode Error: ${lastOpenCodeError}`);
1554
- detectPortFromLogMessage(text);
1555
- detectPrefixFromLogMessage(text);
1556
- settleFirstSignal();
1557
- });
1360
+ const url = new URL(serverInstance.url);
1361
+ const port = parseInt(url.port, 10);
1362
+ const prefix = normalizeApiPrefix(url.pathname);
1558
1363
 
1559
- let startupError = await new Promise((resolve, reject) => {
1560
- const onSpawn = () => {
1561
- setOpenCodePort(desiredPort);
1562
- child.off('error', onError);
1563
- resolve(null);
1564
- };
1565
- const onError = (error) => {
1566
- child.off('spawn', onSpawn);
1567
- reject(error);
1568
- };
1364
+ if (await waitForReady(serverInstance.url, 10000)) {
1365
+ setOpenCodePort(port);
1366
+ setDetectedOpenCodeApiPrefix(prefix); // SDK URL typically includes the prefix if any
1367
+
1368
+ isOpenCodeReady = true;
1369
+ lastOpenCodeError = null;
1370
+ openCodeNotReadySince = 0;
1569
1371
 
1570
- child.once('spawn', onSpawn);
1571
- child.once('error', onError);
1572
- }).catch((error) => {
1372
+ return serverInstance;
1373
+ } else {
1374
+ try {
1375
+ serverInstance.close();
1376
+ } catch {
1377
+ // ignore
1378
+ }
1379
+ throw new Error('Server started but health check failed (timeout)');
1380
+ }
1381
+ } catch (error) {
1573
1382
  lastOpenCodeError = error.message;
1574
1383
  openCodePort = null;
1575
1384
  syncToHmrState();
1576
- settleFirstSignal();
1577
- return error;
1578
- });
1579
-
1580
- if (startupError) {
1581
- if (startupError.code === 'ENOENT') {
1582
- const enhanced = new Error(
1583
- `Failed to start OpenCode – executable "${command}" not found. ` +
1584
- 'Set OPENCODE_BINARY to the full path of the opencode CLI or ensure it is on PATH.'
1585
- );
1586
- enhanced.code = startupError.code;
1587
- startupError = enhanced;
1588
- }
1589
- throw startupError;
1385
+ console.error(`Failed to start OpenCode: ${error.message}`);
1386
+ throw error;
1590
1387
  }
1591
-
1592
- child.on('exit', (code, signal) => {
1593
- lastOpenCodeError = `OpenCode exited with code ${code}, signal ${signal ?? 'null'}`;
1594
- isOpenCodeReady = false;
1595
- openCodeNotReadySince = Date.now();
1596
-
1597
- if (!isShuttingDown && !isRestartingOpenCode) {
1598
- console.log(`OpenCode process exited with code ${code}, signal ${signal}`);
1599
-
1600
- setTimeout(() => {
1601
- restartOpenCode().catch((err) => {
1602
- console.error('Failed to restart OpenCode after exit:', err);
1603
- });
1604
- }, 5000);
1605
- } else if (isRestartingOpenCode) {
1606
- console.log('OpenCode exit during controlled restart, not triggering auto-restart');
1607
- }
1608
- });
1609
-
1610
- child.on('error', (error) => {
1611
- lastOpenCodeError = error.message;
1612
- isOpenCodeReady = false;
1613
- openCodeNotReadySince = Date.now();
1614
- console.error(`OpenCode process error: ${error.message}`);
1615
- if (!isShuttingDown) {
1616
-
1617
- setTimeout(() => {
1618
- restartOpenCode().catch((err) => {
1619
- console.error('Failed to restart OpenCode after error:', err);
1620
- });
1621
- }, 5000);
1622
- }
1623
- });
1624
-
1625
- await firstSignalPromise;
1626
-
1627
- return child;
1628
1388
  }
1629
1389
 
1630
1390
  async function restartOpenCode() {
@@ -1641,60 +1401,16 @@ async function restartOpenCode() {
1641
1401
  console.log('Restarting OpenCode process...');
1642
1402
 
1643
1403
  if (openCodeProcess) {
1644
- console.log('Waiting for OpenCode process to terminate...');
1645
- const processToTerminate = openCodeProcess;
1646
- let forcedTermination = false;
1647
-
1648
- if (processToTerminate.exitCode === null && processToTerminate.signalCode === null) {
1649
- processToTerminate.kill('SIGTERM');
1650
-
1651
- await new Promise((resolve) => {
1652
- let resolved = false;
1653
-
1654
- const cleanup = () => {
1655
- processToTerminate.off('exit', onExit);
1656
- clearTimeout(forceKillTimer);
1657
- clearTimeout(hardStopTimer);
1658
- if (!resolved) {
1659
- resolved = true;
1660
- resolve();
1661
- }
1662
- };
1663
-
1664
- const onExit = () => {
1665
- cleanup();
1666
- };
1667
-
1668
- const forceKillTimer = setTimeout(() => {
1669
- if (resolved) {
1670
- return;
1671
- }
1672
- forcedTermination = true;
1673
- console.warn('OpenCode process did not exit after SIGTERM, sending SIGKILL');
1674
- processToTerminate.kill('SIGKILL');
1675
- }, 3000);
1676
-
1677
- const hardStopTimer = setTimeout(() => {
1678
- if (resolved) {
1679
- return;
1680
- }
1681
- console.warn('OpenCode process unresponsive after SIGKILL, continuing restart');
1682
- cleanup();
1683
- }, 5000);
1684
-
1685
- processToTerminate.once('exit', onExit);
1686
- });
1687
-
1688
- if (forcedTermination) {
1689
- console.log('OpenCode process terminated forcefully during restart');
1690
- }
1691
- } else {
1692
- console.log('OpenCode process already stopped before restart command');
1404
+ console.log('Stopping existing OpenCode process...');
1405
+ try {
1406
+ openCodeProcess.close();
1407
+ } catch (error) {
1408
+ console.warn('Error closing OpenCode process:', error);
1693
1409
  }
1694
-
1695
1410
  openCodeProcess = null;
1696
1411
  syncToHmrState();
1697
-
1412
+
1413
+ // Brief delay to allow port release
1698
1414
  await new Promise((resolve) => setTimeout(resolve, 250));
1699
1415
  }
1700
1416
 
@@ -1705,6 +1421,8 @@ async function restartOpenCode() {
1705
1421
  openCodePort = null;
1706
1422
  syncToHmrState();
1707
1423
  }
1424
+
1425
+ // Reset detection state
1708
1426
  openCodeApiPrefixDetected = false;
1709
1427
  if (openCodeApiDetectionTimer) {
1710
1428
  clearTimeout(openCodeApiDetectionTimer);
@@ -1716,13 +1434,10 @@ async function restartOpenCode() {
1716
1434
  openCodeProcess = await startOpenCode();
1717
1435
  syncToHmrState();
1718
1436
 
1719
- if (!ENV_CONFIGURED_OPENCODE_PORT) {
1720
- await waitForOpenCodePort();
1721
- }
1722
-
1723
1437
  if (expressApp) {
1724
1438
  setupProxy(expressApp);
1725
- scheduleOpenCodeApiDetection();
1439
+ // Ensure prefix is set correctly (SDK usually handles this, but just in case)
1440
+ ensureOpenCodeApiPrefix();
1726
1441
  }
1727
1442
  })();
1728
1443