agentflow-core 0.2.2 → 0.3.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/dist/{chunk-WOJEID7V.js → chunk-NPH34CAL.js} +671 -76
- package/dist/cli.cjs +679 -79
- package/dist/cli.js +13 -6
- package/dist/index.cjs +671 -75
- package/dist/index.d.cts +65 -1
- package/dist/index.d.ts +65 -1
- package/dist/index.js +3 -1
- package/package.json +1 -1
package/dist/cli.cjs
CHANGED
|
@@ -819,9 +819,19 @@ function processJsonlFile(file) {
|
|
|
819
819
|
return [];
|
|
820
820
|
}
|
|
821
821
|
}
|
|
822
|
+
var K = "\x1B[K";
|
|
823
|
+
function writeLine(lines, text) {
|
|
824
|
+
lines.push(text + K);
|
|
825
|
+
}
|
|
826
|
+
function flushLines(lines) {
|
|
827
|
+
process.stdout.write("\x1B[H");
|
|
828
|
+
process.stdout.write(lines.join("\n") + "\n");
|
|
829
|
+
process.stdout.write("\x1B[J");
|
|
830
|
+
}
|
|
822
831
|
var prevFileCount = 0;
|
|
823
832
|
var newExecCount = 0;
|
|
824
833
|
var sessionStart = Date.now();
|
|
834
|
+
var firstRender = true;
|
|
825
835
|
function render(config) {
|
|
826
836
|
const files = scanFiles(config.dirs, config.recursive);
|
|
827
837
|
if (files.length > prevFileCount && prevFileCount > 0) {
|
|
@@ -837,26 +847,58 @@ function render(config) {
|
|
|
837
847
|
if (r.traceData) allTraces.push(r.traceData);
|
|
838
848
|
}
|
|
839
849
|
}
|
|
840
|
-
const
|
|
850
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
841
851
|
for (const r of allRecords) {
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
852
|
+
const arr = byFile.get(r.file) ?? [];
|
|
853
|
+
arr.push(r);
|
|
854
|
+
byFile.set(r.file, arr);
|
|
855
|
+
}
|
|
856
|
+
const groups = [];
|
|
857
|
+
for (const [file, records] of byFile) {
|
|
858
|
+
if (records.length === 1) {
|
|
859
|
+
const r = records[0];
|
|
860
|
+
groups.push({
|
|
861
|
+
name: r.id,
|
|
862
|
+
source: r.source,
|
|
863
|
+
status: r.status,
|
|
864
|
+
lastTs: r.lastActive,
|
|
865
|
+
detail: r.detail,
|
|
866
|
+
children: [],
|
|
867
|
+
ok: r.status === "ok" ? 1 : 0,
|
|
868
|
+
fail: r.status === "error" ? 1 : 0,
|
|
869
|
+
running: r.status === "running" ? 1 : 0,
|
|
870
|
+
total: 1
|
|
871
|
+
});
|
|
872
|
+
} else {
|
|
873
|
+
const groupName = nameFromFile(file);
|
|
874
|
+
let lastTs = 0;
|
|
875
|
+
let ok = 0, fail = 0, running = 0;
|
|
876
|
+
for (const r of records) {
|
|
877
|
+
if (r.lastActive > lastTs) lastTs = r.lastActive;
|
|
878
|
+
if (r.status === "ok") ok++;
|
|
879
|
+
else if (r.status === "error") fail++;
|
|
880
|
+
else if (r.status === "running") running++;
|
|
881
|
+
}
|
|
882
|
+
const status = fail > 0 ? "error" : running > 0 ? "running" : ok > 0 ? "ok" : "unknown";
|
|
883
|
+
groups.push({
|
|
884
|
+
name: groupName,
|
|
885
|
+
source: records[0].source,
|
|
886
|
+
status,
|
|
887
|
+
lastTs,
|
|
888
|
+
detail: `${records.length} agents`,
|
|
889
|
+
children: records.sort((a, b) => b.lastActive - a.lastActive),
|
|
890
|
+
ok,
|
|
891
|
+
fail,
|
|
892
|
+
running,
|
|
893
|
+
total: records.length
|
|
894
|
+
});
|
|
854
895
|
}
|
|
855
896
|
}
|
|
856
|
-
|
|
857
|
-
const totExec =
|
|
858
|
-
const totFail =
|
|
859
|
-
const totRunning =
|
|
897
|
+
groups.sort((a, b) => b.lastTs - a.lastTs);
|
|
898
|
+
const totExec = allRecords.length;
|
|
899
|
+
const totFail = allRecords.filter((r) => r.status === "error").length;
|
|
900
|
+
const totRunning = allRecords.filter((r) => r.status === "running").length;
|
|
901
|
+
const uniqueAgents = new Set(allRecords.map((r) => r.id)).size;
|
|
860
902
|
const sysRate = totExec > 0 ? ((totExec - totFail) / totExec * 100).toFixed(1) : "100.0";
|
|
861
903
|
const now = Date.now();
|
|
862
904
|
const buckets = new Array(12).fill(0);
|
|
@@ -893,7 +935,21 @@ function render(config) {
|
|
|
893
935
|
const upMin = Math.floor(upSec / 60);
|
|
894
936
|
const upStr = upMin > 0 ? `${upMin}m ${upSec % 60}s` : `${upSec}s`;
|
|
895
937
|
const time = (/* @__PURE__ */ new Date()).toLocaleTimeString();
|
|
896
|
-
|
|
938
|
+
function statusIcon(s, recent) {
|
|
939
|
+
if (s === "error") return `${C.red}\u25CF${C.reset}`;
|
|
940
|
+
if (s === "running") return `${C.green}\u25CF${C.reset}`;
|
|
941
|
+
if (s === "ok" && recent) return `${C.green}\u25CF${C.reset}`;
|
|
942
|
+
if (s === "ok") return `${C.dim}\u25CB${C.reset}`;
|
|
943
|
+
return `${C.dim}\u25CB${C.reset}`;
|
|
944
|
+
}
|
|
945
|
+
function statusText(g) {
|
|
946
|
+
if (g.fail > 0 && g.ok === 0 && g.running === 0) return `${C.red}error${C.reset}`;
|
|
947
|
+
if (g.running > 0) return `${C.green}running${C.reset}`;
|
|
948
|
+
if (g.fail > 0) return `${C.yellow}${g.ok}ok/${g.fail}err${C.reset}`;
|
|
949
|
+
if (g.ok > 0) return g.total > 1 ? `${C.green}${g.ok}/${g.total} ok${C.reset}` : `${C.green}ok${C.reset}`;
|
|
950
|
+
return `${C.dim}idle${C.reset}`;
|
|
951
|
+
}
|
|
952
|
+
function sourceTag(s) {
|
|
897
953
|
switch (s) {
|
|
898
954
|
case "trace":
|
|
899
955
|
return `${C.cyan}trace${C.reset}`;
|
|
@@ -906,93 +962,115 @@ function render(config) {
|
|
|
906
962
|
case "state":
|
|
907
963
|
return `${C.dim}state${C.reset}`;
|
|
908
964
|
}
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
965
|
+
}
|
|
966
|
+
function timeStr(ts) {
|
|
967
|
+
if (ts <= 0) return "n/a";
|
|
968
|
+
return new Date(ts).toLocaleTimeString();
|
|
969
|
+
}
|
|
970
|
+
function truncate(s, max) {
|
|
971
|
+
return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
|
|
972
|
+
}
|
|
973
|
+
if (firstRender) {
|
|
974
|
+
process.stdout.write("\x1B[2J");
|
|
975
|
+
firstRender = false;
|
|
976
|
+
}
|
|
977
|
+
const L = [];
|
|
978
|
+
writeLine(L, `${C.bold}${C.cyan}\u2554${"\u2550".repeat(70)}\u2557${C.reset}`);
|
|
979
|
+
writeLine(L, `${C.bold}${C.cyan}\u2551${C.reset} ${C.bold}${C.white}AGENTFLOW LIVE${C.reset} ${C.green}\u25CF LIVE${C.reset} ${C.dim}${time}${C.reset} ${C.bold}${C.cyan}\u2551${C.reset}`);
|
|
913
980
|
const metaLine = `Refresh: ${config.refreshMs / 1e3}s \xB7 Up: ${upStr} \xB7 Files: ${files.length}`;
|
|
914
981
|
const pad1 = Math.max(0, 64 - metaLine.length);
|
|
915
|
-
|
|
916
|
-
|
|
982
|
+
writeLine(L, `${C.bold}${C.cyan}\u2551${C.reset} ${C.dim}${metaLine}${C.reset}${" ".repeat(pad1)}${C.bold}${C.cyan}\u2551${C.reset}`);
|
|
983
|
+
writeLine(L, `${C.bold}${C.cyan}\u255A${"\u2550".repeat(70)}\u255D${C.reset}`);
|
|
917
984
|
const sc = totFail === 0 ? C.green : C.yellow;
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
statusIcon = `${C.yellow}\u25CF${C.reset}`;
|
|
937
|
-
statusText = `${C.yellow}${ag.ok}ok/${ag.fail}err${C.reset}`;
|
|
938
|
-
} else if (ag.ok > 0) {
|
|
939
|
-
statusIcon = isRecent ? `${C.green}\u25CF${C.reset}` : `${C.dim}\u25CB${C.reset}`;
|
|
940
|
-
statusText = ag.total > 1 ? `${C.green}${ag.ok}/${ag.total}${C.reset}` : `${C.green}ok${C.reset}`;
|
|
985
|
+
writeLine(L, "");
|
|
986
|
+
writeLine(L, ` ${C.bold}Agents${C.reset} ${sc}${uniqueAgents}${C.reset} ${C.bold}Records${C.reset} ${sc}${totExec}${C.reset} ${C.bold}Success${C.reset} ${sc}${sysRate}%${C.reset} ${C.bold}Running${C.reset} ${C.green}${totRunning}${C.reset} ${C.bold}Errors${C.reset} ${totFail > 0 ? C.red : C.dim}${totFail}${C.reset} ${C.bold}New${C.reset} ${C.yellow}+${newExecCount}${C.reset}`);
|
|
987
|
+
writeLine(L, "");
|
|
988
|
+
writeLine(L, ` ${C.bold}Activity (1h)${C.reset} ${spark} ${C.dim}\u2190 now${C.reset}`);
|
|
989
|
+
writeLine(L, "");
|
|
990
|
+
writeLine(L, ` ${C.bold}${C.under}Agent Status Last Active Detail${C.reset}`);
|
|
991
|
+
let lineCount = 0;
|
|
992
|
+
for (const g of groups) {
|
|
993
|
+
if (lineCount > 35) break;
|
|
994
|
+
const isRecent = Date.now() - g.lastTs < 3e5;
|
|
995
|
+
const icon = statusIcon(g.status, isRecent);
|
|
996
|
+
const active = isRecent ? `${C.green}${timeStr(g.lastTs)}${C.reset}` : `${C.dim}${timeStr(g.lastTs)}${C.reset}`;
|
|
997
|
+
if (g.children.length === 0) {
|
|
998
|
+
const name = truncate(g.name, 26).padEnd(26);
|
|
999
|
+
const st = statusText(g);
|
|
1000
|
+
const det = truncate(g.detail, 30);
|
|
1001
|
+
writeLine(L, ` ${icon} ${name} ${st.padEnd(20)} ${active.padEnd(20)} ${C.dim}${det}${C.reset}`);
|
|
1002
|
+
lineCount++;
|
|
941
1003
|
} else {
|
|
942
|
-
|
|
943
|
-
|
|
1004
|
+
const name = truncate(g.name, 24).padEnd(24);
|
|
1005
|
+
const st = statusText(g);
|
|
1006
|
+
const tag = sourceTag(g.source);
|
|
1007
|
+
writeLine(L, ` ${icon} ${C.bold}${name}${C.reset} ${st.padEnd(20)} ${active.padEnd(20)} ${tag} ${C.dim}(${g.children.length} agents)${C.reset}`);
|
|
1008
|
+
lineCount++;
|
|
1009
|
+
const kids = g.children.slice(0, 12);
|
|
1010
|
+
for (let i = 0; i < kids.length; i++) {
|
|
1011
|
+
if (lineCount > 35) break;
|
|
1012
|
+
const child = kids[i];
|
|
1013
|
+
const isLast = i === kids.length - 1;
|
|
1014
|
+
const connector = isLast ? "\u2514\u2500" : "\u251C\u2500";
|
|
1015
|
+
const cIcon = statusIcon(child.status, Date.now() - child.lastActive < 3e5);
|
|
1016
|
+
const cName = truncate(child.id, 22).padEnd(22);
|
|
1017
|
+
const cActive = `${C.dim}${timeStr(child.lastActive)}${C.reset}`;
|
|
1018
|
+
const cDet = truncate(child.detail, 25);
|
|
1019
|
+
writeLine(L, ` ${C.dim}${connector}${C.reset} ${cIcon} ${cName} ${cActive.padEnd(20)} ${C.dim}${cDet}${C.reset}`);
|
|
1020
|
+
lineCount++;
|
|
1021
|
+
}
|
|
1022
|
+
if (g.children.length > 12) {
|
|
1023
|
+
writeLine(L, ` ${C.dim} ... +${g.children.length - 12} more${C.reset}`);
|
|
1024
|
+
lineCount++;
|
|
1025
|
+
}
|
|
944
1026
|
}
|
|
945
|
-
const name = ag.name.length > 23 ? ag.name.slice(0, 22) + "\u2026" : ag.name.padEnd(23);
|
|
946
|
-
const src = sourceTag(ag.source).padEnd(16);
|
|
947
|
-
const active = isRecent ? `${C.green}${lastTime}${C.reset}` : `${C.dim}${lastTime}${C.reset}`;
|
|
948
|
-
const detail = ag.detail.length > 30 ? ag.detail.slice(0, 29) + "\u2026" : ag.detail;
|
|
949
|
-
console.log(` ${statusIcon} ${name} ${src} ${statusText.padEnd(18)} ${active.padEnd(20)} ${C.dim}${detail}${C.reset}`);
|
|
950
1027
|
}
|
|
951
1028
|
if (distributedTraces.length > 0) {
|
|
952
|
-
|
|
953
|
-
|
|
1029
|
+
writeLine(L, "");
|
|
1030
|
+
writeLine(L, ` ${C.bold}${C.under}Distributed Traces${C.reset}`);
|
|
954
1031
|
for (const dt of distributedTraces.slice(0, 3)) {
|
|
955
1032
|
const traceTime = new Date(dt.startTime).toLocaleTimeString();
|
|
956
|
-
const
|
|
1033
|
+
const si = dt.status === "completed" ? `${C.green}\u2713${C.reset}` : dt.status === "failed" ? `${C.red}\u2717${C.reset}` : `${C.yellow}\u23F3${C.reset}`;
|
|
957
1034
|
const dur = dt.endTime ? `${dt.endTime - dt.startTime}ms` : "running";
|
|
958
1035
|
const tid = dt.traceId.slice(0, 8);
|
|
959
|
-
|
|
1036
|
+
writeLine(L, ` ${si} ${C.magenta}trace:${tid}${C.reset} ${C.dim}${traceTime} ${dur} (${dt.graphs.size} agents)${C.reset}`);
|
|
960
1037
|
const tree = getTraceTree(dt);
|
|
961
1038
|
for (let i = 0; i < Math.min(tree.length, 6); i++) {
|
|
962
|
-
const
|
|
963
|
-
const depth = getDistDepth(dt,
|
|
1039
|
+
const tg = tree[i];
|
|
1040
|
+
const depth = getDistDepth(dt, tg.spanId);
|
|
964
1041
|
const indent = " " + "\u2502 ".repeat(Math.max(0, depth - 1));
|
|
965
1042
|
const isLast = i === tree.length - 1 || getDistDepth(dt, tree[i + 1]?.spanId) <= depth;
|
|
966
1043
|
const conn = depth === 0 ? " " : isLast ? "\u2514\u2500 " : "\u251C\u2500 ";
|
|
967
|
-
const gs =
|
|
968
|
-
const gd =
|
|
969
|
-
|
|
1044
|
+
const gs = tg.status === "completed" ? `${C.green}\u2713${C.reset}` : tg.status === "failed" ? `${C.red}\u2717${C.reset}` : `${C.yellow}\u23F3${C.reset}`;
|
|
1045
|
+
const gd = tg.endTime ? `${tg.endTime - tg.startTime}ms` : "running";
|
|
1046
|
+
writeLine(L, `${indent}${conn}${gs} ${C.bold}${tg.agentId}${C.reset} ${C.dim}[${tg.trigger}] ${gd}${C.reset}`);
|
|
970
1047
|
}
|
|
971
1048
|
}
|
|
972
1049
|
}
|
|
973
|
-
const recentRecords = allRecords.filter((r) => r.lastActive > 0).sort((a, b) => b.lastActive - a.lastActive).slice(0,
|
|
1050
|
+
const recentRecords = allRecords.filter((r) => r.lastActive > 0).sort((a, b) => b.lastActive - a.lastActive).slice(0, 6);
|
|
974
1051
|
if (recentRecords.length > 0) {
|
|
975
|
-
|
|
976
|
-
|
|
1052
|
+
writeLine(L, "");
|
|
1053
|
+
writeLine(L, ` ${C.bold}${C.under}Recent Activity${C.reset}`);
|
|
977
1054
|
for (const r of recentRecords) {
|
|
978
1055
|
const icon = r.status === "ok" ? `${C.green}\u2713${C.reset}` : r.status === "error" ? `${C.red}\u2717${C.reset}` : r.status === "running" ? `${C.green}\u25B6${C.reset}` : `${C.dim}\u25CB${C.reset}`;
|
|
979
1056
|
const t = new Date(r.lastActive).toLocaleTimeString();
|
|
980
|
-
const agent = r.id
|
|
1057
|
+
const agent = truncate(r.id, 26).padEnd(26);
|
|
981
1058
|
const age = Math.floor((Date.now() - r.lastActive) / 1e3);
|
|
982
1059
|
const ageStr = age < 60 ? age + "s ago" : age < 3600 ? Math.floor(age / 60) + "m ago" : Math.floor(age / 3600) + "h ago";
|
|
983
|
-
const
|
|
984
|
-
|
|
1060
|
+
const det = truncate(r.detail, 25);
|
|
1061
|
+
writeLine(L, ` ${icon} ${agent} ${C.dim}${t} ${ageStr.padStart(8)}${C.reset} ${C.dim}${det}${C.reset}`);
|
|
985
1062
|
}
|
|
986
1063
|
}
|
|
987
1064
|
if (files.length === 0) {
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
for (const d of config.dirs)
|
|
1065
|
+
writeLine(L, "");
|
|
1066
|
+
writeLine(L, ` ${C.dim}No JSON/JSONL files found. Waiting for data in:${C.reset}`);
|
|
1067
|
+
for (const d of config.dirs) writeLine(L, ` ${C.dim} ${d}${C.reset}`);
|
|
991
1068
|
}
|
|
992
|
-
|
|
1069
|
+
writeLine(L, "");
|
|
993
1070
|
const dirLabel = config.dirs.length === 1 ? config.dirs[0] : `${config.dirs.length} directories`;
|
|
994
|
-
|
|
995
|
-
|
|
1071
|
+
writeLine(L, ` ${C.dim}Watching: ${dirLabel}${C.reset}`);
|
|
1072
|
+
writeLine(L, ` ${C.dim}Press Ctrl+C to exit${C.reset}`);
|
|
1073
|
+
flushLines(L);
|
|
996
1074
|
}
|
|
997
1075
|
function getDistDepth(dt, spanId) {
|
|
998
1076
|
if (!spanId) return 0;
|
|
@@ -1031,6 +1109,522 @@ function startLive(argv) {
|
|
|
1031
1109
|
});
|
|
1032
1110
|
}
|
|
1033
1111
|
|
|
1112
|
+
// src/watch.ts
|
|
1113
|
+
var import_node_fs4 = require("fs");
|
|
1114
|
+
var import_node_path3 = require("path");
|
|
1115
|
+
var import_node_os = require("os");
|
|
1116
|
+
|
|
1117
|
+
// src/watch-state.ts
|
|
1118
|
+
var import_node_fs3 = require("fs");
|
|
1119
|
+
function parseDuration(input) {
|
|
1120
|
+
const match = input.match(/^(\d+(?:\.\d+)?)\s*(s|m|h|d)$/i);
|
|
1121
|
+
if (!match) {
|
|
1122
|
+
const n = parseInt(input, 10);
|
|
1123
|
+
return isNaN(n) ? 0 : n * 1e3;
|
|
1124
|
+
}
|
|
1125
|
+
const value = parseFloat(match[1]);
|
|
1126
|
+
switch (match[2].toLowerCase()) {
|
|
1127
|
+
case "s":
|
|
1128
|
+
return value * 1e3;
|
|
1129
|
+
case "m":
|
|
1130
|
+
return value * 6e4;
|
|
1131
|
+
case "h":
|
|
1132
|
+
return value * 36e5;
|
|
1133
|
+
case "d":
|
|
1134
|
+
return value * 864e5;
|
|
1135
|
+
default:
|
|
1136
|
+
return value * 1e3;
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
function emptyState() {
|
|
1140
|
+
return { version: 1, agents: {}, lastPollTime: 0 };
|
|
1141
|
+
}
|
|
1142
|
+
function loadWatchState(filePath) {
|
|
1143
|
+
if (!(0, import_node_fs3.existsSync)(filePath)) return emptyState();
|
|
1144
|
+
try {
|
|
1145
|
+
const raw = JSON.parse((0, import_node_fs3.readFileSync)(filePath, "utf8"));
|
|
1146
|
+
if (raw.version !== 1 || typeof raw.agents !== "object") return emptyState();
|
|
1147
|
+
return raw;
|
|
1148
|
+
} catch {
|
|
1149
|
+
return emptyState();
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
function saveWatchState(filePath, state) {
|
|
1153
|
+
const tmp = filePath + ".tmp";
|
|
1154
|
+
try {
|
|
1155
|
+
(0, import_node_fs3.writeFileSync)(tmp, JSON.stringify(state, null, 2), "utf8");
|
|
1156
|
+
(0, import_node_fs3.renameSync)(tmp, filePath);
|
|
1157
|
+
} catch {
|
|
1158
|
+
try {
|
|
1159
|
+
(0, import_node_fs3.writeFileSync)(filePath, JSON.stringify(state, null, 2), "utf8");
|
|
1160
|
+
} catch {
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
function estimateInterval(history) {
|
|
1165
|
+
if (history.length < 3) return 0;
|
|
1166
|
+
const sorted = [...history].sort((a, b) => a - b);
|
|
1167
|
+
const deltas = [];
|
|
1168
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
1169
|
+
const d = sorted[i] - sorted[i - 1];
|
|
1170
|
+
if (d > 0) deltas.push(d);
|
|
1171
|
+
}
|
|
1172
|
+
if (deltas.length === 0) return 0;
|
|
1173
|
+
deltas.sort((a, b) => a - b);
|
|
1174
|
+
return deltas[Math.floor(deltas.length / 2)];
|
|
1175
|
+
}
|
|
1176
|
+
function detectTransitions(previous, currentRecords, config, now) {
|
|
1177
|
+
const alerts = [];
|
|
1178
|
+
const hasError = config.alertConditions.some((c) => c.type === "error");
|
|
1179
|
+
const hasRecovery = config.alertConditions.some((c) => c.type === "recovery");
|
|
1180
|
+
const staleConditions = config.alertConditions.filter((c) => c.type === "stale");
|
|
1181
|
+
const consecutiveConditions = config.alertConditions.filter((c) => c.type === "consecutive-errors");
|
|
1182
|
+
const byAgent = /* @__PURE__ */ new Map();
|
|
1183
|
+
for (const r of currentRecords) {
|
|
1184
|
+
const existing = byAgent.get(r.id);
|
|
1185
|
+
if (!existing || r.lastActive > existing.lastActive) {
|
|
1186
|
+
byAgent.set(r.id, r);
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
for (const [agentId, record] of byAgent) {
|
|
1190
|
+
const prev = previous.agents[agentId];
|
|
1191
|
+
const prevStatus = prev?.lastStatus ?? "unknown";
|
|
1192
|
+
const currStatus = record.status;
|
|
1193
|
+
if (hasError && currStatus === "error" && prevStatus !== "error") {
|
|
1194
|
+
if (canAlert(prev, "error", config.cooldownMs, now)) {
|
|
1195
|
+
alerts.push(makePayload(agentId, "error", prevStatus, currStatus, record, config.dirs));
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
if (hasRecovery && currStatus === "ok" && prevStatus === "error") {
|
|
1199
|
+
alerts.push(makePayload(agentId, "recovery", prevStatus, currStatus, record, config.dirs));
|
|
1200
|
+
}
|
|
1201
|
+
const newConsec = currStatus === "error" ? (prev?.consecutiveErrors ?? 0) + 1 : 0;
|
|
1202
|
+
for (const cond of consecutiveConditions) {
|
|
1203
|
+
if (newConsec === cond.threshold) {
|
|
1204
|
+
if (canAlert(prev, `consecutive-errors:${cond.threshold}`, config.cooldownMs, now)) {
|
|
1205
|
+
alerts.push(makePayload(
|
|
1206
|
+
agentId,
|
|
1207
|
+
`consecutive-errors (${cond.threshold})`,
|
|
1208
|
+
prevStatus,
|
|
1209
|
+
currStatus,
|
|
1210
|
+
{ ...record, detail: `${newConsec} consecutive errors. ${record.detail}` },
|
|
1211
|
+
config.dirs
|
|
1212
|
+
));
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
for (const cond of staleConditions) {
|
|
1217
|
+
const sinceActive = now - record.lastActive;
|
|
1218
|
+
if (sinceActive > cond.durationMs && record.lastActive > 0) {
|
|
1219
|
+
if (canAlert(prev, "stale", config.cooldownMs, now)) {
|
|
1220
|
+
const mins = Math.floor(sinceActive / 6e4);
|
|
1221
|
+
alerts.push(makePayload(
|
|
1222
|
+
agentId,
|
|
1223
|
+
"stale",
|
|
1224
|
+
prevStatus,
|
|
1225
|
+
currStatus,
|
|
1226
|
+
{ ...record, detail: `No update for ${mins}m. ${record.detail}` },
|
|
1227
|
+
config.dirs
|
|
1228
|
+
));
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
if (staleConditions.length === 0) {
|
|
1233
|
+
const history = prev?.mtimeHistory ?? [];
|
|
1234
|
+
const expectedInterval = estimateInterval(history);
|
|
1235
|
+
if (expectedInterval > 0) {
|
|
1236
|
+
const sinceActive = now - record.lastActive;
|
|
1237
|
+
if (sinceActive > expectedInterval * 3) {
|
|
1238
|
+
if (canAlert(prev, "stale-auto", config.cooldownMs, now)) {
|
|
1239
|
+
const mins = Math.floor(sinceActive / 6e4);
|
|
1240
|
+
const expectedMins = Math.floor(expectedInterval / 6e4);
|
|
1241
|
+
alerts.push(makePayload(
|
|
1242
|
+
agentId,
|
|
1243
|
+
"stale (auto)",
|
|
1244
|
+
prevStatus,
|
|
1245
|
+
currStatus,
|
|
1246
|
+
{ ...record, detail: `No update for ${mins}m (expected every ~${expectedMins}m). ${record.detail}` },
|
|
1247
|
+
config.dirs
|
|
1248
|
+
));
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
return alerts;
|
|
1255
|
+
}
|
|
1256
|
+
function updateWatchState(state, records, alerts, now) {
|
|
1257
|
+
const agents = { ...state.agents };
|
|
1258
|
+
const alertsByAgent = /* @__PURE__ */ new Map();
|
|
1259
|
+
for (const a of alerts) alertsByAgent.set(a.agentId, a);
|
|
1260
|
+
const byAgent = /* @__PURE__ */ new Map();
|
|
1261
|
+
for (const r of records) {
|
|
1262
|
+
const existing = byAgent.get(r.id);
|
|
1263
|
+
if (!existing || r.lastActive > existing.lastActive) {
|
|
1264
|
+
byAgent.set(r.id, r);
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
for (const [agentId, record] of byAgent) {
|
|
1268
|
+
const prev = agents[agentId];
|
|
1269
|
+
const history = prev?.mtimeHistory ?? [];
|
|
1270
|
+
const newHistory = [...history];
|
|
1271
|
+
if (newHistory.length === 0 || newHistory[newHistory.length - 1] !== record.lastActive) {
|
|
1272
|
+
newHistory.push(record.lastActive);
|
|
1273
|
+
}
|
|
1274
|
+
while (newHistory.length > 10) newHistory.shift();
|
|
1275
|
+
const alert = alertsByAgent.get(agentId);
|
|
1276
|
+
const consecutiveErrors = record.status === "error" ? (prev?.consecutiveErrors ?? 0) + 1 : 0;
|
|
1277
|
+
agents[agentId] = {
|
|
1278
|
+
id: agentId,
|
|
1279
|
+
lastStatus: record.status,
|
|
1280
|
+
lastActive: record.lastActive,
|
|
1281
|
+
lastAlertTime: alert ? now : prev?.lastAlertTime ?? 0,
|
|
1282
|
+
lastAlertReason: alert ? alert.condition : prev?.lastAlertReason ?? "",
|
|
1283
|
+
consecutiveErrors,
|
|
1284
|
+
mtimeHistory: newHistory
|
|
1285
|
+
};
|
|
1286
|
+
}
|
|
1287
|
+
return { version: 1, agents, lastPollTime: now };
|
|
1288
|
+
}
|
|
1289
|
+
function canAlert(prev, reason, cooldownMs, now) {
|
|
1290
|
+
if (!prev) return true;
|
|
1291
|
+
if (prev.lastAlertReason !== reason) return true;
|
|
1292
|
+
return now - prev.lastAlertTime > cooldownMs;
|
|
1293
|
+
}
|
|
1294
|
+
function makePayload(agentId, condition, previousStatus, currentStatus, record, dirs) {
|
|
1295
|
+
return {
|
|
1296
|
+
agentId,
|
|
1297
|
+
condition,
|
|
1298
|
+
previousStatus,
|
|
1299
|
+
currentStatus,
|
|
1300
|
+
detail: record.detail,
|
|
1301
|
+
file: record.file,
|
|
1302
|
+
timestamp: Date.now(),
|
|
1303
|
+
dirs
|
|
1304
|
+
};
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
// src/watch-alerts.ts
|
|
1308
|
+
var import_node_https = require("https");
|
|
1309
|
+
var import_node_http = require("http");
|
|
1310
|
+
var import_node_child_process2 = require("child_process");
|
|
1311
|
+
function formatAlertMessage(payload) {
|
|
1312
|
+
const time = new Date(payload.timestamp).toISOString();
|
|
1313
|
+
const arrow = `${payload.previousStatus} \u2192 ${payload.currentStatus}`;
|
|
1314
|
+
return [
|
|
1315
|
+
`[ALERT] ${payload.condition}: "${payload.agentId}"`,
|
|
1316
|
+
` Status: ${arrow}`,
|
|
1317
|
+
payload.detail ? ` Detail: ${payload.detail}` : null,
|
|
1318
|
+
` File: ${payload.file}`,
|
|
1319
|
+
` Time: ${time}`
|
|
1320
|
+
].filter(Boolean).join("\n");
|
|
1321
|
+
}
|
|
1322
|
+
function formatTelegram(payload) {
|
|
1323
|
+
const icon = payload.condition === "recovery" ? "\u2705" : "\u26A0\uFE0F";
|
|
1324
|
+
const time = new Date(payload.timestamp).toLocaleTimeString();
|
|
1325
|
+
return [
|
|
1326
|
+
`${icon} *AgentFlow Alert*`,
|
|
1327
|
+
`*${payload.condition}*: \`${payload.agentId}\``,
|
|
1328
|
+
`Status: ${payload.previousStatus} \u2192 ${payload.currentStatus}`,
|
|
1329
|
+
payload.detail ? `Detail: ${payload.detail.slice(0, 200)}` : null,
|
|
1330
|
+
`Time: ${time}`
|
|
1331
|
+
].filter(Boolean).join("\n");
|
|
1332
|
+
}
|
|
1333
|
+
async function sendAlert(payload, channel) {
|
|
1334
|
+
try {
|
|
1335
|
+
switch (channel.type) {
|
|
1336
|
+
case "stdout":
|
|
1337
|
+
sendStdout(payload);
|
|
1338
|
+
break;
|
|
1339
|
+
case "telegram":
|
|
1340
|
+
await sendTelegram(payload, channel.botToken, channel.chatId);
|
|
1341
|
+
break;
|
|
1342
|
+
case "webhook":
|
|
1343
|
+
await sendWebhook(payload, channel.url);
|
|
1344
|
+
break;
|
|
1345
|
+
case "command":
|
|
1346
|
+
await sendCommand(payload, channel.cmd);
|
|
1347
|
+
break;
|
|
1348
|
+
}
|
|
1349
|
+
} catch (err) {
|
|
1350
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1351
|
+
console.error(`[agentflow] Failed to send ${channel.type} alert: ${msg}`);
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
function sendStdout(payload) {
|
|
1355
|
+
console.log(formatAlertMessage(payload));
|
|
1356
|
+
}
|
|
1357
|
+
function sendTelegram(payload, botToken, chatId) {
|
|
1358
|
+
const body = JSON.stringify({
|
|
1359
|
+
chat_id: chatId,
|
|
1360
|
+
text: formatTelegram(payload),
|
|
1361
|
+
parse_mode: "Markdown"
|
|
1362
|
+
});
|
|
1363
|
+
return new Promise((resolve5, reject) => {
|
|
1364
|
+
const req = (0, import_node_https.request)(
|
|
1365
|
+
`https://api.telegram.org/bot${botToken}/sendMessage`,
|
|
1366
|
+
{ method: "POST", headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) } },
|
|
1367
|
+
(res) => {
|
|
1368
|
+
res.resume();
|
|
1369
|
+
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) resolve5();
|
|
1370
|
+
else reject(new Error(`Telegram API returned ${res.statusCode}`));
|
|
1371
|
+
}
|
|
1372
|
+
);
|
|
1373
|
+
req.on("error", reject);
|
|
1374
|
+
req.write(body);
|
|
1375
|
+
req.end();
|
|
1376
|
+
});
|
|
1377
|
+
}
|
|
1378
|
+
function sendWebhook(payload, url) {
|
|
1379
|
+
const body = JSON.stringify(payload);
|
|
1380
|
+
const isHttps = url.startsWith("https");
|
|
1381
|
+
const doRequest = isHttps ? import_node_https.request : import_node_http.request;
|
|
1382
|
+
return new Promise((resolve5, reject) => {
|
|
1383
|
+
const req = doRequest(
|
|
1384
|
+
url,
|
|
1385
|
+
{ method: "POST", headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) } },
|
|
1386
|
+
(res) => {
|
|
1387
|
+
res.resume();
|
|
1388
|
+
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) resolve5();
|
|
1389
|
+
else reject(new Error(`Webhook returned ${res.statusCode}`));
|
|
1390
|
+
}
|
|
1391
|
+
);
|
|
1392
|
+
req.on("error", reject);
|
|
1393
|
+
req.setTimeout(1e4, () => {
|
|
1394
|
+
req.destroy(new Error("Webhook timeout"));
|
|
1395
|
+
});
|
|
1396
|
+
req.write(body);
|
|
1397
|
+
req.end();
|
|
1398
|
+
});
|
|
1399
|
+
}
|
|
1400
|
+
function sendCommand(payload, cmd) {
|
|
1401
|
+
return new Promise((resolve5, reject) => {
|
|
1402
|
+
const env = {
|
|
1403
|
+
...process.env,
|
|
1404
|
+
AGENTFLOW_ALERT_AGENT: payload.agentId,
|
|
1405
|
+
AGENTFLOW_ALERT_CONDITION: payload.condition,
|
|
1406
|
+
AGENTFLOW_ALERT_STATUS: payload.currentStatus,
|
|
1407
|
+
AGENTFLOW_ALERT_PREVIOUS_STATUS: payload.previousStatus,
|
|
1408
|
+
AGENTFLOW_ALERT_DETAIL: payload.detail,
|
|
1409
|
+
AGENTFLOW_ALERT_FILE: payload.file,
|
|
1410
|
+
AGENTFLOW_ALERT_TIMESTAMP: String(payload.timestamp)
|
|
1411
|
+
};
|
|
1412
|
+
(0, import_node_child_process2.exec)(cmd, { env, timeout: 3e4 }, (err) => {
|
|
1413
|
+
if (err) reject(err);
|
|
1414
|
+
else resolve5();
|
|
1415
|
+
});
|
|
1416
|
+
});
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
// src/watch.ts
|
|
1420
|
+
function parseWatchArgs(argv) {
|
|
1421
|
+
const dirs = [];
|
|
1422
|
+
const alertConditions = [];
|
|
1423
|
+
const notifyChannels = [];
|
|
1424
|
+
let recursive = false;
|
|
1425
|
+
let pollIntervalMs = 3e4;
|
|
1426
|
+
let cooldownMs = 30 * 6e4;
|
|
1427
|
+
let stateFilePath = "";
|
|
1428
|
+
const args = argv.slice(0);
|
|
1429
|
+
if (args[0] === "watch") args.shift();
|
|
1430
|
+
let i = 0;
|
|
1431
|
+
while (i < args.length) {
|
|
1432
|
+
const arg = args[i];
|
|
1433
|
+
if (arg === "--help" || arg === "-h") {
|
|
1434
|
+
printWatchUsage();
|
|
1435
|
+
process.exit(0);
|
|
1436
|
+
} else if (arg === "--alert-on") {
|
|
1437
|
+
i++;
|
|
1438
|
+
const val = args[i] ?? "";
|
|
1439
|
+
if (val === "error") {
|
|
1440
|
+
alertConditions.push({ type: "error" });
|
|
1441
|
+
} else if (val === "recovery") {
|
|
1442
|
+
alertConditions.push({ type: "recovery" });
|
|
1443
|
+
} else if (val.startsWith("stale:")) {
|
|
1444
|
+
const dur = parseDuration(val.slice(6));
|
|
1445
|
+
if (dur > 0) alertConditions.push({ type: "stale", durationMs: dur });
|
|
1446
|
+
} else if (val.startsWith("consecutive-errors:")) {
|
|
1447
|
+
const n = parseInt(val.slice(19), 10);
|
|
1448
|
+
if (n > 0) alertConditions.push({ type: "consecutive-errors", threshold: n });
|
|
1449
|
+
}
|
|
1450
|
+
i++;
|
|
1451
|
+
} else if (arg === "--notify") {
|
|
1452
|
+
i++;
|
|
1453
|
+
const val = args[i] ?? "";
|
|
1454
|
+
if (val === "telegram") {
|
|
1455
|
+
const botToken = process.env["AGENTFLOW_TELEGRAM_BOT_TOKEN"] ?? "";
|
|
1456
|
+
const chatId = process.env["AGENTFLOW_TELEGRAM_CHAT_ID"] ?? "";
|
|
1457
|
+
if (botToken && chatId) {
|
|
1458
|
+
notifyChannels.push({ type: "telegram", botToken, chatId });
|
|
1459
|
+
} else {
|
|
1460
|
+
console.error("Warning: --notify telegram requires AGENTFLOW_TELEGRAM_BOT_TOKEN and AGENTFLOW_TELEGRAM_CHAT_ID env vars");
|
|
1461
|
+
}
|
|
1462
|
+
} else if (val.startsWith("webhook:")) {
|
|
1463
|
+
notifyChannels.push({ type: "webhook", url: val.slice(8) });
|
|
1464
|
+
} else if (val.startsWith("command:")) {
|
|
1465
|
+
notifyChannels.push({ type: "command", cmd: val.slice(8) });
|
|
1466
|
+
}
|
|
1467
|
+
i++;
|
|
1468
|
+
} else if (arg === "--poll") {
|
|
1469
|
+
i++;
|
|
1470
|
+
const v = parseInt(args[i] ?? "", 10);
|
|
1471
|
+
if (!isNaN(v) && v > 0) pollIntervalMs = v * 1e3;
|
|
1472
|
+
i++;
|
|
1473
|
+
} else if (arg === "--cooldown") {
|
|
1474
|
+
i++;
|
|
1475
|
+
const dur = parseDuration(args[i] ?? "30m");
|
|
1476
|
+
if (dur > 0) cooldownMs = dur;
|
|
1477
|
+
i++;
|
|
1478
|
+
} else if (arg === "--state-file") {
|
|
1479
|
+
i++;
|
|
1480
|
+
stateFilePath = args[i] ?? "";
|
|
1481
|
+
i++;
|
|
1482
|
+
} else if (arg === "--recursive" || arg === "-R") {
|
|
1483
|
+
recursive = true;
|
|
1484
|
+
i++;
|
|
1485
|
+
} else if (!arg.startsWith("-")) {
|
|
1486
|
+
dirs.push((0, import_node_path3.resolve)(arg));
|
|
1487
|
+
i++;
|
|
1488
|
+
} else {
|
|
1489
|
+
i++;
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
if (dirs.length === 0) dirs.push((0, import_node_path3.resolve)("."));
|
|
1493
|
+
if (alertConditions.length === 0) {
|
|
1494
|
+
alertConditions.push({ type: "error" });
|
|
1495
|
+
alertConditions.push({ type: "recovery" });
|
|
1496
|
+
}
|
|
1497
|
+
notifyChannels.unshift({ type: "stdout" });
|
|
1498
|
+
if (!stateFilePath) {
|
|
1499
|
+
stateFilePath = (0, import_node_path3.join)(dirs[0], ".agentflow-watch-state.json");
|
|
1500
|
+
}
|
|
1501
|
+
return {
|
|
1502
|
+
dirs,
|
|
1503
|
+
recursive,
|
|
1504
|
+
pollIntervalMs,
|
|
1505
|
+
alertConditions,
|
|
1506
|
+
notifyChannels,
|
|
1507
|
+
stateFilePath: (0, import_node_path3.resolve)(stateFilePath),
|
|
1508
|
+
cooldownMs
|
|
1509
|
+
};
|
|
1510
|
+
}
|
|
1511
|
+
function printWatchUsage() {
|
|
1512
|
+
console.log(`
|
|
1513
|
+
AgentFlow Watch \u2014 headless alert system for agent infrastructure.
|
|
1514
|
+
|
|
1515
|
+
Polls directories for JSON/JSONL files, detects failures and stale
|
|
1516
|
+
agents, sends alerts. Same auto-detection as \`agentflow live\`.
|
|
1517
|
+
|
|
1518
|
+
Usage:
|
|
1519
|
+
agentflow watch [dir...] [options]
|
|
1520
|
+
|
|
1521
|
+
Arguments:
|
|
1522
|
+
dir One or more directories to watch (default: .)
|
|
1523
|
+
|
|
1524
|
+
Alert conditions (--alert-on, repeatable):
|
|
1525
|
+
error Agent transitions to error status
|
|
1526
|
+
recovery Agent recovers from error to ok
|
|
1527
|
+
stale:DURATION No file update within duration (e.g. 15m, 1h)
|
|
1528
|
+
consecutive-errors:N N consecutive error observations
|
|
1529
|
+
|
|
1530
|
+
Default (if none specified): error + recovery
|
|
1531
|
+
|
|
1532
|
+
Notification channels (--notify, repeatable):
|
|
1533
|
+
telegram Telegram Bot API (needs env vars)
|
|
1534
|
+
webhook:URL POST JSON to any URL
|
|
1535
|
+
command:CMD Run shell command with alert env vars
|
|
1536
|
+
|
|
1537
|
+
Stdout alerts are always printed regardless of --notify flags.
|
|
1538
|
+
|
|
1539
|
+
Options:
|
|
1540
|
+
--poll <secs> Poll interval in seconds (default: 30)
|
|
1541
|
+
--cooldown <duration> Alert dedup cooldown (default: 30m)
|
|
1542
|
+
--state-file <path> Persistence file (default: <dir>/.agentflow-watch-state.json)
|
|
1543
|
+
-R, --recursive Scan subdirectories (1 level deep)
|
|
1544
|
+
-h, --help Show this help message
|
|
1545
|
+
|
|
1546
|
+
Environment variables:
|
|
1547
|
+
AGENTFLOW_TELEGRAM_BOT_TOKEN Telegram bot token (for --notify telegram)
|
|
1548
|
+
AGENTFLOW_TELEGRAM_CHAT_ID Telegram chat ID (for --notify telegram)
|
|
1549
|
+
|
|
1550
|
+
Examples:
|
|
1551
|
+
agentflow watch ./data --alert-on error --alert-on stale:15m
|
|
1552
|
+
agentflow watch ./data ./cron --notify telegram --poll 60
|
|
1553
|
+
agentflow watch ./traces --notify webhook:https://hooks.slack.com/... --alert-on consecutive-errors:3
|
|
1554
|
+
agentflow watch ./data --notify "command:curl -X POST https://my-pagerduty/alert"
|
|
1555
|
+
`.trim());
|
|
1556
|
+
}
|
|
1557
|
+
function startWatch(argv) {
|
|
1558
|
+
const config = parseWatchArgs(argv);
|
|
1559
|
+
const valid = config.dirs.filter((d) => (0, import_node_fs4.existsSync)(d));
|
|
1560
|
+
if (valid.length === 0) {
|
|
1561
|
+
console.error(`No valid directories found: ${config.dirs.join(", ")}`);
|
|
1562
|
+
process.exit(1);
|
|
1563
|
+
}
|
|
1564
|
+
const invalid = config.dirs.filter((d) => !(0, import_node_fs4.existsSync)(d));
|
|
1565
|
+
if (invalid.length > 0) {
|
|
1566
|
+
console.warn(`Skipping non-existent: ${invalid.join(", ")}`);
|
|
1567
|
+
}
|
|
1568
|
+
let state = loadWatchState(config.stateFilePath);
|
|
1569
|
+
const condLabels = config.alertConditions.map((c) => {
|
|
1570
|
+
if (c.type === "stale") return `stale:${Math.floor(c.durationMs / 6e4)}m`;
|
|
1571
|
+
if (c.type === "consecutive-errors") return `consecutive-errors:${c.threshold}`;
|
|
1572
|
+
return c.type;
|
|
1573
|
+
});
|
|
1574
|
+
const channelLabels = config.notifyChannels.filter((c) => c.type !== "stdout").map((c) => {
|
|
1575
|
+
if (c.type === "webhook") return `webhook:${c.url.slice(0, 40)}...`;
|
|
1576
|
+
if (c.type === "command") return `command:${c.cmd.slice(0, 40)}`;
|
|
1577
|
+
return c.type;
|
|
1578
|
+
});
|
|
1579
|
+
console.log(`
|
|
1580
|
+
agentflow watch started`);
|
|
1581
|
+
console.log(` Directories: ${valid.join(", ")}`);
|
|
1582
|
+
console.log(` Poll: ${config.pollIntervalMs / 1e3}s`);
|
|
1583
|
+
console.log(` Alert on: ${condLabels.join(", ")}`);
|
|
1584
|
+
console.log(` Notify: stdout${channelLabels.length > 0 ? ", " + channelLabels.join(", ") : ""}`);
|
|
1585
|
+
console.log(` Cooldown: ${Math.floor(config.cooldownMs / 6e4)}m`);
|
|
1586
|
+
console.log(` State: ${config.stateFilePath}`);
|
|
1587
|
+
console.log(` Hostname: ${(0, import_node_os.hostname)()}`);
|
|
1588
|
+
console.log("");
|
|
1589
|
+
let pollCount = 0;
|
|
1590
|
+
async function poll() {
|
|
1591
|
+
const now = Date.now();
|
|
1592
|
+
pollCount++;
|
|
1593
|
+
const files = scanFiles(valid, config.recursive);
|
|
1594
|
+
const records = [];
|
|
1595
|
+
for (const f of files.slice(0, 500)) {
|
|
1596
|
+
const recs = f.ext === ".jsonl" ? processJsonlFile(f) : processJsonFile(f);
|
|
1597
|
+
records.push(...recs);
|
|
1598
|
+
}
|
|
1599
|
+
const alerts = detectTransitions(state, records, config, now);
|
|
1600
|
+
for (const alert of alerts) {
|
|
1601
|
+
for (const channel of config.notifyChannels) {
|
|
1602
|
+
await sendAlert(alert, channel);
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
state = updateWatchState(state, records, alerts, now);
|
|
1606
|
+
saveWatchState(config.stateFilePath, state);
|
|
1607
|
+
if (pollCount % 10 === 0) {
|
|
1608
|
+
const agentCount = Object.keys(state.agents).length;
|
|
1609
|
+
const errorCount = Object.values(state.agents).filter((a) => a.lastStatus === "error").length;
|
|
1610
|
+
const runningCount = Object.values(state.agents).filter((a) => a.lastStatus === "running").length;
|
|
1611
|
+
const time = (/* @__PURE__ */ new Date()).toLocaleTimeString();
|
|
1612
|
+
console.log(`[${time}] heartbeat: ${agentCount} agents, ${runningCount} running, ${errorCount} errors, ${files.length} files`);
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
poll();
|
|
1616
|
+
setInterval(() => {
|
|
1617
|
+
poll();
|
|
1618
|
+
}, config.pollIntervalMs);
|
|
1619
|
+
function shutdown() {
|
|
1620
|
+
console.log("\nagentflow watch stopped.");
|
|
1621
|
+
saveWatchState(config.stateFilePath, state);
|
|
1622
|
+
process.exit(0);
|
|
1623
|
+
}
|
|
1624
|
+
process.on("SIGINT", shutdown);
|
|
1625
|
+
process.on("SIGTERM", shutdown);
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1034
1628
|
// src/cli.ts
|
|
1035
1629
|
function printHelp() {
|
|
1036
1630
|
console.log(`
|
|
@@ -1040,8 +1634,9 @@ Usage:
|
|
|
1040
1634
|
agentflow <command> [options]
|
|
1041
1635
|
|
|
1042
1636
|
Commands:
|
|
1043
|
-
run
|
|
1044
|
-
live
|
|
1637
|
+
run [options] -- <cmd> Wrap a command with automatic execution tracing
|
|
1638
|
+
live [dir...] [options] Real-time terminal monitor (auto-detects any JSON/JSONL)
|
|
1639
|
+
watch [dir...] [options] Headless alert system \u2014 detects failures, sends notifications
|
|
1045
1640
|
|
|
1046
1641
|
Run \`agentflow <command> --help\` for command-specific options.
|
|
1047
1642
|
|
|
@@ -1049,7 +1644,8 @@ Examples:
|
|
|
1049
1644
|
agentflow run --traces-dir ./traces -- python -m myagent process
|
|
1050
1645
|
agentflow live ./data
|
|
1051
1646
|
agentflow live ./traces ./cron ./workers -R
|
|
1052
|
-
agentflow
|
|
1647
|
+
agentflow watch ./data --alert-on error --notify telegram
|
|
1648
|
+
agentflow watch ./data ./cron --alert-on stale:15m --notify webhook:https://...
|
|
1053
1649
|
`.trim());
|
|
1054
1650
|
}
|
|
1055
1651
|
function parseRunArgs(argv) {
|
|
@@ -1187,7 +1783,8 @@ async function runCommand(argv) {
|
|
|
1187
1783
|
}
|
|
1188
1784
|
async function main() {
|
|
1189
1785
|
const argv = process.argv.slice(2);
|
|
1190
|
-
|
|
1786
|
+
const knownCommands = ["run", "live", "watch"];
|
|
1787
|
+
if (argv.length === 0 || !knownCommands.includes(argv[0]) && (argv.includes("--help") || argv.includes("-h"))) {
|
|
1191
1788
|
printHelp();
|
|
1192
1789
|
process.exit(0);
|
|
1193
1790
|
}
|
|
@@ -1199,6 +1796,9 @@ async function main() {
|
|
|
1199
1796
|
case "live":
|
|
1200
1797
|
startLive(argv);
|
|
1201
1798
|
break;
|
|
1799
|
+
case "watch":
|
|
1800
|
+
startWatch(argv);
|
|
1801
|
+
break;
|
|
1202
1802
|
default:
|
|
1203
1803
|
if (!subcommand?.startsWith("-")) {
|
|
1204
1804
|
startLive(["live", ...argv]);
|