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/bin/vibora.js +11 -8
- package/dist/assets/index-BxOt05pH.css +1 -0
- package/dist/assets/index-DDBvX7Ij.css +1 -0
- package/dist/assets/index-DP-OAlLv.js +116 -0
- package/dist/assets/index-DS5B_viR.js +116 -0
- package/dist/index.html +2 -2
- package/drizzle/0004_dear_beyonder.sql +9 -0
- package/drizzle/0005_zippy_professor_monster.sql +1 -0
- package/drizzle/meta/0004_snapshot.json +464 -0
- package/drizzle/meta/0005_snapshot.json +472 -0
- package/drizzle/meta/_journal.json +14 -0
- package/package.json +1 -1
- package/server/index.js +577 -20
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
|
-
|
|
137945
|
-
|
|
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
|
-
|
|
138007
|
-
|
|
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
|
-
|
|
139510
|
-
|
|
139511
|
-
|
|
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
|
|
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 =
|
|
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
|
|
144112
|
+
var intervalId2 = null;
|
|
143559
144113
|
function startPRMonitor() {
|
|
143560
|
-
if (
|
|
144114
|
+
if (intervalId2)
|
|
143561
144115
|
return;
|
|
143562
144116
|
console.log("PR Monitor started (60s interval)");
|
|
143563
144117
|
pollPRs().catch(console.error);
|
|
143564
|
-
|
|
144118
|
+
intervalId2 = setInterval(() => {
|
|
143565
144119
|
pollPRs().catch(console.error);
|
|
143566
144120
|
}, POLL_INTERVAL);
|
|
143567
144121
|
}
|
|
143568
144122
|
function stopPRMonitor() {
|
|
143569
|
-
if (
|
|
143570
|
-
clearInterval(
|
|
143571
|
-
|
|
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);
|