vibora 1.6.0 → 1.8.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/server/index.js CHANGED
@@ -7667,6 +7667,7 @@ __export(exports_schema, {
7667
7667
  terminalViewState: () => terminalViewState,
7668
7668
  terminalTabs: () => terminalTabs,
7669
7669
  tasks: () => tasks,
7670
+ systemMetrics: () => systemMetrics,
7670
7671
  repositories: () => repositories
7671
7672
  });
7672
7673
  var tasks = sqliteTable("tasks", {
@@ -7725,6 +7726,16 @@ var repositories = sqliteTable("repositories", {
7725
7726
  createdAt: text("created_at").notNull(),
7726
7727
  updatedAt: text("updated_at").notNull()
7727
7728
  });
7729
+ var systemMetrics = sqliteTable("system_metrics", {
7730
+ id: integer("id").primaryKey({ autoIncrement: true }),
7731
+ timestamp: integer("timestamp").notNull(),
7732
+ cpuPercent: real("cpu_percent").notNull(),
7733
+ memoryUsedBytes: integer("memory_used_bytes").notNull(),
7734
+ memoryTotalBytes: integer("memory_total_bytes").notNull(),
7735
+ memoryCacheBytes: integer("memory_cache_bytes").notNull().default(0),
7736
+ diskUsedBytes: integer("disk_used_bytes").notNull(),
7737
+ diskTotalBytes: integer("disk_total_bytes").notNull()
7738
+ });
7728
7739
 
7729
7740
  // server/db/index.ts
7730
7741
  initializeViboraDirectories();
@@ -137940,9 +137951,9 @@ app2.delete("/bulk", async (c) => {
137940
137951
  continue;
137941
137952
  if (existing.worktreePath) {
137942
137953
  destroyTerminalsForWorktree(existing.worktreePath);
137943
- }
137944
- if (existing.worktreePath && existing.repoPath) {
137945
- deleteGitWorktree(existing.repoPath, existing.worktreePath);
137954
+ if (body.deleteLinkedWorktrees && existing.repoPath) {
137955
+ deleteGitWorktree(existing.repoPath, existing.worktreePath);
137956
+ }
137946
137957
  }
137947
137958
  const columnTasks = db.select().from(tasks).where(eq(tasks.status, existing.status)).all();
137948
137959
  for (const t of columnTasks) {
@@ -137996,15 +138007,16 @@ app2.patch("/:id", async (c) => {
137996
138007
  });
137997
138008
  app2.delete("/:id", (c) => {
137998
138009
  const id = c.req.param("id");
138010
+ const deleteLinkedWorktree = c.req.query("deleteLinkedWorktree") === "true";
137999
138011
  const existing = db.select().from(tasks).where(eq(tasks.id, id)).get();
138000
138012
  if (!existing) {
138001
138013
  return c.json({ error: "Task not found" }, 404);
138002
138014
  }
138003
138015
  if (existing.worktreePath) {
138004
138016
  destroyTerminalsForWorktree(existing.worktreePath);
138005
- }
138006
- if (existing.worktreePath && existing.repoPath) {
138007
- deleteGitWorktree(existing.repoPath, existing.worktreePath);
138017
+ if (deleteLinkedWorktree && existing.repoPath) {
138018
+ deleteGitWorktree(existing.repoPath, existing.worktreePath);
138019
+ }
138008
138020
  }
138009
138021
  const columnTasks = db.select().from(tasks).where(eq(tasks.status, existing.status)).all();
138010
138022
  const now = new Date().toISOString();
@@ -139504,15 +139516,19 @@ app7.delete("/", async (c) => {
139504
139516
  await deleteWorktree(body.worktreePath, body.repoPath || linkedTask?.repoPath);
139505
139517
  let deletedTaskId;
139506
139518
  if (linkedTask) {
139507
- const columnTasks = db.select().from(tasks).where(eq(tasks.status, linkedTask.status)).all();
139508
139519
  const now = new Date().toISOString();
139509
- for (const t of columnTasks) {
139510
- if (t.position > linkedTask.position) {
139511
- db.update(tasks).set({ position: t.position - 1, updatedAt: now }).where(eq(tasks.id, t.id)).run();
139520
+ if (body.deleteLinkedTask) {
139521
+ const columnTasks = db.select().from(tasks).where(eq(tasks.status, linkedTask.status)).all();
139522
+ for (const t of columnTasks) {
139523
+ if (t.position > linkedTask.position) {
139524
+ db.update(tasks).set({ position: t.position - 1, updatedAt: now }).where(eq(tasks.id, t.id)).run();
139525
+ }
139512
139526
  }
139527
+ db.delete(tasks).where(eq(tasks.id, linkedTask.id)).run();
139528
+ deletedTaskId = linkedTask.id;
139529
+ } else {
139530
+ db.update(tasks).set({ worktreePath: null, updatedAt: now }).where(eq(tasks.id, linkedTask.id)).run();
139513
139531
  }
139514
- db.delete(tasks).where(eq(tasks.id, linkedTask.id)).run();
139515
- deletedTaskId = linkedTask.id;
139516
139532
  }
139517
139533
  return c.json({ success: true, path: body.worktreePath, deletedTaskId });
139518
139534
  } catch (err) {
@@ -143434,6 +143450,543 @@ app12.get("/check", async (c) => {
143434
143450
  });
143435
143451
  var auth_default = app12;
143436
143452
 
143453
+ // server/routes/monitoring.ts
143454
+ import { readdirSync as readdirSync7, readFileSync as readFileSync7, readlinkSync as readlinkSync2 } from "fs";
143455
+ import { execSync as execSync5 } from "child_process";
143456
+
143457
+ // server/services/metrics-collector.ts
143458
+ import os5 from "os";
143459
+ import fs7 from "fs";
143460
+ import { execSync as execSync4 } from "child_process";
143461
+ var COLLECT_INTERVAL = 5000;
143462
+ var RETENTION_HOURS = 24;
143463
+ function getMemoryInfo() {
143464
+ const total = os5.totalmem();
143465
+ try {
143466
+ const meminfo = fs7.readFileSync("/proc/meminfo", "utf-8");
143467
+ const values = {};
143468
+ for (const line of meminfo.split(`
143469
+ `)) {
143470
+ const match3 = line.match(/^(\w+):\s+(\d+)\s+kB/);
143471
+ if (match3) {
143472
+ values[match3[1]] = parseInt(match3[2], 10) * 1024;
143473
+ }
143474
+ }
143475
+ const memTotal = values["MemTotal"] || total;
143476
+ const memFree = values["MemFree"] || 0;
143477
+ const buffers = values["Buffers"] || 0;
143478
+ const cached = values["Cached"] || 0;
143479
+ const sReclaimable = values["SReclaimable"] || 0;
143480
+ const shmem = values["Shmem"] || 0;
143481
+ let cacheBuffers = buffers + cached + sReclaimable - shmem;
143482
+ if (cacheBuffers < 0) {
143483
+ cacheBuffers = 0;
143484
+ }
143485
+ const used = memTotal - memFree - buffers - cached - sReclaimable + shmem;
143486
+ return {
143487
+ total: memTotal,
143488
+ used: Math.max(used, 0),
143489
+ cache: cacheBuffers
143490
+ };
143491
+ } catch {
143492
+ const free = os5.freemem();
143493
+ return {
143494
+ total,
143495
+ used: total - free,
143496
+ cache: 0
143497
+ };
143498
+ }
143499
+ }
143500
+ var intervalId = null;
143501
+ var previousCpu = null;
143502
+ function getCpuSnapshot() {
143503
+ const cpus = os5.cpus();
143504
+ let idle = 0;
143505
+ let total = 0;
143506
+ for (const cpu of cpus) {
143507
+ idle += cpu.times.idle;
143508
+ total += cpu.times.user + cpu.times.nice + cpu.times.sys + cpu.times.idle + cpu.times.irq;
143509
+ }
143510
+ return { idle, total };
143511
+ }
143512
+ function calculateCpuPercent() {
143513
+ const current = getCpuSnapshot();
143514
+ if (!previousCpu) {
143515
+ previousCpu = current;
143516
+ return 0;
143517
+ }
143518
+ const idleDiff = current.idle - previousCpu.idle;
143519
+ const totalDiff = current.total - previousCpu.total;
143520
+ previousCpu = current;
143521
+ if (totalDiff === 0)
143522
+ return 0;
143523
+ const usedPercent = (totalDiff - idleDiff) / totalDiff * 100;
143524
+ return Math.round(usedPercent * 100) / 100;
143525
+ }
143526
+ function getDiskUsage() {
143527
+ try {
143528
+ const output = execSync4("df -B1 / | tail -1", {
143529
+ encoding: "utf-8",
143530
+ timeout: 5000,
143531
+ stdio: ["pipe", "pipe", "pipe"]
143532
+ });
143533
+ const parts = output.trim().split(/\s+/);
143534
+ if (parts.length >= 4) {
143535
+ const total = parseInt(parts[1], 10) || 0;
143536
+ const used = parseInt(parts[2], 10) || 0;
143537
+ return { used, total };
143538
+ }
143539
+ } catch (err) {
143540
+ console.error("Failed to get disk usage:", err);
143541
+ }
143542
+ return { used: 0, total: 0 };
143543
+ }
143544
+ function collectMetrics() {
143545
+ const timestamp = Math.floor(Date.now() / 1000);
143546
+ const cpuPercent = calculateCpuPercent();
143547
+ const memory = getMemoryInfo();
143548
+ const disk = getDiskUsage();
143549
+ db.insert(systemMetrics).values({
143550
+ timestamp,
143551
+ cpuPercent,
143552
+ memoryUsedBytes: memory.used,
143553
+ memoryTotalBytes: memory.total,
143554
+ memoryCacheBytes: memory.cache,
143555
+ diskUsedBytes: disk.used,
143556
+ diskTotalBytes: disk.total
143557
+ }).run();
143558
+ }
143559
+ function pruneOldMetrics() {
143560
+ const cutoff = Math.floor(Date.now() / 1000) - RETENTION_HOURS * 60 * 60;
143561
+ const result = db.delete(systemMetrics).where(lt(systemMetrics.timestamp, cutoff)).run();
143562
+ if (result.changes > 0) {
143563
+ console.log(`[MetricsCollector] Pruned ${result.changes} old metrics records`);
143564
+ }
143565
+ }
143566
+ function startMetricsCollector() {
143567
+ if (intervalId)
143568
+ return;
143569
+ console.log(`Metrics collector started (${COLLECT_INTERVAL / 1000}s interval)`);
143570
+ previousCpu = getCpuSnapshot();
143571
+ setTimeout(() => {
143572
+ collectMetrics();
143573
+ }, 1000);
143574
+ intervalId = setInterval(() => {
143575
+ collectMetrics();
143576
+ }, COLLECT_INTERVAL);
143577
+ setInterval(() => {
143578
+ pruneOldMetrics();
143579
+ }, 60 * 60 * 1000);
143580
+ pruneOldMetrics();
143581
+ }
143582
+ function stopMetricsCollector() {
143583
+ if (intervalId) {
143584
+ clearInterval(intervalId);
143585
+ intervalId = null;
143586
+ console.log("Metrics collector stopped");
143587
+ }
143588
+ }
143589
+ function getMetrics(windowSeconds) {
143590
+ const cutoff = Math.floor(Date.now() / 1000) - windowSeconds;
143591
+ const rows = db.select().from(systemMetrics).where(lt(systemMetrics.timestamp, cutoff) ? undefined : undefined).all().filter((r) => r.timestamp >= cutoff);
143592
+ return rows.map((row) => ({
143593
+ timestamp: row.timestamp,
143594
+ cpuPercent: row.cpuPercent,
143595
+ memoryUsedPercent: row.memoryTotalBytes > 0 ? row.memoryUsedBytes / row.memoryTotalBytes * 100 : 0,
143596
+ memoryCachePercent: row.memoryTotalBytes > 0 ? row.memoryCacheBytes / row.memoryTotalBytes * 100 : 0,
143597
+ diskUsedPercent: row.diskTotalBytes > 0 ? row.diskUsedBytes / row.diskTotalBytes * 100 : 0
143598
+ }));
143599
+ }
143600
+ function getCurrentMetrics() {
143601
+ const memory = getMemoryInfo();
143602
+ const disk = getDiskUsage();
143603
+ const latest = db.select().from(systemMetrics).orderBy(systemMetrics.timestamp).limit(1).all();
143604
+ return {
143605
+ cpu: latest.length > 0 ? latest[0].cpuPercent : 0,
143606
+ memory: {
143607
+ total: memory.total,
143608
+ used: memory.used,
143609
+ cache: memory.cache,
143610
+ usedPercent: memory.total > 0 ? memory.used / memory.total * 100 : 0,
143611
+ cachePercent: memory.total > 0 ? memory.cache / memory.total * 100 : 0
143612
+ },
143613
+ disk: {
143614
+ total: disk.total,
143615
+ used: disk.used,
143616
+ usedPercent: disk.total > 0 ? disk.used / disk.total * 100 : 0,
143617
+ path: "/"
143618
+ }
143619
+ };
143620
+ }
143621
+
143622
+ // server/routes/monitoring.ts
143623
+ function parseWindow(window) {
143624
+ const match3 = window.match(/^(\d+)(m|h)$/);
143625
+ if (!match3)
143626
+ return 3600;
143627
+ const value = parseInt(match3[1], 10);
143628
+ const unit = match3[2];
143629
+ if (unit === "m")
143630
+ return value * 60;
143631
+ if (unit === "h")
143632
+ return value * 3600;
143633
+ return 3600;
143634
+ }
143635
+ function findAllClaudeProcesses() {
143636
+ const claudeProcesses = [];
143637
+ try {
143638
+ const procDirs = readdirSync7("/proc").filter((d) => /^\d+$/.test(d));
143639
+ for (const pidStr of procDirs) {
143640
+ const pid = parseInt(pidStr, 10);
143641
+ try {
143642
+ const cmdline = readFileSync7(`/proc/${pid}/cmdline`, "utf-8");
143643
+ if (/\bclaude\b/i.test(cmdline)) {
143644
+ claudeProcesses.push({ pid, cmdline });
143645
+ }
143646
+ } catch {}
143647
+ }
143648
+ } catch {
143649
+ try {
143650
+ const result = execSync5("pgrep -f claude", { encoding: "utf-8" });
143651
+ for (const line of result.trim().split(`
143652
+ `)) {
143653
+ const pid = parseInt(line, 10);
143654
+ if (!isNaN(pid)) {
143655
+ try {
143656
+ const cmdline = execSync5(`ps -p ${pid} -o args=`, { encoding: "utf-8" }).trim();
143657
+ claudeProcesses.push({ pid, cmdline });
143658
+ } catch {
143659
+ claudeProcesses.push({ pid, cmdline: "claude" });
143660
+ }
143661
+ }
143662
+ }
143663
+ } catch {}
143664
+ }
143665
+ return claudeProcesses;
143666
+ }
143667
+ function getProcessCwd(pid) {
143668
+ try {
143669
+ return readlinkSync2(`/proc/${pid}/cwd`);
143670
+ } catch {
143671
+ try {
143672
+ const result = execSync5(`lsof -p ${pid} -d cwd -Fn 2>/dev/null | grep ^n | cut -c2-`, {
143673
+ encoding: "utf-8"
143674
+ });
143675
+ return result.trim() || "(unknown)";
143676
+ } catch {
143677
+ return "(unknown)";
143678
+ }
143679
+ }
143680
+ }
143681
+ function getProcessMemoryMB(pid) {
143682
+ try {
143683
+ const status = readFileSync7(`/proc/${pid}/status`, "utf-8");
143684
+ const match3 = status.match(/VmRSS:\s+(\d+)\s+kB/);
143685
+ return match3 ? parseInt(match3[1], 10) / 1024 : 0;
143686
+ } catch {
143687
+ try {
143688
+ const result = execSync5(`ps -o rss= -p ${pid}`, { encoding: "utf-8" });
143689
+ return parseInt(result.trim(), 10) / 1024;
143690
+ } catch {
143691
+ return 0;
143692
+ }
143693
+ }
143694
+ }
143695
+ function getProcessStartTime(pid) {
143696
+ try {
143697
+ const stat = readFileSync7(`/proc/${pid}/stat`, "utf-8");
143698
+ const fields = stat.split(" ");
143699
+ const starttime = parseInt(fields[21], 10);
143700
+ const uptime = parseFloat(readFileSync7("/proc/uptime", "utf-8").split(" ")[0]);
143701
+ const clockTicks = 100;
143702
+ const bootTime = Math.floor(Date.now() / 1000) - uptime;
143703
+ return Math.floor(bootTime + starttime / clockTicks);
143704
+ } catch {
143705
+ return null;
143706
+ }
143707
+ }
143708
+ function getDescendantPids2(pid) {
143709
+ const descendants = [];
143710
+ try {
143711
+ const result = execSync5(`ps --ppid ${pid} -o pid= 2>/dev/null || true`, { encoding: "utf-8" });
143712
+ for (const line of result.trim().split(`
143713
+ `)) {
143714
+ const childPid = parseInt(line.trim(), 10);
143715
+ if (!isNaN(childPid)) {
143716
+ descendants.push(childPid);
143717
+ descendants.push(...getDescendantPids2(childPid));
143718
+ }
143719
+ }
143720
+ } catch {}
143721
+ return descendants;
143722
+ }
143723
+ var monitoringRoutes = new Hono2;
143724
+ monitoringRoutes.get("/claude-instances", (c) => {
143725
+ const filter2 = c.req.query("filter") || "vibora";
143726
+ const allClaudeProcesses = findAllClaudeProcesses();
143727
+ const viboraManagedPids = new Map;
143728
+ try {
143729
+ const ptyManager2 = getPTYManager();
143730
+ const terminals2 = ptyManager2.listTerminals();
143731
+ const dtachService2 = getDtachService();
143732
+ for (const terminal of terminals2) {
143733
+ const socketPath = dtachService2.getSocketPath(terminal.id);
143734
+ try {
143735
+ const procDirs = readdirSync7("/proc").filter((d) => /^\d+$/.test(d));
143736
+ for (const pidStr of procDirs) {
143737
+ const pid = parseInt(pidStr, 10);
143738
+ try {
143739
+ const cmdline = readFileSync7(`/proc/${pid}/cmdline`, "utf-8");
143740
+ if (cmdline.includes(socketPath)) {
143741
+ const descendants = getDescendantPids2(pid);
143742
+ for (const descendantPid of [...descendants, pid]) {
143743
+ viboraManagedPids.set(descendantPid, {
143744
+ terminalId: terminal.id,
143745
+ terminalName: terminal.name,
143746
+ cwd: terminal.cwd
143747
+ });
143748
+ }
143749
+ }
143750
+ } catch {}
143751
+ }
143752
+ } catch {}
143753
+ }
143754
+ } catch {}
143755
+ const allTasks = db.select().from(tasks).all();
143756
+ const tasksByWorktree = new Map(allTasks.filter((t) => t.worktreePath).map((t) => [t.worktreePath, t]));
143757
+ const instances = [];
143758
+ for (const { pid } of allClaudeProcesses) {
143759
+ const viboraInfo = viboraManagedPids.get(pid);
143760
+ const isViboraManaged = !!viboraInfo;
143761
+ if (filter2 === "vibora" && !isViboraManaged) {
143762
+ continue;
143763
+ }
143764
+ const cwd = viboraInfo?.cwd || getProcessCwd(pid);
143765
+ const ramMB = Math.round(getProcessMemoryMB(pid) * 10) / 10;
143766
+ const startedAt = getProcessStartTime(pid);
143767
+ let taskId = null;
143768
+ let taskTitle = null;
143769
+ let worktreePath = null;
143770
+ if (viboraInfo) {
143771
+ const task = tasksByWorktree.get(viboraInfo.cwd);
143772
+ if (task) {
143773
+ taskId = task.id;
143774
+ taskTitle = task.title;
143775
+ worktreePath = viboraInfo.cwd;
143776
+ }
143777
+ }
143778
+ instances.push({
143779
+ pid,
143780
+ cwd,
143781
+ ramMB,
143782
+ startedAt,
143783
+ terminalId: viboraInfo?.terminalId || null,
143784
+ terminalName: viboraInfo?.terminalName || null,
143785
+ taskId,
143786
+ taskTitle,
143787
+ worktreePath,
143788
+ isViboraManaged
143789
+ });
143790
+ }
143791
+ instances.sort((a, b) => {
143792
+ if (a.isViboraManaged !== b.isViboraManaged) {
143793
+ return a.isViboraManaged ? -1 : 1;
143794
+ }
143795
+ return b.ramMB - a.ramMB;
143796
+ });
143797
+ return c.json(instances);
143798
+ });
143799
+ monitoringRoutes.get("/system-metrics", (c) => {
143800
+ const windowStr = c.req.query("window") || "1h";
143801
+ const windowSeconds = parseWindow(windowStr);
143802
+ const dataPoints = getMetrics(windowSeconds);
143803
+ const current = getCurrentMetrics();
143804
+ return c.json({
143805
+ window: windowStr,
143806
+ dataPoints,
143807
+ current
143808
+ });
143809
+ });
143810
+ monitoringRoutes.post("/claude-instances/:terminalId/kill", (c) => {
143811
+ const terminalId = c.req.param("terminalId");
143812
+ try {
143813
+ const dtachService2 = getDtachService();
143814
+ const killed = dtachService2.killClaudeInSession(terminalId);
143815
+ return c.json({ success: true, killed });
143816
+ } catch (err) {
143817
+ const message = err instanceof Error ? err.message : String(err);
143818
+ return c.json({ error: message }, 500);
143819
+ }
143820
+ });
143821
+ monitoringRoutes.post("/claude-instances/:pid/kill-pid", (c) => {
143822
+ const pidStr = c.req.param("pid");
143823
+ const pid = parseInt(pidStr, 10);
143824
+ if (isNaN(pid)) {
143825
+ return c.json({ error: "Invalid PID" }, 400);
143826
+ }
143827
+ try {
143828
+ const cmdline = readFileSync7(`/proc/${pid}/cmdline`, "utf-8");
143829
+ if (!/\bclaude\b/i.test(cmdline)) {
143830
+ return c.json({ error: "Process is not a Claude instance" }, 400);
143831
+ }
143832
+ process.kill(pid, "SIGTERM");
143833
+ return c.json({ success: true, killed: true });
143834
+ } catch (err) {
143835
+ const message = err instanceof Error ? err.message : String(err);
143836
+ return c.json({ error: message }, 500);
143837
+ }
143838
+ });
143839
+ monitoringRoutes.get("/top-processes", (c) => {
143840
+ const sortBy = c.req.query("sort") || "memory";
143841
+ const limit = parseInt(c.req.query("limit") || "10", 10);
143842
+ try {
143843
+ const memTotal = parseInt(readFileSync7("/proc/meminfo", "utf-8").match(/MemTotal:\s+(\d+)/)?.[1] || "0", 10) * 1024;
143844
+ const processes = [];
143845
+ const procDirs = readdirSync7("/proc").filter((d) => /^\d+$/.test(d));
143846
+ for (const pidStr of procDirs) {
143847
+ const pid = parseInt(pidStr, 10);
143848
+ try {
143849
+ const status = readFileSync7(`/proc/${pid}/status`, "utf-8");
143850
+ const nameMatch = status.match(/Name:\s+(.+)/);
143851
+ const rssMatch = status.match(/VmRSS:\s+(\d+)\s+kB/);
143852
+ if (!nameMatch || !rssMatch)
143853
+ continue;
143854
+ const name = nameMatch[1].trim();
143855
+ const memoryKB = parseInt(rssMatch[1], 10);
143856
+ const memoryMB = memoryKB / 1024;
143857
+ const memoryPercent = memTotal > 0 ? memoryKB * 1024 / memTotal * 100 : 0;
143858
+ let command = "";
143859
+ try {
143860
+ command = readFileSync7(`/proc/${pid}/cmdline`, "utf-8").replace(/\0/g, " ").trim().slice(0, 200);
143861
+ } catch {
143862
+ command = name;
143863
+ }
143864
+ const cpuPercent = 0;
143865
+ processes.push({
143866
+ pid,
143867
+ name,
143868
+ command,
143869
+ cpuPercent: Math.round(cpuPercent * 10) / 10,
143870
+ memoryMB: Math.round(memoryMB * 10) / 10,
143871
+ memoryPercent: Math.round(memoryPercent * 10) / 10
143872
+ });
143873
+ } catch {
143874
+ continue;
143875
+ }
143876
+ }
143877
+ if (sortBy === "cpu") {
143878
+ processes.sort((a, b) => b.cpuPercent - a.cpuPercent);
143879
+ } else {
143880
+ processes.sort((a, b) => b.memoryMB - a.memoryMB);
143881
+ }
143882
+ return c.json(processes.slice(0, limit));
143883
+ } catch {
143884
+ try {
143885
+ const sortFlag = sortBy === "cpu" ? "-pcpu" : "-rss";
143886
+ const result = execSync5(`ps -eo pid,comm,args,%cpu,rss --sort=${sortFlag} --no-headers | head -${limit + 1}`, { encoding: "utf-8", timeout: 5000 });
143887
+ const memTotal = parseInt(execSync5("grep MemTotal /proc/meminfo", { encoding: "utf-8" }).match(/(\d+)/)?.[1] || "0", 10) * 1024;
143888
+ const processes = [];
143889
+ for (const line of result.trim().split(`
143890
+ `)) {
143891
+ const match3 = line.match(/^\s*(\d+)\s+(\S+)\s+(.+?)\s+([\d.]+)\s+(\d+)\s*$/);
143892
+ if (match3) {
143893
+ const memoryKB = parseInt(match3[5], 10);
143894
+ processes.push({
143895
+ pid: parseInt(match3[1], 10),
143896
+ name: match3[2],
143897
+ command: match3[3].trim().slice(0, 200),
143898
+ cpuPercent: parseFloat(match3[4]),
143899
+ memoryMB: Math.round(memoryKB / 1024 * 10) / 10,
143900
+ memoryPercent: memTotal > 0 ? Math.round(memoryKB * 1024 / memTotal * 1000) / 10 : 0
143901
+ });
143902
+ }
143903
+ }
143904
+ return c.json(processes);
143905
+ } catch (fallbackErr) {
143906
+ const message = fallbackErr instanceof Error ? fallbackErr.message : String(fallbackErr);
143907
+ return c.json({ error: message }, 500);
143908
+ }
143909
+ }
143910
+ });
143911
+ monitoringRoutes.get("/docker-stats", (c) => {
143912
+ try {
143913
+ let result;
143914
+ let runtime = "docker";
143915
+ try {
143916
+ result = execSync5('docker stats --no-stream --format "{{json .}}"', {
143917
+ encoding: "utf-8",
143918
+ timeout: 1e4,
143919
+ stdio: ["pipe", "pipe", "pipe"]
143920
+ });
143921
+ } catch {
143922
+ try {
143923
+ result = execSync5('podman stats --no-stream --format "{{json .}}"', {
143924
+ encoding: "utf-8",
143925
+ timeout: 1e4,
143926
+ stdio: ["pipe", "pipe", "pipe"]
143927
+ });
143928
+ runtime = "podman";
143929
+ } catch {
143930
+ return c.json({ containers: [], available: false, runtime: null });
143931
+ }
143932
+ }
143933
+ const containers = [];
143934
+ for (const line of result.trim().split(`
143935
+ `)) {
143936
+ if (!line.trim())
143937
+ continue;
143938
+ try {
143939
+ const data = JSON.parse(line);
143940
+ const cpuStr = data.CPUPerc || "0%";
143941
+ const cpuPercent = parseFloat(cpuStr.replace("%", "")) || 0;
143942
+ const memUsageStr = data.MemUsage || "0B / 0B";
143943
+ const [usedStr, limitStr] = memUsageStr.split(" / ");
143944
+ const parseMemory = (str) => {
143945
+ const match3 = str.match(/([\d.]+)\s*(B|KB|KiB|MB|MiB|GB|GiB)/i);
143946
+ if (!match3)
143947
+ return 0;
143948
+ const value = parseFloat(match3[1]);
143949
+ const unit = match3[2].toLowerCase();
143950
+ switch (unit) {
143951
+ case "b":
143952
+ return value / (1024 * 1024);
143953
+ case "kb":
143954
+ case "kib":
143955
+ return value / 1024;
143956
+ case "mb":
143957
+ case "mib":
143958
+ return value;
143959
+ case "gb":
143960
+ case "gib":
143961
+ return value * 1024;
143962
+ default:
143963
+ return value;
143964
+ }
143965
+ };
143966
+ const memoryMB = parseMemory(usedStr);
143967
+ const memoryLimit = parseMemory(limitStr);
143968
+ const memPercStr = data.MemPerc || "0%";
143969
+ const memoryPercent = parseFloat(memPercStr.replace("%", "")) || 0;
143970
+ containers.push({
143971
+ id: (data.ID || data.Id || "").slice(0, 12),
143972
+ name: data.Name || data.Names || "unknown",
143973
+ cpuPercent: Math.round(cpuPercent * 10) / 10,
143974
+ memoryMB: Math.round(memoryMB * 10) / 10,
143975
+ memoryLimit: Math.round(memoryLimit * 10) / 10,
143976
+ memoryPercent: Math.round(memoryPercent * 10) / 10
143977
+ });
143978
+ } catch {
143979
+ continue;
143980
+ }
143981
+ }
143982
+ containers.sort((a, b) => b.memoryMB - a.memoryMB);
143983
+ return c.json({ containers, available: true, runtime });
143984
+ } catch (err) {
143985
+ const message = err instanceof Error ? err.message : String(err);
143986
+ return c.json({ error: message }, 500);
143987
+ }
143988
+ });
143989
+
143437
143990
  // server/app.ts
143438
143991
  function getDistPath() {
143439
143992
  if (process.env.VIBORA_PACKAGE_ROOT) {
@@ -143462,6 +144015,7 @@ function createApp() {
143462
144015
  app13.route("/api/linear", linear_default);
143463
144016
  app13.route("/api/github", github_default);
143464
144017
  app13.route("/api/auth", auth_default);
144018
+ app13.route("/api/monitoring", monitoringRoutes);
143465
144019
  if (process.env.VIBORA_PACKAGE_ROOT) {
143466
144020
  const distPath = getDistPath();
143467
144021
  const serveFile = async (filePath) => {
@@ -143514,7 +144068,7 @@ function createApp() {
143514
144068
  }
143515
144069
 
143516
144070
  // server/services/pr-monitor.ts
143517
- import { execSync as execSync4 } from "child_process";
144071
+ import { execSync as execSync6 } from "child_process";
143518
144072
  var POLL_INTERVAL = 60000;
143519
144073
  function parsePrUrl(url) {
143520
144074
  const match3 = url.match(/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)/);
@@ -143529,7 +144083,7 @@ function checkPrStatus(prUrl) {
143529
144083
  return null;
143530
144084
  }
143531
144085
  try {
143532
- const output = execSync4(`gh pr view ${parsed.number} --repo ${parsed.owner}/${parsed.repo} --json state,mergedAt`, { encoding: "utf-8", timeout: 1e4, stdio: ["pipe", "pipe", "pipe"] });
144086
+ const output = execSync6(`gh pr view ${parsed.number} --repo ${parsed.owner}/${parsed.repo} --json state,mergedAt`, { encoding: "utf-8", timeout: 1e4, stdio: ["pipe", "pipe", "pipe"] });
143533
144087
  const data = JSON.parse(output);
143534
144088
  return {
143535
144089
  state: data.state,
@@ -143555,20 +144109,20 @@ async function pollPRs() {
143555
144109
  }
143556
144110
  }
143557
144111
  }
143558
- var intervalId = null;
144112
+ var intervalId2 = null;
143559
144113
  function startPRMonitor() {
143560
- if (intervalId)
144114
+ if (intervalId2)
143561
144115
  return;
143562
144116
  console.log("PR Monitor started (60s interval)");
143563
144117
  pollPRs().catch(console.error);
143564
- intervalId = setInterval(() => {
144118
+ intervalId2 = setInterval(() => {
143565
144119
  pollPRs().catch(console.error);
143566
144120
  }, POLL_INTERVAL);
143567
144121
  }
143568
144122
  function stopPRMonitor() {
143569
- if (intervalId) {
143570
- clearInterval(intervalId);
143571
- intervalId = null;
144123
+ if (intervalId2) {
144124
+ clearInterval(intervalId2);
144125
+ intervalId2 = null;
143572
144126
  console.log("PR Monitor stopped");
143573
144127
  }
143574
144128
  }
@@ -143611,10 +144165,12 @@ var server = serve({
143611
144165
  });
143612
144166
  injectWebSocket(server);
143613
144167
  startPRMonitor();
144168
+ startMetricsCollector();
143614
144169
  process.on("SIGINT", () => {
143615
144170
  console.log(`
143616
144171
  Shutting down (terminals will persist)...`);
143617
144172
  stopPRMonitor();
144173
+ stopMetricsCollector();
143618
144174
  ptyManager2.detachAll();
143619
144175
  server.close();
143620
144176
  process.exit(0);
@@ -143623,6 +144179,7 @@ process.on("SIGTERM", () => {
143623
144179
  console.log(`
143624
144180
  Shutting down (terminals will persist)...`);
143625
144181
  stopPRMonitor();
144182
+ stopMetricsCollector();
143626
144183
  ptyManager2.detachAll();
143627
144184
  server.close();
143628
144185
  process.exit(0);