osborn 0.9.155 β†’ 0.9.159

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.
@@ -140,10 +140,17 @@ function ensureCompactionSettings() {
140
140
  else {
141
141
  mkdirSync(claudeDir, { recursive: true });
142
142
  }
143
+ let dirty = false;
143
144
  if (settings.autoCompactWindow !== 1_000_000) {
144
145
  settings.autoCompactWindow = 1_000_000;
146
+ dirty = true;
147
+ }
148
+ if (settings.cleanupPeriodDays !== 3650) {
149
+ settings.cleanupPeriodDays = 3650;
150
+ dirty = true;
151
+ }
152
+ if (dirty) {
145
153
  writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
146
- console.log('πŸͺŸ autoCompactWindow set to 1,000,000 in', settingsPath);
147
154
  }
148
155
  }
149
156
  catch (err) {
package/dist/config.js CHANGED
@@ -595,7 +595,7 @@ async function extractCwd(filePath) {
595
595
  *
596
596
  * @param limit - Max sessions to return (default 100, sorted by recency)
597
597
  */
598
- export async function listAllClaudeSessions(limit = 100) {
598
+ export async function listAllClaudeSessions(limit = 2000) {
599
599
  const projectsDir = getClaudeProjectsDir();
600
600
  if (!existsSync(projectsDir))
601
601
  return [];
package/dist/index.js CHANGED
@@ -267,6 +267,42 @@ let codeServerStartHook = null;
267
267
  // code-server runs on 127.0.0.1:8300; the agent proxies it through its own
268
268
  // public HTTP server so no Cloudflare tunnel is needed.
269
269
  const IDE_TARGET = 'http://127.0.0.1:8300';
270
+ const OSBORN_API_PORT = parseInt(process.env.OSBORN_API_PORT || '8741', 10);
271
+ // Dev-domain wildcard subdomain routing.
272
+ // When DEV_DOMAIN is set (e.g. "dev.voice-native.com"), requests arriving with
273
+ // Host: PORT-APPNAME.dev.voice-native.com are proxied to localhost:PORT at root.
274
+ // The fleet router (apps/dev-router) handles TLS + fly-replay to reach this machine;
275
+ // the agent only needs to detect the pattern and forward.
276
+ const DEV_DOMAIN = process.env.DEV_DOMAIN || '';
277
+ function parseDevSubdomainPort(req) {
278
+ if (!DEV_DOMAIN)
279
+ return null;
280
+ // Primary: Host header β€” preserved through fly-replay by Fly
281
+ const hostname = (req.headers.host || '').split(':')[0].toLowerCase();
282
+ const suffix = '.' + DEV_DOMAIN;
283
+ if (hostname.endsWith(suffix)) {
284
+ const label = hostname.slice(0, -suffix.length); // e.g. "3000-osborn-abc"
285
+ const dashIdx = label.indexOf('-');
286
+ if (dashIdx > 0) {
287
+ const port = parseInt(label.slice(0, dashIdx), 10);
288
+ if (!isNaN(port) && port > 1024 && port < 65536 && port !== 8300 && port !== OSBORN_API_PORT) {
289
+ return port;
290
+ }
291
+ }
292
+ }
293
+ // Fallback: fly-replay-src state field β€” fleet router encodes "state=port:PORT"
294
+ const replaySrc = req.headers['fly-replay-src'];
295
+ if (replaySrc) {
296
+ const m = replaySrc.match(/state=port:(\d+)/);
297
+ if (m) {
298
+ const port = parseInt(m[1], 10);
299
+ if (!isNaN(port) && port > 1024 && port < 65536 && port !== 8300 && port !== OSBORN_API_PORT) {
300
+ return port;
301
+ }
302
+ }
303
+ }
304
+ return null;
305
+ }
270
306
  // Route prefixes that belong to the agent itself. The proxy fall-through checks
271
307
  // this list FIRST so it never intercepts an agent API path.
272
308
  const AGENT_ROUTE_PREFIXES = [
@@ -1498,21 +1534,44 @@ function startApiServer(workingDir, port) {
1498
1534
  // 3 s until ready.
1499
1535
  if (url.pathname === '/editor' && req.method === 'GET') {
1500
1536
  // AUTH GATE (future): validate key / signed session before minting the cookie
1537
+ // Optional ?folder= param: must be an absolute path under /workspace with no
1538
+ // directory-traversal sequences. If absent or invalid, fall back to bare '/'.
1539
+ const rawFolder = url.searchParams.get('folder') ?? '';
1540
+ const folderParam = (() => {
1541
+ if (!rawFolder)
1542
+ return '';
1543
+ // Must be absolute, start with /workspace, no traversal sequences
1544
+ if (!rawFolder.startsWith('/workspace') ||
1545
+ rawFolder.includes('..') ||
1546
+ rawFolder.includes('\0'))
1547
+ return '';
1548
+ // Normalise and re-check (catches encoded or doubled slashes resolving outside /workspace)
1549
+ const normalised = rawFolder.replace(/\/+/g, '/');
1550
+ if (!normalised.startsWith('/workspace'))
1551
+ return '';
1552
+ return normalised;
1553
+ })();
1501
1554
  if (ideProxyEnabled && ideSessionToken) {
1502
- // code-server is up β€” set cookie and redirect to root (code-server's asset base)
1555
+ // code-server is up β€” set cookie and redirect into the requested folder (if valid)
1556
+ // or to root if no valid folder was supplied.
1557
+ const location = folderParam ? `/?folder=${encodeURIComponent(folderParam)}` : '/';
1503
1558
  res.writeHead(302, {
1504
1559
  'Set-Cookie': `osborn_ide=${ideSessionToken}; Path=/; HttpOnly; SameSite=Lax`,
1505
- 'Location': '/',
1560
+ 'Location': location,
1506
1561
  });
1507
1562
  res.end();
1508
1563
  }
1509
1564
  else {
1510
- // Not yet running β€” kick off start in the background, show "Starting…" page
1565
+ // Not yet running β€” kick off start in the background, show "Starting…" page.
1566
+ // Preserve ?folder= in the refresh URL so it survives the polling loop.
1511
1567
  if (codeServerStartHook) {
1512
1568
  codeServerStartHook().catch((err) => {
1513
1569
  console.error('❌ startCodeServer() from /editor failed:', err);
1514
1570
  });
1515
1571
  }
1572
+ const refreshTarget = folderParam
1573
+ ? `/editor?folder=${encodeURIComponent(folderParam)}`
1574
+ : '/editor';
1516
1575
  res.writeHead(200, {
1517
1576
  'Content-Type': 'text/html; charset=utf-8',
1518
1577
  'Cache-Control': 'no-store, must-revalidate',
@@ -1520,7 +1579,7 @@ function startApiServer(workingDir, port) {
1520
1579
  });
1521
1580
  res.end(`<!DOCTYPE html><html><head>
1522
1581
  <meta charset="utf-8">
1523
- <meta http-equiv="refresh" content="3;url=/editor">
1582
+ <meta http-equiv="refresh" content="3;url=${refreshTarget}">
1524
1583
  <title>Starting editor…</title>
1525
1584
  <style>body{font-family:sans-serif;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;background:#1e1e1e;color:#ccc}</style>
1526
1585
  </head><body><p>Starting your editor…</p></body></html>`);
@@ -1528,6 +1587,32 @@ function startApiServer(workingDir, port) {
1528
1587
  return;
1529
1588
  }
1530
1589
  // ────────────────────────────────────────────────────────────────────────
1590
+ // ── Dev-domain subdomain proxy: PORT-slug.dev.voice-native.com β†’ localhost:PORT/* ──
1591
+ // Fly replay routes the request here from the fleet router; the app lives at root
1592
+ // so the full path is forwarded unchanged (no basePath prefix needed).
1593
+ const devPort = parseDevSubdomainPort(req);
1594
+ if (devPort !== null) {
1595
+ ideLastProxiedActivity = Date.now();
1596
+ const devProxyOptions = {
1597
+ hostname: '127.0.0.1',
1598
+ port: devPort,
1599
+ path: url.pathname + (url.search || ''),
1600
+ method: req.method,
1601
+ headers: { ...req.headers, host: `127.0.0.1:${devPort}` },
1602
+ };
1603
+ const devProxyReq = httpRequest(devProxyOptions, (devProxyRes) => {
1604
+ res.writeHead(devProxyRes.statusCode || 502, devProxyRes.headers);
1605
+ devProxyRes.pipe(res);
1606
+ });
1607
+ devProxyReq.on('error', () => {
1608
+ if (!res.headersSent) {
1609
+ res.writeHead(502);
1610
+ res.end('Dev port proxy error');
1611
+ }
1612
+ });
1613
+ req.pipe(devProxyReq);
1614
+ return;
1615
+ }
1531
1616
  // ── Port proxy: /proxy/PORT/* β†’ localhost:PORT/proxy/PORT/* ─────────────
1532
1617
  // Forwards to a local dev server while PRESERVING the full path so that
1533
1618
  // basePath-aware apps (e.g. Next.js with basePath: '/proxy/3000') receive
@@ -1536,7 +1621,7 @@ function startApiServer(workingDir, port) {
1536
1621
  const portProxyMatch = url.pathname.match(/^\/proxy\/(\d+)(\/.*)?$/);
1537
1622
  if (portProxyMatch) {
1538
1623
  const targetPort = parseInt(portProxyMatch[1]);
1539
- if (targetPort > 1024 && targetPort < 65536 && targetPort !== 8300 && targetPort !== parseInt(process.env.OSBORN_API_PORT || '8741', 10)) {
1624
+ if (targetPort > 1024 && targetPort < 65536 && targetPort !== 8300 && targetPort !== OSBORN_API_PORT) {
1540
1625
  const portProxyOptions = {
1541
1626
  hostname: '127.0.0.1',
1542
1627
  port: targetPort,
@@ -1635,6 +1720,13 @@ function startApiServer(workingDir, port) {
1635
1720
  // β€” this must work for the integrated terminal to connect.
1636
1721
  // Reject all other upgrades (meeting audio moved to polling in 0.9.xx).
1637
1722
  server.on('upgrade', (req, socket, head) => {
1723
+ // Dev-domain subdomain WebSocket proxy (same pattern as HTTP handler above)
1724
+ const wsDevPort = parseDevSubdomainPort(req);
1725
+ if (wsDevPort !== null) {
1726
+ ideLastProxiedActivity = Date.now();
1727
+ wsProxy.ws(req, socket, head, { target: `http://127.0.0.1:${wsDevPort}` });
1728
+ return;
1729
+ }
1638
1730
  const upgradePath = new URL(req.url || '/', `http://localhost`).pathname;
1639
1731
  const isAgentRoute = AGENT_ROUTE_PREFIXES.some(p => upgradePath === p || upgradePath.startsWith(p + '/'));
1640
1732
  if (ideProxyEnabled && hasValidIdeCookie(req) && !isAgentRoute) {
@@ -5137,7 +5229,7 @@ async function main() {
5137
5229
  let readySent = false;
5138
5230
  const provider = sessionVoiceMode === 'realtime' ? realtimeConfig.provider : 'claude';
5139
5231
  // Fetch full session list for startup session browser (all Claude projects)
5140
- const allSessions = await listAllClaudeSessions(50);
5232
+ const allSessions = await listAllClaudeSessions();
5141
5233
  const recentSessionId = allSessions.length > 0 ? allSessions[0].sessionId : null;
5142
5234
  const hasRecentSession = allSessions.length > 0;
5143
5235
  // Prepare sessions for frontend (up to 50)
@@ -5391,7 +5483,7 @@ async function main() {
5391
5483
  // List available sessions across all Claude projects
5392
5484
  console.log('πŸ“‹ Listing available sessions (all projects)...');
5393
5485
  try {
5394
- const sessions = await listAllClaudeSessions(100);
5486
+ const sessions = await listAllClaudeSessions();
5395
5487
  await sendToFrontend({
5396
5488
  type: 'sessions_list',
5397
5489
  // See `/sessions` HTTP handler β€” baseCwd is the agent's
@@ -6220,7 +6312,7 @@ async function main() {
6220
6312
  let resolvedSessionId = targetSessionId;
6221
6313
  if (!resolvedSessionId) {
6222
6314
  // Pick the most recently modified session, excluding the focused one.
6223
- const allSess = await listAllClaudeSessions(50);
6315
+ const allSess = await listAllClaudeSessions();
6224
6316
  const focusedId = currentLLM?.sessionId || currentResumeSessionId || null;
6225
6317
  const candidate = allSess.find(s => s.sessionId !== focusedId);
6226
6318
  resolvedSessionId = candidate?.sessionId ?? undefined;
@@ -6294,8 +6386,9 @@ async function main() {
6294
6386
  // Process exited between readdir and read β€” skip it
6295
6387
  }
6296
6388
  }
6297
- // Sort by resident memory descending, cap at 40 entries
6389
+ // Sort by resident memory descending, cap table at 40 but keep real total
6298
6390
  processes.sort((a, b) => b.rssMb - a.rssMb);
6391
+ const totalCount = processes.length;
6299
6392
  const topProcesses = processes.slice(0, 40);
6300
6393
  // Reuse the existing os module memory totals (same source as /health endpoint)
6301
6394
  const totalMb = Math.round(totalmem() / 1024 / 1024);
@@ -6304,6 +6397,7 @@ async function main() {
6304
6397
  await sendToFrontend({
6305
6398
  type: 'process_list',
6306
6399
  processes: topProcesses,
6400
+ totalCount,
6307
6401
  memory: { usedMb, totalMb, freeMb },
6308
6402
  });
6309
6403
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "osborn",
3
- "version": "0.9.155",
3
+ "version": "0.9.159",
4
4
  "description": "Voice AI coding assistant - local agent that connects to Osborn frontend",
5
5
  "type": "module",
6
6
  "bin": {