tmux-agent-monitor 0.0.20 → 0.0.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +237 -114
- package/dist/{src-CFxYk-pF.mjs → src-DoQ4vemC.mjs} +3 -0
- package/dist/tmux-agent-monitor-hook.js +1 -1
- package/dist/web/assets/index-BXEFFvF6.css +1 -0
- package/dist/web/assets/index-JAfMtLyc.js +55 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-CftL7Y2p.js +0 -55
- package/dist/web/assets/index-DMIClyUO.css +0 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { a as resolveServerKey, c as allowedKeys, i as resolveLogPaths, l as defaultConfig, n as configSchema, o as compileDangerPatterns, r as wsClientMessageSchema, s as isDangerousCommand, t as claudeHookEventSchema } from "./src-
|
|
2
|
+
import { a as resolveServerKey, c as allowedKeys, i as resolveLogPaths, l as defaultConfig, n as configSchema, o as compileDangerPatterns, r as wsClientMessageSchema, s as isDangerousCommand, t as claudeHookEventSchema } from "./src-DoQ4vemC.mjs";
|
|
3
3
|
import { serve } from "@hono/node-server";
|
|
4
4
|
import { execa } from "execa";
|
|
5
5
|
import qrcode from "qrcode-terminal";
|
|
@@ -8,6 +8,8 @@ import path from "node:path";
|
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
9
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
10
10
|
import { createNodeWebSocket } from "@hono/node-ws";
|
|
11
|
+
import { zValidator } from "@hono/zod-validator";
|
|
12
|
+
import { z } from "zod";
|
|
11
13
|
import { Hono } from "hono";
|
|
12
14
|
import crypto, { randomUUID } from "node:crypto";
|
|
13
15
|
import os, { networkInterfaces } from "node:os";
|
|
@@ -357,7 +359,7 @@ const nowIso$1 = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
|
357
359
|
const logCache = /* @__PURE__ */ new Map();
|
|
358
360
|
const detailCache = /* @__PURE__ */ new Map();
|
|
359
361
|
const fileCache$1 = /* @__PURE__ */ new Map();
|
|
360
|
-
const runGit$
|
|
362
|
+
const runGit$2 = async (cwd, args) => {
|
|
361
363
|
try {
|
|
362
364
|
return (await execFileAsync$3("git", [
|
|
363
365
|
"-C",
|
|
@@ -373,9 +375,9 @@ const runGit$1 = async (cwd, args) => {
|
|
|
373
375
|
throw err;
|
|
374
376
|
}
|
|
375
377
|
};
|
|
376
|
-
const resolveRepoRoot$
|
|
378
|
+
const resolveRepoRoot$2 = async (cwd) => {
|
|
377
379
|
try {
|
|
378
|
-
const trimmed = (await runGit$
|
|
380
|
+
const trimmed = (await runGit$2(cwd, ["rev-parse", "--show-toplevel"])).trim();
|
|
379
381
|
return trimmed.length > 0 ? trimmed : null;
|
|
380
382
|
} catch {
|
|
381
383
|
return null;
|
|
@@ -383,7 +385,7 @@ const resolveRepoRoot$1 = async (cwd) => {
|
|
|
383
385
|
};
|
|
384
386
|
const resolveHead = async (repoRoot) => {
|
|
385
387
|
try {
|
|
386
|
-
const trimmed = (await runGit$
|
|
388
|
+
const trimmed = (await runGit$2(repoRoot, ["rev-parse", "HEAD"])).trim();
|
|
387
389
|
return trimmed.length > 0 ? trimmed : null;
|
|
388
390
|
} catch {
|
|
389
391
|
return null;
|
|
@@ -497,7 +499,7 @@ const fetchCommitLog = async (cwd, options) => {
|
|
|
497
499
|
commits: [],
|
|
498
500
|
reason: "cwd_unknown"
|
|
499
501
|
};
|
|
500
|
-
const repoRoot = await resolveRepoRoot$
|
|
502
|
+
const repoRoot = await resolveRepoRoot$2(cwd);
|
|
501
503
|
if (!repoRoot) return {
|
|
502
504
|
repoRoot: null,
|
|
503
505
|
rev: null,
|
|
@@ -529,7 +531,7 @@ const fetchCommitLog = async (cwd, options) => {
|
|
|
529
531
|
FIELD_SEPARATOR,
|
|
530
532
|
"%b"
|
|
531
533
|
].join("");
|
|
532
|
-
const commits = parseCommitLogOutput(await runGit$
|
|
534
|
+
const commits = parseCommitLogOutput(await runGit$2(repoRoot, [
|
|
533
535
|
"log",
|
|
534
536
|
"-n",
|
|
535
537
|
String(limit),
|
|
@@ -567,7 +569,7 @@ const fetchCommitDetail = async (repoRoot, hash, options) => {
|
|
|
567
569
|
const nowMs = Date.now();
|
|
568
570
|
if (!options?.force && cached && nowMs - cached.at < DETAIL_TTL_MS) return cached.detail;
|
|
569
571
|
try {
|
|
570
|
-
const meta = parseCommitLogOutput(await runGit$
|
|
572
|
+
const meta = parseCommitLogOutput(await runGit$2(repoRoot, [
|
|
571
573
|
"show",
|
|
572
574
|
"-s",
|
|
573
575
|
"--date=iso-strict",
|
|
@@ -590,13 +592,13 @@ const fetchCommitDetail = async (repoRoot, hash, options) => {
|
|
|
590
592
|
hash
|
|
591
593
|
]))[0];
|
|
592
594
|
if (!meta) return null;
|
|
593
|
-
const nameStatusOutput = await runGit$
|
|
595
|
+
const nameStatusOutput = await runGit$2(repoRoot, [
|
|
594
596
|
"show",
|
|
595
597
|
"--name-status",
|
|
596
598
|
"--format=",
|
|
597
599
|
hash
|
|
598
600
|
]);
|
|
599
|
-
const numstatOutput = await runGit$
|
|
601
|
+
const numstatOutput = await runGit$2(repoRoot, [
|
|
600
602
|
"show",
|
|
601
603
|
"--numstat",
|
|
602
604
|
"--format=",
|
|
@@ -632,7 +634,7 @@ const fetchCommitFile = async (repoRoot, hash, file, options) => {
|
|
|
632
634
|
if (!options?.force && cached && nowMs - cached.at < FILE_TTL_MS$1) return cached.file;
|
|
633
635
|
let patch = "";
|
|
634
636
|
try {
|
|
635
|
-
patch = await runGit$
|
|
637
|
+
patch = await runGit$2(repoRoot, [
|
|
636
638
|
"show",
|
|
637
639
|
"--find-renames",
|
|
638
640
|
"--format=",
|
|
@@ -640,7 +642,7 @@ const fetchCommitFile = async (repoRoot, hash, file, options) => {
|
|
|
640
642
|
"--",
|
|
641
643
|
file.path
|
|
642
644
|
]);
|
|
643
|
-
if (!patch && file.renamedFrom) patch = await runGit$
|
|
645
|
+
if (!patch && file.renamedFrom) patch = await runGit$2(repoRoot, [
|
|
644
646
|
"show",
|
|
645
647
|
"--find-renames",
|
|
646
648
|
"--format=",
|
|
@@ -682,7 +684,7 @@ const nowIso = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
|
682
684
|
const summaryCache = /* @__PURE__ */ new Map();
|
|
683
685
|
const fileCache = /* @__PURE__ */ new Map();
|
|
684
686
|
const createRevision = (statusOutput) => crypto.createHash("sha1").update(statusOutput).digest("hex");
|
|
685
|
-
const runGit = async (cwd, args) => {
|
|
687
|
+
const runGit$1 = async (cwd, args) => {
|
|
686
688
|
try {
|
|
687
689
|
return (await execFileAsync$2("git", [
|
|
688
690
|
"-C",
|
|
@@ -698,9 +700,9 @@ const runGit = async (cwd, args) => {
|
|
|
698
700
|
throw err;
|
|
699
701
|
}
|
|
700
702
|
};
|
|
701
|
-
const resolveRepoRoot = async (cwd) => {
|
|
703
|
+
const resolveRepoRoot$1 = async (cwd) => {
|
|
702
704
|
try {
|
|
703
|
-
const trimmed = (await runGit(cwd, ["rev-parse", "--show-toplevel"])).trim();
|
|
705
|
+
const trimmed = (await runGit$1(cwd, ["rev-parse", "--show-toplevel"])).trim();
|
|
704
706
|
return trimmed.length > 0 ? trimmed : null;
|
|
705
707
|
} catch {
|
|
706
708
|
return null;
|
|
@@ -797,7 +799,7 @@ const fetchDiffSummary = async (cwd, options) => {
|
|
|
797
799
|
files: [],
|
|
798
800
|
reason: "cwd_unknown"
|
|
799
801
|
};
|
|
800
|
-
const repoRoot = await resolveRepoRoot(cwd);
|
|
802
|
+
const repoRoot = await resolveRepoRoot$1(cwd);
|
|
801
803
|
if (!repoRoot) return {
|
|
802
804
|
repoRoot: null,
|
|
803
805
|
rev: null,
|
|
@@ -809,13 +811,13 @@ const fetchDiffSummary = async (cwd, options) => {
|
|
|
809
811
|
const nowMs = Date.now();
|
|
810
812
|
if (!options?.force && cached && nowMs - cached.at < SUMMARY_TTL_MS) return cached.summary;
|
|
811
813
|
try {
|
|
812
|
-
const statusOutput = await runGit(repoRoot, [
|
|
814
|
+
const statusOutput = await runGit$1(repoRoot, [
|
|
813
815
|
"status",
|
|
814
816
|
"--porcelain",
|
|
815
817
|
"-z"
|
|
816
818
|
]);
|
|
817
819
|
const files = parseGitStatus(statusOutput);
|
|
818
|
-
const stats = parseNumstat(await runGit(repoRoot, [
|
|
820
|
+
const stats = parseNumstat(await runGit$1(repoRoot, [
|
|
819
821
|
"diff",
|
|
820
822
|
"HEAD",
|
|
821
823
|
"--numstat",
|
|
@@ -826,7 +828,7 @@ const fetchDiffSummary = async (cwd, options) => {
|
|
|
826
828
|
if (file.status !== "?") continue;
|
|
827
829
|
const safePath = resolveSafePath(repoRoot, file.path);
|
|
828
830
|
if (!safePath) continue;
|
|
829
|
-
const parsed = parseNumstatLine(await runGit(repoRoot, [
|
|
831
|
+
const parsed = parseNumstatLine(await runGit$1(repoRoot, [
|
|
830
832
|
"diff",
|
|
831
833
|
"--no-index",
|
|
832
834
|
"--numstat",
|
|
@@ -885,14 +887,14 @@ const fetchDiffFile = async (repoRoot, file, rev, options) => {
|
|
|
885
887
|
let numstat = null;
|
|
886
888
|
try {
|
|
887
889
|
if (file.status === "?") {
|
|
888
|
-
patch = await runGit(repoRoot, [
|
|
890
|
+
patch = await runGit$1(repoRoot, [
|
|
889
891
|
"diff",
|
|
890
892
|
"--no-index",
|
|
891
893
|
"--",
|
|
892
894
|
"/dev/null",
|
|
893
895
|
safePath
|
|
894
896
|
]);
|
|
895
|
-
numstat = parseNumstatLine(await runGit(repoRoot, [
|
|
897
|
+
numstat = parseNumstatLine(await runGit$1(repoRoot, [
|
|
896
898
|
"diff",
|
|
897
899
|
"--no-index",
|
|
898
900
|
"--numstat",
|
|
@@ -901,13 +903,13 @@ const fetchDiffFile = async (repoRoot, file, rev, options) => {
|
|
|
901
903
|
safePath
|
|
902
904
|
]));
|
|
903
905
|
} else {
|
|
904
|
-
patch = await runGit(repoRoot, [
|
|
906
|
+
patch = await runGit$1(repoRoot, [
|
|
905
907
|
"diff",
|
|
906
908
|
"HEAD",
|
|
907
909
|
"--",
|
|
908
910
|
file.path
|
|
909
911
|
]);
|
|
910
|
-
numstat = parseNumstatLine(await runGit(repoRoot, [
|
|
912
|
+
numstat = parseNumstatLine(await runGit$1(repoRoot, [
|
|
911
913
|
"diff",
|
|
912
914
|
"HEAD",
|
|
913
915
|
"--numstat",
|
|
@@ -1222,129 +1224,108 @@ const buildError$1 = (code, message) => ({
|
|
|
1222
1224
|
code,
|
|
1223
1225
|
message
|
|
1224
1226
|
});
|
|
1227
|
+
const requireAuth = (config, c) => {
|
|
1228
|
+
const auth = c.req.header("authorization") ?? c.req.header("Authorization");
|
|
1229
|
+
if (!auth?.startsWith("Bearer ")) return false;
|
|
1230
|
+
return auth.replace("Bearer ", "").trim() === config.token;
|
|
1231
|
+
};
|
|
1232
|
+
const isOriginAllowed = (config, origin, host) => {
|
|
1233
|
+
if (config.allowedOrigins.length === 0) return true;
|
|
1234
|
+
if (!origin) return false;
|
|
1235
|
+
return config.allowedOrigins.includes(origin) || (host ? config.allowedOrigins.includes(host) : false);
|
|
1236
|
+
};
|
|
1225
1237
|
const buildEnvelope = (type, data, reqId) => ({
|
|
1226
1238
|
type,
|
|
1227
1239
|
ts: now(),
|
|
1228
1240
|
reqId,
|
|
1229
1241
|
data
|
|
1230
1242
|
});
|
|
1231
|
-
const
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
};
|
|
1248
|
-
const
|
|
1249
|
-
const
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
const screenLimiter = createRateLimiter(config.rateLimit.screen.windowMs, config.rateLimit.screen.max);
|
|
1254
|
-
const requireAuth = (c) => {
|
|
1255
|
-
const auth = c.req.header("authorization") ?? c.req.header("Authorization");
|
|
1256
|
-
if (!auth?.startsWith("Bearer ")) return false;
|
|
1257
|
-
return auth.replace("Bearer ", "").trim() === config.token;
|
|
1258
|
-
};
|
|
1259
|
-
const requireStaticAuth = (c) => {
|
|
1260
|
-
const token = c.req.query("token");
|
|
1261
|
-
if (!token) return false;
|
|
1262
|
-
return token === config.token;
|
|
1263
|
-
};
|
|
1264
|
-
const isOriginAllowed = (origin, host) => {
|
|
1265
|
-
if (config.allowedOrigins.length === 0) return true;
|
|
1266
|
-
if (!origin) return false;
|
|
1267
|
-
return config.allowedOrigins.includes(origin) || (host ? config.allowedOrigins.includes(host) : false);
|
|
1268
|
-
};
|
|
1269
|
-
const sendWs = (ws, message) => {
|
|
1270
|
-
ws.send(JSON.stringify(message));
|
|
1271
|
-
};
|
|
1272
|
-
const closeAllWsClients = (code, reason) => {
|
|
1273
|
-
wsClients.forEach((ws) => {
|
|
1274
|
-
try {
|
|
1275
|
-
ws.close(code, reason);
|
|
1276
|
-
} catch {}
|
|
1277
|
-
});
|
|
1278
|
-
wsClients.clear();
|
|
1279
|
-
};
|
|
1280
|
-
const broadcast = (message) => {
|
|
1281
|
-
const payload = JSON.stringify(message);
|
|
1282
|
-
wsClients.forEach((ws) => ws.send(payload));
|
|
1283
|
-
};
|
|
1284
|
-
monitor.registry.onChanged((session) => {
|
|
1285
|
-
broadcast(buildEnvelope("session.updated", { session }));
|
|
1286
|
-
});
|
|
1287
|
-
monitor.registry.onRemoved((paneId) => {
|
|
1288
|
-
broadcast(buildEnvelope("session.removed", { paneId }));
|
|
1289
|
-
});
|
|
1290
|
-
app.use("/api/*", async (c, next) => {
|
|
1291
|
-
if (!requireAuth(c)) return c.json({ error: buildError$1("INVALID_PAYLOAD", "unauthorized") }, 401);
|
|
1292
|
-
if (!isOriginAllowed(c.req.header("origin"), c.req.header("host"))) return c.json({ error: buildError$1("INVALID_PAYLOAD", "origin not allowed") }, 403);
|
|
1243
|
+
const forceQuerySchema = z.object({ force: z.string().optional() });
|
|
1244
|
+
const diffFileQuerySchema = z.object({
|
|
1245
|
+
path: z.string(),
|
|
1246
|
+
rev: z.string().optional(),
|
|
1247
|
+
force: z.string().optional()
|
|
1248
|
+
});
|
|
1249
|
+
const commitLogQuerySchema = z.object({
|
|
1250
|
+
limit: z.string().optional(),
|
|
1251
|
+
skip: z.string().optional(),
|
|
1252
|
+
force: z.string().optional()
|
|
1253
|
+
});
|
|
1254
|
+
const commitDetailQuerySchema = z.object({ force: z.string().optional() });
|
|
1255
|
+
const commitFileQuerySchema = z.object({
|
|
1256
|
+
path: z.string(),
|
|
1257
|
+
force: z.string().optional()
|
|
1258
|
+
});
|
|
1259
|
+
const titleSchema = z.object({ title: z.string().nullable() });
|
|
1260
|
+
const createApiRouter = ({ config, monitor }) => {
|
|
1261
|
+
const api = new Hono();
|
|
1262
|
+
api.use("*", async (c, next) => {
|
|
1263
|
+
if (!requireAuth(config, c)) return c.json({ error: buildError$1("INVALID_PAYLOAD", "unauthorized") }, 401);
|
|
1264
|
+
if (!isOriginAllowed(config, c.req.header("origin"), c.req.header("host"))) return c.json({ error: buildError$1("INVALID_PAYLOAD", "origin not allowed") }, 403);
|
|
1293
1265
|
await next();
|
|
1294
1266
|
});
|
|
1295
|
-
|
|
1267
|
+
return api.get("/sessions", (c) => {
|
|
1296
1268
|
return c.json({
|
|
1297
1269
|
sessions: monitor.registry.snapshot(),
|
|
1298
1270
|
serverTime: now()
|
|
1299
1271
|
});
|
|
1300
|
-
})
|
|
1301
|
-
app.get("/api/sessions/:paneId", (c) => {
|
|
1272
|
+
}).get("/sessions/:paneId", (c) => {
|
|
1302
1273
|
const paneId = c.req.param("paneId");
|
|
1303
1274
|
if (!paneId) return c.json({ error: buildError$1("INVALID_PAYLOAD", "invalid pane id") }, 400);
|
|
1304
1275
|
const detail = monitor.registry.getDetail(paneId);
|
|
1305
1276
|
if (!detail) return c.json({ error: buildError$1("NOT_FOUND", "pane not found") }, 404);
|
|
1306
1277
|
return c.json({ session: detail });
|
|
1307
|
-
})
|
|
1308
|
-
|
|
1278
|
+
}).put("/sessions/:paneId/title", zValidator("json", titleSchema), async (c) => {
|
|
1279
|
+
if (config.readOnly) return c.json({ error: buildError$1("READ_ONLY", "read-only mode") }, 403);
|
|
1309
1280
|
const paneId = c.req.param("paneId");
|
|
1310
1281
|
if (!paneId) return c.json({ error: buildError$1("INVALID_PAYLOAD", "invalid pane id") }, 400);
|
|
1311
1282
|
const detail = monitor.registry.getDetail(paneId);
|
|
1312
1283
|
if (!detail) return c.json({ error: buildError$1("NOT_FOUND", "pane not found") }, 404);
|
|
1313
|
-
const
|
|
1284
|
+
const { title } = c.req.valid("json");
|
|
1285
|
+
const trimmed = title ? title.trim() : null;
|
|
1286
|
+
if (trimmed && trimmed.length > 80) return c.json({ error: buildError$1("INVALID_PAYLOAD", "title too long") }, 400);
|
|
1287
|
+
const nextTitle = trimmed && trimmed.length > 0 ? trimmed : null;
|
|
1288
|
+
monitor.setCustomTitle(paneId, nextTitle);
|
|
1289
|
+
const updated = monitor.registry.getDetail(paneId) ?? detail;
|
|
1290
|
+
return c.json({ session: updated });
|
|
1291
|
+
}).get("/sessions/:paneId/diff", zValidator("query", forceQuerySchema), async (c) => {
|
|
1292
|
+
const paneId = c.req.param("paneId");
|
|
1293
|
+
if (!paneId) return c.json({ error: buildError$1("INVALID_PAYLOAD", "invalid pane id") }, 400);
|
|
1294
|
+
const detail = monitor.registry.getDetail(paneId);
|
|
1295
|
+
if (!detail) return c.json({ error: buildError$1("NOT_FOUND", "pane not found") }, 404);
|
|
1296
|
+
const force = c.req.valid("query").force === "1";
|
|
1314
1297
|
const summary = await fetchDiffSummary(detail.currentPath, { force });
|
|
1315
1298
|
return c.json({ summary });
|
|
1316
|
-
})
|
|
1317
|
-
app.get("/api/sessions/:paneId/diff/file", async (c) => {
|
|
1299
|
+
}).get("/sessions/:paneId/diff/file", zValidator("query", diffFileQuerySchema), async (c) => {
|
|
1318
1300
|
const paneId = c.req.param("paneId");
|
|
1319
1301
|
if (!paneId) return c.json({ error: buildError$1("INVALID_PAYLOAD", "invalid pane id") }, 400);
|
|
1320
1302
|
const detail = monitor.registry.getDetail(paneId);
|
|
1321
1303
|
if (!detail) return c.json({ error: buildError$1("NOT_FOUND", "pane not found") }, 404);
|
|
1322
|
-
const
|
|
1323
|
-
|
|
1324
|
-
const force =
|
|
1304
|
+
const query = c.req.valid("query");
|
|
1305
|
+
const pathParam = query.path;
|
|
1306
|
+
const force = query.force === "1";
|
|
1325
1307
|
const summary = await fetchDiffSummary(detail.currentPath, { force });
|
|
1326
1308
|
if (!summary.repoRoot || summary.reason || !summary.rev) return c.json({ error: buildError$1("INVALID_PAYLOAD", "diff summary unavailable") }, 400);
|
|
1327
1309
|
const target = summary.files.find((file) => file.path === pathParam);
|
|
1328
1310
|
if (!target) return c.json({ error: buildError$1("NOT_FOUND", "file not found") }, 404);
|
|
1329
1311
|
const file = await fetchDiffFile(summary.repoRoot, target, summary.rev, { force });
|
|
1330
1312
|
return c.json({ file });
|
|
1331
|
-
})
|
|
1332
|
-
app.get("/api/sessions/:paneId/commits", async (c) => {
|
|
1313
|
+
}).get("/sessions/:paneId/commits", zValidator("query", commitLogQuerySchema), async (c) => {
|
|
1333
1314
|
const paneId = c.req.param("paneId");
|
|
1334
1315
|
if (!paneId) return c.json({ error: buildError$1("INVALID_PAYLOAD", "invalid pane id") }, 400);
|
|
1335
1316
|
const detail = monitor.registry.getDetail(paneId);
|
|
1336
1317
|
if (!detail) return c.json({ error: buildError$1("NOT_FOUND", "pane not found") }, 404);
|
|
1337
|
-
const
|
|
1338
|
-
const
|
|
1339
|
-
const
|
|
1318
|
+
const query = c.req.valid("query");
|
|
1319
|
+
const limit = Number.parseInt(query.limit ?? "10", 10);
|
|
1320
|
+
const skip = Number.parseInt(query.skip ?? "0", 10);
|
|
1321
|
+
const force = query.force === "1";
|
|
1340
1322
|
const log = await fetchCommitLog(detail.currentPath, {
|
|
1341
1323
|
limit: Number.isFinite(limit) ? limit : 10,
|
|
1342
1324
|
skip: Number.isFinite(skip) ? skip : 0,
|
|
1343
1325
|
force
|
|
1344
1326
|
});
|
|
1345
1327
|
return c.json({ log });
|
|
1346
|
-
})
|
|
1347
|
-
app.get("/api/sessions/:paneId/commits/:hash", async (c) => {
|
|
1328
|
+
}).get("/sessions/:paneId/commits/:hash", zValidator("query", commitDetailQuerySchema), async (c) => {
|
|
1348
1329
|
const paneId = c.req.param("paneId");
|
|
1349
1330
|
if (!paneId) return c.json({ error: buildError$1("INVALID_PAYLOAD", "invalid pane id") }, 400);
|
|
1350
1331
|
const detail = monitor.registry.getDetail(paneId);
|
|
@@ -1356,18 +1337,19 @@ const createApp = ({ config, monitor, tmuxActions }) => {
|
|
|
1356
1337
|
skip: 0
|
|
1357
1338
|
});
|
|
1358
1339
|
if (!log.repoRoot || log.reason) return c.json({ error: buildError$1("INVALID_PAYLOAD", "commit log unavailable") }, 400);
|
|
1359
|
-
const
|
|
1340
|
+
const query = c.req.valid("query");
|
|
1341
|
+
const commit = await fetchCommitDetail(log.repoRoot, hash, { force: query.force === "1" });
|
|
1360
1342
|
if (!commit) return c.json({ error: buildError$1("NOT_FOUND", "commit not found") }, 404);
|
|
1361
1343
|
return c.json({ commit });
|
|
1362
|
-
})
|
|
1363
|
-
app.get("/api/sessions/:paneId/commits/:hash/file", async (c) => {
|
|
1344
|
+
}).get("/sessions/:paneId/commits/:hash/file", zValidator("query", commitFileQuerySchema), async (c) => {
|
|
1364
1345
|
const paneId = c.req.param("paneId");
|
|
1365
1346
|
if (!paneId) return c.json({ error: buildError$1("INVALID_PAYLOAD", "invalid pane id") }, 400);
|
|
1366
1347
|
const detail = monitor.registry.getDetail(paneId);
|
|
1367
1348
|
if (!detail) return c.json({ error: buildError$1("NOT_FOUND", "pane not found") }, 404);
|
|
1368
1349
|
const hash = c.req.param("hash");
|
|
1369
|
-
const
|
|
1370
|
-
|
|
1350
|
+
const query = c.req.valid("query");
|
|
1351
|
+
const pathParam = query.path;
|
|
1352
|
+
if (!hash) return c.json({ error: buildError$1("INVALID_PAYLOAD", "missing hash") }, 400);
|
|
1371
1353
|
const log = await fetchCommitLog(detail.currentPath, {
|
|
1372
1354
|
limit: 1,
|
|
1373
1355
|
skip: 0
|
|
@@ -1377,9 +1359,69 @@ const createApp = ({ config, monitor, tmuxActions }) => {
|
|
|
1377
1359
|
if (!commit) return c.json({ error: buildError$1("NOT_FOUND", "commit not found") }, 404);
|
|
1378
1360
|
const target = commit.files.find((file) => file.path === pathParam) ?? commit.files.find((file) => file.renamedFrom === pathParam);
|
|
1379
1361
|
if (!target) return c.json({ error: buildError$1("NOT_FOUND", "file not found") }, 404);
|
|
1380
|
-
const file = await fetchCommitFile(log.repoRoot, hash, target, { force:
|
|
1362
|
+
const file = await fetchCommitFile(log.repoRoot, hash, target, { force: query.force === "1" });
|
|
1381
1363
|
return c.json({ file });
|
|
1382
1364
|
});
|
|
1365
|
+
};
|
|
1366
|
+
const createRateLimiter = (windowMs, max) => {
|
|
1367
|
+
const hits = /* @__PURE__ */ new Map();
|
|
1368
|
+
return (key) => {
|
|
1369
|
+
const nowMs = Date.now();
|
|
1370
|
+
const entry = hits.get(key);
|
|
1371
|
+
if (!entry || entry.expiresAt <= nowMs) {
|
|
1372
|
+
hits.set(key, {
|
|
1373
|
+
count: 1,
|
|
1374
|
+
expiresAt: nowMs + windowMs
|
|
1375
|
+
});
|
|
1376
|
+
return true;
|
|
1377
|
+
}
|
|
1378
|
+
if (entry.count >= max) return false;
|
|
1379
|
+
entry.count += 1;
|
|
1380
|
+
return true;
|
|
1381
|
+
};
|
|
1382
|
+
};
|
|
1383
|
+
const createApp = ({ config, monitor, tmuxActions }) => {
|
|
1384
|
+
const app = new Hono();
|
|
1385
|
+
const { injectWebSocket, upgradeWebSocket } = createNodeWebSocket({ app });
|
|
1386
|
+
const wsClients = /* @__PURE__ */ new Set();
|
|
1387
|
+
const sendLimiter = createRateLimiter(config.rateLimit.send.windowMs, config.rateLimit.send.max);
|
|
1388
|
+
const screenLimiter = createRateLimiter(config.rateLimit.screen.windowMs, config.rateLimit.screen.max);
|
|
1389
|
+
const requireStaticAuth = (c) => {
|
|
1390
|
+
const token = c.req.query("token");
|
|
1391
|
+
if (!token) return false;
|
|
1392
|
+
return token === config.token;
|
|
1393
|
+
};
|
|
1394
|
+
const sendWs = (ws, message) => {
|
|
1395
|
+
ws.send(JSON.stringify(message));
|
|
1396
|
+
};
|
|
1397
|
+
const closeAllWsClients = (code, reason) => {
|
|
1398
|
+
wsClients.forEach((ws) => {
|
|
1399
|
+
try {
|
|
1400
|
+
ws.close(code, reason);
|
|
1401
|
+
} catch {}
|
|
1402
|
+
});
|
|
1403
|
+
wsClients.clear();
|
|
1404
|
+
};
|
|
1405
|
+
const broadcast = (message) => {
|
|
1406
|
+
const payload = JSON.stringify(message);
|
|
1407
|
+
wsClients.forEach((ws) => ws.send(payload));
|
|
1408
|
+
};
|
|
1409
|
+
monitor.registry.onChanged((session) => {
|
|
1410
|
+
broadcast(buildEnvelope("session.updated", { session }));
|
|
1411
|
+
});
|
|
1412
|
+
monitor.registry.onRemoved((paneId) => {
|
|
1413
|
+
broadcast(buildEnvelope("session.removed", { paneId }));
|
|
1414
|
+
});
|
|
1415
|
+
const api = createApiRouter({
|
|
1416
|
+
config,
|
|
1417
|
+
monitor
|
|
1418
|
+
});
|
|
1419
|
+
app.route("/api", api);
|
|
1420
|
+
app.use("/api/admin/*", async (c, next) => {
|
|
1421
|
+
if (!requireAuth(config, c)) return c.json({ error: buildError$1("INVALID_PAYLOAD", "unauthorized") }, 401);
|
|
1422
|
+
if (!isOriginAllowed(config, c.req.header("origin"), c.req.header("host"))) return c.json({ error: buildError$1("INVALID_PAYLOAD", "origin not allowed") }, 403);
|
|
1423
|
+
await next();
|
|
1424
|
+
});
|
|
1383
1425
|
app.post("/api/admin/token/rotate", (c) => {
|
|
1384
1426
|
const next = rotateToken();
|
|
1385
1427
|
config.token = next.token;
|
|
@@ -1575,11 +1617,15 @@ const createApp = ({ config, monitor, tmuxActions }) => {
|
|
|
1575
1617
|
return;
|
|
1576
1618
|
}
|
|
1577
1619
|
if (message.type === "send.text") {
|
|
1578
|
-
|
|
1620
|
+
const result = await tmuxActions.sendText(message.data.paneId, message.data.text, message.data.enter ?? true);
|
|
1621
|
+
if (result.ok) monitor.recordInput(message.data.paneId);
|
|
1622
|
+
sendWs(ws, buildEnvelope("command.response", result, reqId));
|
|
1579
1623
|
return;
|
|
1580
1624
|
}
|
|
1581
1625
|
if (message.type === "send.keys") {
|
|
1582
|
-
|
|
1626
|
+
const result = await tmuxActions.sendKeys(message.data.paneId, message.data.keys);
|
|
1627
|
+
if (result.ok) monitor.recordInput(message.data.paneId);
|
|
1628
|
+
sendWs(ws, buildEnvelope("command.response", result, reqId));
|
|
1583
1629
|
return;
|
|
1584
1630
|
}
|
|
1585
1631
|
}
|
|
@@ -1587,7 +1633,7 @@ const createApp = ({ config, monitor, tmuxActions }) => {
|
|
|
1587
1633
|
app.use("/ws", async (c, next) => {
|
|
1588
1634
|
const token = c.req.query("token");
|
|
1589
1635
|
if (!token || token !== config.token) return c.text("Unauthorized", 401);
|
|
1590
|
-
if (!isOriginAllowed(c.req.header("origin"), c.req.header("host"))) return c.text("Forbidden", 403);
|
|
1636
|
+
if (!isOriginAllowed(config, c.req.header("origin"), c.req.header("host"))) return c.text("Forbidden", 403);
|
|
1591
1637
|
await next();
|
|
1592
1638
|
});
|
|
1593
1639
|
app.get("/ws", wsHandler);
|
|
@@ -1850,6 +1896,8 @@ const saveState = (sessions) => {
|
|
|
1850
1896
|
lastOutputAt: session.lastOutputAt,
|
|
1851
1897
|
lastEventAt: session.lastEventAt,
|
|
1852
1898
|
lastMessage: session.lastMessage,
|
|
1899
|
+
lastInputAt: session.lastInputAt,
|
|
1900
|
+
customTitle: session.customTitle ?? null,
|
|
1853
1901
|
state: session.state,
|
|
1854
1902
|
stateReason: session.stateReason
|
|
1855
1903
|
}]))
|
|
@@ -1912,6 +1960,8 @@ const hasAgentHint = (value) => Boolean(value && agentHintPattern.test(value));
|
|
|
1912
1960
|
const processCacheTtlMs = 5e3;
|
|
1913
1961
|
const processCommandCache = /* @__PURE__ */ new Map();
|
|
1914
1962
|
const ttyAgentCache = /* @__PURE__ */ new Map();
|
|
1963
|
+
const repoRootCacheTtlMs = 1e4;
|
|
1964
|
+
const repoRootCache = /* @__PURE__ */ new Map();
|
|
1915
1965
|
const processSnapshotCache = {
|
|
1916
1966
|
at: 0,
|
|
1917
1967
|
byPid: /* @__PURE__ */ new Map(),
|
|
@@ -1938,6 +1988,40 @@ const hostCandidates = (() => {
|
|
|
1938
1988
|
`${short}.local`
|
|
1939
1989
|
]);
|
|
1940
1990
|
})();
|
|
1991
|
+
const runGit = async (cwd, args) => {
|
|
1992
|
+
try {
|
|
1993
|
+
return (await execFileAsync("git", [
|
|
1994
|
+
"-C",
|
|
1995
|
+
cwd,
|
|
1996
|
+
...args
|
|
1997
|
+
], {
|
|
1998
|
+
encoding: "utf8",
|
|
1999
|
+
timeout: 2e3,
|
|
2000
|
+
maxBuffer: 2e6
|
|
2001
|
+
})).stdout ?? "";
|
|
2002
|
+
} catch {
|
|
2003
|
+
return "";
|
|
2004
|
+
}
|
|
2005
|
+
};
|
|
2006
|
+
const resolveRepoRoot = async (cwd) => {
|
|
2007
|
+
if (!cwd) return null;
|
|
2008
|
+
const trimmed = (await runGit(cwd, ["rev-parse", "--show-toplevel"])).trim();
|
|
2009
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
2010
|
+
};
|
|
2011
|
+
const resolveRepoRootCached = async (cwd) => {
|
|
2012
|
+
if (!cwd) return null;
|
|
2013
|
+
const normalized = cwd.replace(/\/+$/, "");
|
|
2014
|
+
if (!normalized) return null;
|
|
2015
|
+
const nowMs = Date.now();
|
|
2016
|
+
const cached = repoRootCache.get(normalized);
|
|
2017
|
+
if (cached && nowMs - cached.at < repoRootCacheTtlMs) return cached.repoRoot;
|
|
2018
|
+
const repoRoot = await resolveRepoRoot(normalized);
|
|
2019
|
+
repoRootCache.set(normalized, {
|
|
2020
|
+
repoRoot,
|
|
2021
|
+
at: nowMs
|
|
2022
|
+
});
|
|
2023
|
+
return repoRoot;
|
|
2024
|
+
};
|
|
1941
2025
|
const toIsoFromEpochSeconds = (value) => {
|
|
1942
2026
|
if (!value) return null;
|
|
1943
2027
|
const date = /* @__PURE__ */ new Date(value * 1e3);
|
|
@@ -2088,7 +2172,9 @@ const createSessionMonitor = (adapter, config) => {
|
|
|
2088
2172
|
const lastOutputAt = /* @__PURE__ */ new Map();
|
|
2089
2173
|
const lastEventAt = /* @__PURE__ */ new Map();
|
|
2090
2174
|
const lastMessage = /* @__PURE__ */ new Map();
|
|
2175
|
+
const lastInputAt = /* @__PURE__ */ new Map();
|
|
2091
2176
|
const lastFingerprint = /* @__PURE__ */ new Map();
|
|
2177
|
+
const customTitles = /* @__PURE__ */ new Map();
|
|
2092
2178
|
const restored = restoreSessions();
|
|
2093
2179
|
const restoredReason = /* @__PURE__ */ new Set();
|
|
2094
2180
|
const serverKey = resolveServerKey(config.tmux.socketName, config.tmux.socketPath);
|
|
@@ -2101,6 +2187,8 @@ const createSessionMonitor = (adapter, config) => {
|
|
|
2101
2187
|
lastOutputAt.set(paneId, session.lastOutputAt ?? null);
|
|
2102
2188
|
lastEventAt.set(paneId, session.lastEventAt ?? null);
|
|
2103
2189
|
lastMessage.set(paneId, session.lastMessage ?? null);
|
|
2190
|
+
if (session.lastInputAt) lastInputAt.set(paneId, session.lastInputAt);
|
|
2191
|
+
if (session.customTitle) customTitles.set(paneId, session.customTitle);
|
|
2104
2192
|
});
|
|
2105
2193
|
const getPaneLogPath = (paneId) => {
|
|
2106
2194
|
return resolveLogPaths(baseDir, serverKey, paneId).paneLogPath;
|
|
@@ -2216,6 +2304,9 @@ const createSessionMonitor = (adapter, config) => {
|
|
|
2216
2304
|
const paneTitle = normalizeTitle(pane.paneTitle);
|
|
2217
2305
|
const defaultTitle = buildDefaultTitle(pane.currentPath, pane.paneId, pane.sessionName);
|
|
2218
2306
|
const title = paneTitle && !hostCandidates.has(paneTitle) ? paneTitle : defaultTitle;
|
|
2307
|
+
const customTitle = customTitles.get(pane.paneId) ?? null;
|
|
2308
|
+
const repoRoot = await resolveRepoRootCached(pane.currentPath);
|
|
2309
|
+
const inputAt = lastInputAt.get(pane.paneId) ?? null;
|
|
2219
2310
|
const detail = {
|
|
2220
2311
|
paneId: pane.paneId,
|
|
2221
2312
|
sessionName: pane.sessionName,
|
|
@@ -2227,12 +2318,15 @@ const createSessionMonitor = (adapter, config) => {
|
|
|
2227
2318
|
currentPath: pane.currentPath,
|
|
2228
2319
|
paneTty: pane.paneTty,
|
|
2229
2320
|
title,
|
|
2321
|
+
customTitle,
|
|
2322
|
+
repoRoot,
|
|
2230
2323
|
agent,
|
|
2231
2324
|
state: finalState,
|
|
2232
2325
|
stateReason: finalReason,
|
|
2233
2326
|
lastMessage: message,
|
|
2234
2327
|
lastOutputAt: outputAt,
|
|
2235
2328
|
lastEventAt: eventAt,
|
|
2329
|
+
lastInputAt: inputAt,
|
|
2236
2330
|
paneDead: pane.paneDead,
|
|
2237
2331
|
alternateOn: pane.alternateOn,
|
|
2238
2332
|
pipeAttached,
|
|
@@ -2242,22 +2336,49 @@ const createSessionMonitor = (adapter, config) => {
|
|
|
2242
2336
|
};
|
|
2243
2337
|
registry.update(detail);
|
|
2244
2338
|
}
|
|
2245
|
-
registry.removeMissing(activePaneIds)
|
|
2339
|
+
registry.removeMissing(activePaneIds).forEach((paneId) => {
|
|
2340
|
+
customTitles.delete(paneId);
|
|
2341
|
+
});
|
|
2246
2342
|
lastOutputAt.forEach((_, paneId) => {
|
|
2247
2343
|
if (!activePaneIds.has(paneId)) {
|
|
2248
2344
|
lastOutputAt.delete(paneId);
|
|
2249
2345
|
lastEventAt.delete(paneId);
|
|
2250
2346
|
lastMessage.delete(paneId);
|
|
2347
|
+
lastInputAt.delete(paneId);
|
|
2251
2348
|
lastFingerprint.delete(paneId);
|
|
2252
2349
|
hookStates.delete(paneId);
|
|
2253
2350
|
}
|
|
2254
2351
|
});
|
|
2255
2352
|
saveState(registry.values());
|
|
2256
2353
|
};
|
|
2354
|
+
const setCustomTitle = (paneId, title) => {
|
|
2355
|
+
if (title) customTitles.set(paneId, title);
|
|
2356
|
+
else customTitles.delete(paneId);
|
|
2357
|
+
const existing = registry.getDetail(paneId);
|
|
2358
|
+
if (!existing || existing.customTitle === (title ?? null)) return;
|
|
2359
|
+
const next = {
|
|
2360
|
+
...existing,
|
|
2361
|
+
customTitle: title
|
|
2362
|
+
};
|
|
2363
|
+
registry.update(next);
|
|
2364
|
+
saveState(registry.values());
|
|
2365
|
+
};
|
|
2257
2366
|
const handleHookEvent = (context) => {
|
|
2258
2367
|
hookStates.set(context.paneId, context.hookState);
|
|
2259
2368
|
lastEventAt.set(context.paneId, context.hookState.at);
|
|
2260
2369
|
};
|
|
2370
|
+
const recordInput = (paneId, at = (/* @__PURE__ */ new Date()).toISOString()) => {
|
|
2371
|
+
lastInputAt.set(paneId, at);
|
|
2372
|
+
const existing = registry.getDetail(paneId);
|
|
2373
|
+
if (!existing) return;
|
|
2374
|
+
if (existing.lastInputAt === at) return;
|
|
2375
|
+
const next = {
|
|
2376
|
+
...existing,
|
|
2377
|
+
lastInputAt: at
|
|
2378
|
+
};
|
|
2379
|
+
registry.update(next);
|
|
2380
|
+
saveState(registry.values());
|
|
2381
|
+
};
|
|
2261
2382
|
const startHookTailer = async () => {
|
|
2262
2383
|
await ensureDir(eventsDir);
|
|
2263
2384
|
await fs$1.open(eventLogPath, "a").then((handle) => handle.close());
|
|
@@ -2309,7 +2430,9 @@ const createSessionMonitor = (adapter, config) => {
|
|
|
2309
2430
|
start,
|
|
2310
2431
|
stop,
|
|
2311
2432
|
handleHookEvent,
|
|
2312
|
-
getScreenCapture
|
|
2433
|
+
getScreenCapture,
|
|
2434
|
+
setCustomTitle,
|
|
2435
|
+
recordInput
|
|
2313
2436
|
};
|
|
2314
2437
|
};
|
|
2315
2438
|
|