- senses build to dist/senses/<name>/index.js - workflows build to dist/workflows/<name>/index.js - scripts/build.mjs: clean dist/ before build, output to dist/ - .gitignore: simplified to just dist/ 小橘 <xiaoju@shazhou.work>
111 lines
3.6 KiB
JavaScript
111 lines
3.6 KiB
JavaScript
// senses/hermes-session-message-stats/src/index.ts
|
|
import { createReadStream } from "node:fs";
|
|
import { readdir } from "node:fs/promises";
|
|
import { homedir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { createInterface } from "node:readline";
|
|
|
|
// senses/hermes-session-message-stats/src/schema.ts
|
|
import { integer, sqliteTable } from "drizzle-orm/sqlite-core";
|
|
var hermesSessionMessageStats = sqliteTable("hermes_session_message_stats", {
|
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
ts: integer("ts").notNull(),
|
|
totalUserMessages: integer("total_user_messages").notNull(),
|
|
totalAssistantMessages: integer("total_assistant_messages").notNull(),
|
|
totalToolMessages: integer("total_tool_messages").notNull(),
|
|
totalMessages: integer("total_messages").notNull(),
|
|
activeSessions: integer("active_sessions").notNull(),
|
|
measurementWindowSeconds: integer("measurement_window_seconds").notNull()
|
|
});
|
|
|
|
// senses/hermes-session-message-stats/src/index.ts
|
|
var MEASUREMENT_WINDOW_MS = 9e5;
|
|
var MEASUREMENT_WINDOW_SECONDS = 900;
|
|
async function aggregateJsonlFile(filePath, cutoffMs, nowMs) {
|
|
let user = 0;
|
|
let assistant = 0;
|
|
let tool = 0;
|
|
let fileHadActivity = false;
|
|
const input = createReadStream(filePath, { encoding: "utf8" });
|
|
const rl = createInterface({ input, crlfDelay: Infinity });
|
|
try {
|
|
for await (const line of rl) {
|
|
const trimmed = line.trim();
|
|
if (!trimmed) continue;
|
|
let obj;
|
|
try {
|
|
obj = JSON.parse(trimmed);
|
|
} catch {
|
|
continue;
|
|
}
|
|
if (typeof obj !== "object" || obj === null || typeof obj.role !== "string" || typeof obj.timestamp !== "string") {
|
|
continue;
|
|
}
|
|
const record = obj;
|
|
const t = Date.parse(record.timestamp);
|
|
if (!Number.isFinite(t) || t < cutoffMs || t > nowMs) continue;
|
|
const roleNorm = record.role.trim().toLowerCase();
|
|
if (roleNorm === "user") {
|
|
user++;
|
|
fileHadActivity = true;
|
|
} else if (roleNorm === "assistant") {
|
|
assistant++;
|
|
fileHadActivity = true;
|
|
} else if (roleNorm === "tool") {
|
|
tool++;
|
|
fileHadActivity = true;
|
|
}
|
|
}
|
|
} finally {
|
|
rl.close();
|
|
}
|
|
return { user, assistant, tool, fileHadActivity };
|
|
}
|
|
async function compute() {
|
|
const nowMs = Date.now();
|
|
const cutoffMs = nowMs - MEASUREMENT_WINDOW_MS;
|
|
const ts = nowMs;
|
|
let totalUserMessages = 0;
|
|
let totalAssistantMessages = 0;
|
|
let totalToolMessages = 0;
|
|
let activeSessions = 0;
|
|
const sessionsDir = join(homedir(), ".hermes", "sessions");
|
|
let files = [];
|
|
try {
|
|
const entries = await readdir(sessionsDir, { withFileTypes: true });
|
|
files = entries.filter((e) => e.isFile() && e.name.endsWith(".jsonl")).map((e) => join(sessionsDir, e.name));
|
|
} catch (err) {
|
|
if (err && typeof err === "object" && "code" in err && err.code === "ENOENT") {
|
|
files = [];
|
|
} else {
|
|
throw err;
|
|
}
|
|
}
|
|
for (const filePath of files) {
|
|
const { user, assistant, tool, fileHadActivity } = await aggregateJsonlFile(
|
|
filePath,
|
|
cutoffMs,
|
|
nowMs
|
|
);
|
|
totalUserMessages += user;
|
|
totalAssistantMessages += assistant;
|
|
totalToolMessages += tool;
|
|
if (fileHadActivity) activeSessions++;
|
|
}
|
|
const totalMessages = totalUserMessages + totalAssistantMessages + totalToolMessages;
|
|
const row = {
|
|
ts,
|
|
totalUserMessages,
|
|
totalAssistantMessages,
|
|
totalToolMessages,
|
|
totalMessages,
|
|
activeSessions,
|
|
measurementWindowSeconds: MEASUREMENT_WINDOW_SECONDS
|
|
};
|
|
return { signal: row, workflow: null };
|
|
}
|
|
export {
|
|
compute,
|
|
hermesSessionMessageStats as table
|
|
};
|