vibora 1.6.0 → 1.7.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,15 @@ 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
+ diskUsedBytes: integer("disk_used_bytes").notNull(),
7736
+ diskTotalBytes: integer("disk_total_bytes").notNull()
7737
+ });
7728
7738
 
7729
7739
  // server/db/index.ts
7730
7740
  initializeViboraDirectories();
@@ -137940,9 +137950,9 @@ app2.delete("/bulk", async (c) => {
137940
137950
  continue;
137941
137951
  if (existing.worktreePath) {
137942
137952
  destroyTerminalsForWorktree(existing.worktreePath);
137943
- }
137944
- if (existing.worktreePath && existing.repoPath) {
137945
- deleteGitWorktree(existing.repoPath, existing.worktreePath);
137953
+ if (body.deleteLinkedWorktrees && existing.repoPath) {
137954
+ deleteGitWorktree(existing.repoPath, existing.worktreePath);
137955
+ }
137946
137956
  }
137947
137957
  const columnTasks = db.select().from(tasks).where(eq(tasks.status, existing.status)).all();
137948
137958
  for (const t of columnTasks) {
@@ -137996,15 +138006,16 @@ app2.patch("/:id", async (c) => {
137996
138006
  });
137997
138007
  app2.delete("/:id", (c) => {
137998
138008
  const id = c.req.param("id");
138009
+ const deleteLinkedWorktree = c.req.query("deleteLinkedWorktree") === "true";
137999
138010
  const existing = db.select().from(tasks).where(eq(tasks.id, id)).get();
138000
138011
  if (!existing) {
138001
138012
  return c.json({ error: "Task not found" }, 404);
138002
138013
  }
138003
138014
  if (existing.worktreePath) {
138004
138015
  destroyTerminalsForWorktree(existing.worktreePath);
138005
- }
138006
- if (existing.worktreePath && existing.repoPath) {
138007
- deleteGitWorktree(existing.repoPath, existing.worktreePath);
138016
+ if (deleteLinkedWorktree && existing.repoPath) {
138017
+ deleteGitWorktree(existing.repoPath, existing.worktreePath);
138018
+ }
138008
138019
  }
138009
138020
  const columnTasks = db.select().from(tasks).where(eq(tasks.status, existing.status)).all();
138010
138021
  const now = new Date().toISOString();
@@ -139504,15 +139515,19 @@ app7.delete("/", async (c) => {
139504
139515
  await deleteWorktree(body.worktreePath, body.repoPath || linkedTask?.repoPath);
139505
139516
  let deletedTaskId;
139506
139517
  if (linkedTask) {
139507
- const columnTasks = db.select().from(tasks).where(eq(tasks.status, linkedTask.status)).all();
139508
139518
  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();
139519
+ if (body.deleteLinkedTask) {
139520
+ const columnTasks = db.select().from(tasks).where(eq(tasks.status, linkedTask.status)).all();
139521
+ for (const t of columnTasks) {
139522
+ if (t.position > linkedTask.position) {
139523
+ db.update(tasks).set({ position: t.position - 1, updatedAt: now }).where(eq(tasks.id, t.id)).run();
139524
+ }
139512
139525
  }
139526
+ db.delete(tasks).where(eq(tasks.id, linkedTask.id)).run();
139527
+ deletedTaskId = linkedTask.id;
139528
+ } else {
139529
+ db.update(tasks).set({ worktreePath: null, updatedAt: now }).where(eq(tasks.id, linkedTask.id)).run();
139513
139530
  }
139514
- db.delete(tasks).where(eq(tasks.id, linkedTask.id)).run();
139515
- deletedTaskId = linkedTask.id;
139516
139531
  }
139517
139532
  return c.json({ success: true, path: body.worktreePath, deletedTaskId });
139518
139533
  } catch (err) {
@@ -143434,6 +143449,355 @@ app12.get("/check", async (c) => {
143434
143449
  });
143435
143450
  var auth_default = app12;
143436
143451
 
143452
+ // server/routes/monitoring.ts
143453
+ import { readdirSync as readdirSync7, readFileSync as readFileSync7, readlinkSync as readlinkSync2 } from "fs";
143454
+ import { execSync as execSync5 } from "child_process";
143455
+
143456
+ // server/services/metrics-collector.ts
143457
+ import os5 from "os";
143458
+ import { execSync as execSync4 } from "child_process";
143459
+ var COLLECT_INTERVAL = 5000;
143460
+ var RETENTION_HOURS = 24;
143461
+ var intervalId = null;
143462
+ var previousCpu = null;
143463
+ function getCpuSnapshot() {
143464
+ const cpus = os5.cpus();
143465
+ let idle = 0;
143466
+ let total = 0;
143467
+ for (const cpu of cpus) {
143468
+ idle += cpu.times.idle;
143469
+ total += cpu.times.user + cpu.times.nice + cpu.times.sys + cpu.times.idle + cpu.times.irq;
143470
+ }
143471
+ return { idle, total };
143472
+ }
143473
+ function calculateCpuPercent() {
143474
+ const current = getCpuSnapshot();
143475
+ if (!previousCpu) {
143476
+ previousCpu = current;
143477
+ return 0;
143478
+ }
143479
+ const idleDiff = current.idle - previousCpu.idle;
143480
+ const totalDiff = current.total - previousCpu.total;
143481
+ previousCpu = current;
143482
+ if (totalDiff === 0)
143483
+ return 0;
143484
+ const usedPercent = (totalDiff - idleDiff) / totalDiff * 100;
143485
+ return Math.round(usedPercent * 100) / 100;
143486
+ }
143487
+ function getDiskUsage() {
143488
+ try {
143489
+ const output = execSync4("df -B1 / | tail -1", {
143490
+ encoding: "utf-8",
143491
+ timeout: 5000,
143492
+ stdio: ["pipe", "pipe", "pipe"]
143493
+ });
143494
+ const parts = output.trim().split(/\s+/);
143495
+ if (parts.length >= 4) {
143496
+ const total = parseInt(parts[1], 10) || 0;
143497
+ const used = parseInt(parts[2], 10) || 0;
143498
+ return { used, total };
143499
+ }
143500
+ } catch (err) {
143501
+ console.error("Failed to get disk usage:", err);
143502
+ }
143503
+ return { used: 0, total: 0 };
143504
+ }
143505
+ function collectMetrics() {
143506
+ const timestamp = Math.floor(Date.now() / 1000);
143507
+ const cpuPercent = calculateCpuPercent();
143508
+ const memoryTotal = os5.totalmem();
143509
+ const memoryFree = os5.freemem();
143510
+ const memoryUsed = memoryTotal - memoryFree;
143511
+ const disk = getDiskUsage();
143512
+ db.insert(systemMetrics).values({
143513
+ timestamp,
143514
+ cpuPercent,
143515
+ memoryUsedBytes: memoryUsed,
143516
+ memoryTotalBytes: memoryTotal,
143517
+ diskUsedBytes: disk.used,
143518
+ diskTotalBytes: disk.total
143519
+ }).run();
143520
+ }
143521
+ function pruneOldMetrics() {
143522
+ const cutoff = Math.floor(Date.now() / 1000) - RETENTION_HOURS * 60 * 60;
143523
+ const result = db.delete(systemMetrics).where(lt(systemMetrics.timestamp, cutoff)).run();
143524
+ if (result.changes > 0) {
143525
+ console.log(`[MetricsCollector] Pruned ${result.changes} old metrics records`);
143526
+ }
143527
+ }
143528
+ function startMetricsCollector() {
143529
+ if (intervalId)
143530
+ return;
143531
+ console.log(`Metrics collector started (${COLLECT_INTERVAL / 1000}s interval)`);
143532
+ previousCpu = getCpuSnapshot();
143533
+ setTimeout(() => {
143534
+ collectMetrics();
143535
+ }, 1000);
143536
+ intervalId = setInterval(() => {
143537
+ collectMetrics();
143538
+ }, COLLECT_INTERVAL);
143539
+ setInterval(() => {
143540
+ pruneOldMetrics();
143541
+ }, 60 * 60 * 1000);
143542
+ pruneOldMetrics();
143543
+ }
143544
+ function stopMetricsCollector() {
143545
+ if (intervalId) {
143546
+ clearInterval(intervalId);
143547
+ intervalId = null;
143548
+ console.log("Metrics collector stopped");
143549
+ }
143550
+ }
143551
+ function getMetrics(windowSeconds) {
143552
+ const cutoff = Math.floor(Date.now() / 1000) - windowSeconds;
143553
+ const rows = db.select().from(systemMetrics).where(lt(systemMetrics.timestamp, cutoff) ? undefined : undefined).all().filter((r) => r.timestamp >= cutoff);
143554
+ return rows.map((row) => ({
143555
+ timestamp: row.timestamp,
143556
+ cpuPercent: row.cpuPercent,
143557
+ memoryUsedPercent: row.memoryTotalBytes > 0 ? row.memoryUsedBytes / row.memoryTotalBytes * 100 : 0,
143558
+ diskUsedPercent: row.diskTotalBytes > 0 ? row.diskUsedBytes / row.diskTotalBytes * 100 : 0
143559
+ }));
143560
+ }
143561
+ function getCurrentMetrics() {
143562
+ const memoryTotal = os5.totalmem();
143563
+ const memoryFree = os5.freemem();
143564
+ const memoryUsed = memoryTotal - memoryFree;
143565
+ const disk = getDiskUsage();
143566
+ const latest = db.select().from(systemMetrics).orderBy(systemMetrics.timestamp).limit(1).all();
143567
+ return {
143568
+ cpu: latest.length > 0 ? latest[0].cpuPercent : 0,
143569
+ memory: {
143570
+ total: memoryTotal,
143571
+ used: memoryUsed,
143572
+ usedPercent: memoryUsed / memoryTotal * 100
143573
+ },
143574
+ disk: {
143575
+ total: disk.total,
143576
+ used: disk.used,
143577
+ usedPercent: disk.total > 0 ? disk.used / disk.total * 100 : 0,
143578
+ path: "/"
143579
+ }
143580
+ };
143581
+ }
143582
+
143583
+ // server/routes/monitoring.ts
143584
+ function parseWindow(window) {
143585
+ const match3 = window.match(/^(\d+)(m|h)$/);
143586
+ if (!match3)
143587
+ return 3600;
143588
+ const value = parseInt(match3[1], 10);
143589
+ const unit = match3[2];
143590
+ if (unit === "m")
143591
+ return value * 60;
143592
+ if (unit === "h")
143593
+ return value * 3600;
143594
+ return 3600;
143595
+ }
143596
+ function findAllClaudeProcesses() {
143597
+ const claudeProcesses = [];
143598
+ try {
143599
+ const procDirs = readdirSync7("/proc").filter((d) => /^\d+$/.test(d));
143600
+ for (const pidStr of procDirs) {
143601
+ const pid = parseInt(pidStr, 10);
143602
+ try {
143603
+ const cmdline = readFileSync7(`/proc/${pid}/cmdline`, "utf-8");
143604
+ if (/\bclaude\b/i.test(cmdline)) {
143605
+ claudeProcesses.push({ pid, cmdline });
143606
+ }
143607
+ } catch {}
143608
+ }
143609
+ } catch {
143610
+ try {
143611
+ const result = execSync5("pgrep -f claude", { encoding: "utf-8" });
143612
+ for (const line of result.trim().split(`
143613
+ `)) {
143614
+ const pid = parseInt(line, 10);
143615
+ if (!isNaN(pid)) {
143616
+ try {
143617
+ const cmdline = execSync5(`ps -p ${pid} -o args=`, { encoding: "utf-8" }).trim();
143618
+ claudeProcesses.push({ pid, cmdline });
143619
+ } catch {
143620
+ claudeProcesses.push({ pid, cmdline: "claude" });
143621
+ }
143622
+ }
143623
+ }
143624
+ } catch {}
143625
+ }
143626
+ return claudeProcesses;
143627
+ }
143628
+ function getProcessCwd(pid) {
143629
+ try {
143630
+ return readlinkSync2(`/proc/${pid}/cwd`);
143631
+ } catch {
143632
+ try {
143633
+ const result = execSync5(`lsof -p ${pid} -d cwd -Fn 2>/dev/null | grep ^n | cut -c2-`, {
143634
+ encoding: "utf-8"
143635
+ });
143636
+ return result.trim() || "(unknown)";
143637
+ } catch {
143638
+ return "(unknown)";
143639
+ }
143640
+ }
143641
+ }
143642
+ function getProcessMemoryMB(pid) {
143643
+ try {
143644
+ const status = readFileSync7(`/proc/${pid}/status`, "utf-8");
143645
+ const match3 = status.match(/VmRSS:\s+(\d+)\s+kB/);
143646
+ return match3 ? parseInt(match3[1], 10) / 1024 : 0;
143647
+ } catch {
143648
+ try {
143649
+ const result = execSync5(`ps -o rss= -p ${pid}`, { encoding: "utf-8" });
143650
+ return parseInt(result.trim(), 10) / 1024;
143651
+ } catch {
143652
+ return 0;
143653
+ }
143654
+ }
143655
+ }
143656
+ function getProcessStartTime(pid) {
143657
+ try {
143658
+ const stat = readFileSync7(`/proc/${pid}/stat`, "utf-8");
143659
+ const fields = stat.split(" ");
143660
+ const starttime = parseInt(fields[21], 10);
143661
+ const uptime = parseFloat(readFileSync7("/proc/uptime", "utf-8").split(" ")[0]);
143662
+ const clockTicks = 100;
143663
+ const bootTime = Math.floor(Date.now() / 1000) - uptime;
143664
+ return Math.floor(bootTime + starttime / clockTicks);
143665
+ } catch {
143666
+ return null;
143667
+ }
143668
+ }
143669
+ function getDescendantPids2(pid) {
143670
+ const descendants = [];
143671
+ try {
143672
+ const result = execSync5(`ps --ppid ${pid} -o pid= 2>/dev/null || true`, { encoding: "utf-8" });
143673
+ for (const line of result.trim().split(`
143674
+ `)) {
143675
+ const childPid = parseInt(line.trim(), 10);
143676
+ if (!isNaN(childPid)) {
143677
+ descendants.push(childPid);
143678
+ descendants.push(...getDescendantPids2(childPid));
143679
+ }
143680
+ }
143681
+ } catch {}
143682
+ return descendants;
143683
+ }
143684
+ var monitoringRoutes = new Hono2;
143685
+ monitoringRoutes.get("/claude-instances", (c) => {
143686
+ const filter2 = c.req.query("filter") || "vibora";
143687
+ const allClaudeProcesses = findAllClaudeProcesses();
143688
+ const viboraManagedPids = new Map;
143689
+ try {
143690
+ const ptyManager2 = getPTYManager();
143691
+ const terminals2 = ptyManager2.listTerminals();
143692
+ const dtachService2 = getDtachService();
143693
+ for (const terminal of terminals2) {
143694
+ const socketPath = dtachService2.getSocketPath(terminal.id);
143695
+ try {
143696
+ const procDirs = readdirSync7("/proc").filter((d) => /^\d+$/.test(d));
143697
+ for (const pidStr of procDirs) {
143698
+ const pid = parseInt(pidStr, 10);
143699
+ try {
143700
+ const cmdline = readFileSync7(`/proc/${pid}/cmdline`, "utf-8");
143701
+ if (cmdline.includes(socketPath)) {
143702
+ const descendants = getDescendantPids2(pid);
143703
+ for (const descendantPid of [...descendants, pid]) {
143704
+ viboraManagedPids.set(descendantPid, {
143705
+ terminalId: terminal.id,
143706
+ terminalName: terminal.name,
143707
+ cwd: terminal.cwd
143708
+ });
143709
+ }
143710
+ }
143711
+ } catch {}
143712
+ }
143713
+ } catch {}
143714
+ }
143715
+ } catch {}
143716
+ const allTasks = db.select().from(tasks).all();
143717
+ const tasksByWorktree = new Map(allTasks.filter((t) => t.worktreePath).map((t) => [t.worktreePath, t]));
143718
+ const instances = [];
143719
+ for (const { pid } of allClaudeProcesses) {
143720
+ const viboraInfo = viboraManagedPids.get(pid);
143721
+ const isViboraManaged = !!viboraInfo;
143722
+ if (filter2 === "vibora" && !isViboraManaged) {
143723
+ continue;
143724
+ }
143725
+ const cwd = viboraInfo?.cwd || getProcessCwd(pid);
143726
+ const ramMB = Math.round(getProcessMemoryMB(pid) * 10) / 10;
143727
+ const startedAt = getProcessStartTime(pid);
143728
+ let taskId = null;
143729
+ let taskTitle = null;
143730
+ let worktreePath = null;
143731
+ if (viboraInfo) {
143732
+ const task = tasksByWorktree.get(viboraInfo.cwd);
143733
+ if (task) {
143734
+ taskId = task.id;
143735
+ taskTitle = task.title;
143736
+ worktreePath = viboraInfo.cwd;
143737
+ }
143738
+ }
143739
+ instances.push({
143740
+ pid,
143741
+ cwd,
143742
+ ramMB,
143743
+ startedAt,
143744
+ terminalId: viboraInfo?.terminalId || null,
143745
+ terminalName: viboraInfo?.terminalName || null,
143746
+ taskId,
143747
+ taskTitle,
143748
+ worktreePath,
143749
+ isViboraManaged
143750
+ });
143751
+ }
143752
+ instances.sort((a, b) => {
143753
+ if (a.isViboraManaged !== b.isViboraManaged) {
143754
+ return a.isViboraManaged ? -1 : 1;
143755
+ }
143756
+ return b.ramMB - a.ramMB;
143757
+ });
143758
+ return c.json(instances);
143759
+ });
143760
+ monitoringRoutes.get("/system-metrics", (c) => {
143761
+ const windowStr = c.req.query("window") || "1h";
143762
+ const windowSeconds = parseWindow(windowStr);
143763
+ const dataPoints = getMetrics(windowSeconds);
143764
+ const current = getCurrentMetrics();
143765
+ return c.json({
143766
+ window: windowStr,
143767
+ dataPoints,
143768
+ current
143769
+ });
143770
+ });
143771
+ monitoringRoutes.post("/claude-instances/:terminalId/kill", (c) => {
143772
+ const terminalId = c.req.param("terminalId");
143773
+ try {
143774
+ const dtachService2 = getDtachService();
143775
+ const killed = dtachService2.killClaudeInSession(terminalId);
143776
+ return c.json({ success: true, killed });
143777
+ } catch (err) {
143778
+ const message = err instanceof Error ? err.message : String(err);
143779
+ return c.json({ error: message }, 500);
143780
+ }
143781
+ });
143782
+ monitoringRoutes.post("/claude-instances/:pid/kill-pid", (c) => {
143783
+ const pidStr = c.req.param("pid");
143784
+ const pid = parseInt(pidStr, 10);
143785
+ if (isNaN(pid)) {
143786
+ return c.json({ error: "Invalid PID" }, 400);
143787
+ }
143788
+ try {
143789
+ const cmdline = readFileSync7(`/proc/${pid}/cmdline`, "utf-8");
143790
+ if (!/\bclaude\b/i.test(cmdline)) {
143791
+ return c.json({ error: "Process is not a Claude instance" }, 400);
143792
+ }
143793
+ process.kill(pid, "SIGTERM");
143794
+ return c.json({ success: true, killed: true });
143795
+ } catch (err) {
143796
+ const message = err instanceof Error ? err.message : String(err);
143797
+ return c.json({ error: message }, 500);
143798
+ }
143799
+ });
143800
+
143437
143801
  // server/app.ts
143438
143802
  function getDistPath() {
143439
143803
  if (process.env.VIBORA_PACKAGE_ROOT) {
@@ -143462,6 +143826,7 @@ function createApp() {
143462
143826
  app13.route("/api/linear", linear_default);
143463
143827
  app13.route("/api/github", github_default);
143464
143828
  app13.route("/api/auth", auth_default);
143829
+ app13.route("/api/monitoring", monitoringRoutes);
143465
143830
  if (process.env.VIBORA_PACKAGE_ROOT) {
143466
143831
  const distPath = getDistPath();
143467
143832
  const serveFile = async (filePath) => {
@@ -143514,7 +143879,7 @@ function createApp() {
143514
143879
  }
143515
143880
 
143516
143881
  // server/services/pr-monitor.ts
143517
- import { execSync as execSync4 } from "child_process";
143882
+ import { execSync as execSync6 } from "child_process";
143518
143883
  var POLL_INTERVAL = 60000;
143519
143884
  function parsePrUrl(url) {
143520
143885
  const match3 = url.match(/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)/);
@@ -143529,7 +143894,7 @@ function checkPrStatus(prUrl) {
143529
143894
  return null;
143530
143895
  }
143531
143896
  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"] });
143897
+ 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
143898
  const data = JSON.parse(output);
143534
143899
  return {
143535
143900
  state: data.state,
@@ -143555,20 +143920,20 @@ async function pollPRs() {
143555
143920
  }
143556
143921
  }
143557
143922
  }
143558
- var intervalId = null;
143923
+ var intervalId2 = null;
143559
143924
  function startPRMonitor() {
143560
- if (intervalId)
143925
+ if (intervalId2)
143561
143926
  return;
143562
143927
  console.log("PR Monitor started (60s interval)");
143563
143928
  pollPRs().catch(console.error);
143564
- intervalId = setInterval(() => {
143929
+ intervalId2 = setInterval(() => {
143565
143930
  pollPRs().catch(console.error);
143566
143931
  }, POLL_INTERVAL);
143567
143932
  }
143568
143933
  function stopPRMonitor() {
143569
- if (intervalId) {
143570
- clearInterval(intervalId);
143571
- intervalId = null;
143934
+ if (intervalId2) {
143935
+ clearInterval(intervalId2);
143936
+ intervalId2 = null;
143572
143937
  console.log("PR Monitor stopped");
143573
143938
  }
143574
143939
  }
@@ -143611,10 +143976,12 @@ var server = serve({
143611
143976
  });
143612
143977
  injectWebSocket(server);
143613
143978
  startPRMonitor();
143979
+ startMetricsCollector();
143614
143980
  process.on("SIGINT", () => {
143615
143981
  console.log(`
143616
143982
  Shutting down (terminals will persist)...`);
143617
143983
  stopPRMonitor();
143984
+ stopMetricsCollector();
143618
143985
  ptyManager2.detachAll();
143619
143986
  server.close();
143620
143987
  process.exit(0);
@@ -143623,6 +143990,7 @@ process.on("SIGTERM", () => {
143623
143990
  console.log(`
143624
143991
  Shutting down (terminals will persist)...`);
143625
143992
  stopPRMonitor();
143993
+ stopMetricsCollector();
143626
143994
  ptyManager2.detachAll();
143627
143995
  server.close();
143628
143996
  process.exit(0);