@agents-tower/core 0.1.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/adapters/claude.d.ts +2 -0
- package/dist/adapters/claude.js +43 -0
- package/dist/adapters/claude.js.map +1 -0
- package/dist/adapters/codex-cloud.d.ts +4 -0
- package/dist/adapters/codex-cloud.js +80 -0
- package/dist/adapters/codex-cloud.js.map +1 -0
- package/dist/adapters/codex-local.d.ts +17 -0
- package/dist/adapters/codex-local.js +180 -0
- package/dist/adapters/codex-local.js.map +1 -0
- package/dist/adapters/cursor-cloud.d.ts +2 -0
- package/dist/adapters/cursor-cloud.js +54 -0
- package/dist/adapters/cursor-cloud.js.map +1 -0
- package/dist/adapters/cursor-local.d.ts +2 -0
- package/dist/adapters/cursor-local.js +37 -0
- package/dist/adapters/cursor-local.js.map +1 -0
- package/dist/adapters/helpers.d.ts +15 -0
- package/dist/adapters/helpers.js +33 -0
- package/dist/adapters/helpers.js.map +1 -0
- package/dist/adapters/hermes.d.ts +2 -0
- package/dist/adapters/hermes.js +41 -0
- package/dist/adapters/hermes.js.map +1 -0
- package/dist/adapters/index.d.ts +2 -0
- package/dist/adapters/index.js +37 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/openclaw.d.ts +2 -0
- package/dist/adapters/openclaw.js +41 -0
- package/dist/adapters/openclaw.js.map +1 -0
- package/dist/adapters/presence.d.ts +2 -0
- package/dist/adapters/presence.js +36 -0
- package/dist/adapters/presence.js.map +1 -0
- package/dist/adapters/static-source.d.ts +15 -0
- package/dist/adapters/static-source.js +41 -0
- package/dist/adapters/static-source.js.map +1 -0
- package/dist/adapters/types.d.ts +44 -0
- package/dist/adapters/types.js +3 -0
- package/dist/adapters/types.js.map +1 -0
- package/dist/app-server.d.ts +94 -0
- package/dist/app-server.js +382 -0
- package/dist/app-server.js.map +1 -0
- package/dist/app-settings.d.ts +52 -0
- package/dist/app-settings.js +268 -0
- package/dist/app-settings.js.map +1 -0
- package/dist/appearance.d.ts +6 -0
- package/dist/appearance.js +84 -0
- package/dist/appearance.js.map +1 -0
- package/dist/aseprite.d.ts +4 -0
- package/dist/aseprite.js +42 -0
- package/dist/aseprite.js.map +1 -0
- package/dist/claude-agent-sdk.d.ts +44 -0
- package/dist/claude-agent-sdk.js +347 -0
- package/dist/claude-agent-sdk.js.map +1 -0
- package/dist/claude-home-cache.d.ts +61 -0
- package/dist/claude-home-cache.js +438 -0
- package/dist/claude-home-cache.js.map +1 -0
- package/dist/claude.d.ts +154 -0
- package/dist/claude.js +3495 -0
- package/dist/claude.js.map +1 -0
- package/dist/cloud.d.ts +2 -0
- package/dist/cloud.js +25 -0
- package/dist/cloud.js.map +1 -0
- package/dist/codex-command.d.ts +25 -0
- package/dist/codex-command.js +235 -0
- package/dist/codex-command.js.map +1 -0
- package/dist/codex-session-files.d.ts +8 -0
- package/dist/codex-session-files.js +509 -0
- package/dist/codex-session-files.js.map +1 -0
- package/dist/codex-thread-query.d.ts +14 -0
- package/dist/codex-thread-query.js +82 -0
- package/dist/codex-thread-query.js.map +1 -0
- package/dist/cursor-cloud-data.d.ts +34 -0
- package/dist/cursor-cloud-data.js +344 -0
- package/dist/cursor-cloud-data.js.map +1 -0
- package/dist/cursor-lib/local-discovery.d.ts +17 -0
- package/dist/cursor-lib/local-discovery.js +251 -0
- package/dist/cursor-lib/local-discovery.js.map +1 -0
- package/dist/cursor-lib/shared.d.ts +5 -0
- package/dist/cursor-lib/shared.js +88 -0
- package/dist/cursor-lib/shared.js.map +1 -0
- package/dist/cursor.d.ts +8 -0
- package/dist/cursor.js +1584 -0
- package/dist/cursor.js.map +1 -0
- package/dist/domain/codex-turn-semantics.d.ts +4 -0
- package/dist/domain/codex-turn-semantics.js +49 -0
- package/dist/domain/codex-turn-semantics.js.map +1 -0
- package/dist/domain/workload-policy.d.ts +8 -0
- package/dist/domain/workload-policy.js +268 -0
- package/dist/domain/workload-policy.js.map +1 -0
- package/dist/domain/workspace-activity.d.ts +10 -0
- package/dist/domain/workspace-activity.js +372 -0
- package/dist/domain/workspace-activity.js.map +1 -0
- package/dist/goal.d.ts +11 -0
- package/dist/goal.js +76 -0
- package/dist/goal.js.map +1 -0
- package/dist/hermes-hook-install.d.ts +12 -0
- package/dist/hermes-hook-install.js +335 -0
- package/dist/hermes-hook-install.js.map +1 -0
- package/dist/hermes.d.ts +97 -0
- package/dist/hermes.js +2446 -0
- package/dist/hermes.js.map +1 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.js +47 -0
- package/dist/index.js.map +1 -0
- package/dist/live-monitor-lib/events.d.ts +43 -0
- package/dist/live-monitor-lib/events.js +1463 -0
- package/dist/live-monitor-lib/events.js.map +1 -0
- package/dist/live-monitor-lib/rollout-hooks.d.ts +13 -0
- package/dist/live-monitor-lib/rollout-hooks.js +253 -0
- package/dist/live-monitor-lib/rollout-hooks.js.map +1 -0
- package/dist/live-monitor.d.ts +102 -0
- package/dist/live-monitor.js +1461 -0
- package/dist/live-monitor.js.map +1 -0
- package/dist/local-thread-selection.d.ts +2 -0
- package/dist/local-thread-selection.js +58 -0
- package/dist/local-thread-selection.js.map +1 -0
- package/dist/openclaw.d.ts +41 -0
- package/dist/openclaw.js +715 -0
- package/dist/openclaw.js.map +1 -0
- package/dist/presence.d.ts +9 -0
- package/dist/presence.js +108 -0
- package/dist/presence.js.map +1 -0
- package/dist/project-identity.d.ts +7 -0
- package/dist/project-identity.js +162 -0
- package/dist/project-identity.js.map +1 -0
- package/dist/project-paths.d.ts +23 -0
- package/dist/project-paths.js +394 -0
- package/dist/project-paths.js.map +1 -0
- package/dist/project-storage.d.ts +7 -0
- package/dist/project-storage.js +79 -0
- package/dist/project-storage.js.map +1 -0
- package/dist/room-config.d.ts +21 -0
- package/dist/room-config.js +230 -0
- package/dist/room-config.js.map +1 -0
- package/dist/services/project-discovery.d.ts +1 -0
- package/dist/services/project-discovery.js +16 -0
- package/dist/services/project-discovery.js.map +1 -0
- package/dist/services/project-live-monitor.d.ts +1 -0
- package/dist/services/project-live-monitor.js +6 -0
- package/dist/services/project-live-monitor.js.map +1 -0
- package/dist/services/project-snapshot-coordinator.d.ts +49 -0
- package/dist/services/project-snapshot-coordinator.js +152 -0
- package/dist/services/project-snapshot-coordinator.js.map +1 -0
- package/dist/services/refresh-scheduler.d.ts +8 -0
- package/dist/services/refresh-scheduler.js +44 -0
- package/dist/services/refresh-scheduler.js.map +1 -0
- package/dist/services/snapshot-assembler.d.ts +8 -0
- package/dist/services/snapshot-assembler.js +167 -0
- package/dist/services/snapshot-assembler.js.map +1 -0
- package/dist/services/thread-store.d.ts +4 -0
- package/dist/services/thread-store.js +8 -0
- package/dist/services/thread-store.js.map +1 -0
- package/dist/snapshot-lib/activity-summary.d.ts +3 -0
- package/dist/snapshot-lib/activity-summary.js +220 -0
- package/dist/snapshot-lib/activity-summary.js.map +1 -0
- package/dist/snapshot-lib/dashboard-builder.d.ts +21 -0
- package/dist/snapshot-lib/dashboard-builder.js +146 -0
- package/dist/snapshot-lib/dashboard-builder.js.map +1 -0
- package/dist/snapshot-lib/thread-summary.d.ts +27 -0
- package/dist/snapshot-lib/thread-summary.js +953 -0
- package/dist/snapshot-lib/thread-summary.js.map +1 -0
- package/dist/snapshot.d.ts +2 -0
- package/dist/snapshot.js +21 -0
- package/dist/snapshot.js.map +1 -0
- package/dist/types.d.ts +368 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/json.d.ts +4 -0
- package/dist/utils/json.js +29 -0
- package/dist/utils/json.js.map +1 -0
- package/dist/utils/text.d.ts +3 -0
- package/dist/utils/text.js +26 -0
- package/dist/utils/text.js.map +1 -0
- package/dist/web-search.d.ts +1 -0
- package/dist/web-search.js +52 -0
- package/dist/web-search.js.map +1 -0
- package/dist/workload.d.ts +1 -0
- package/dist/workload.js +12 -0
- package/dist/workload.js.map +1 -0
- package/package.json +19 -0
package/dist/hermes.js
ADDED
|
@@ -0,0 +1,2446 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.loadHermesProjectSnapshotData = loadHermesProjectSnapshotData;
|
|
4
|
+
exports.discoverHermesProjects = discoverHermesProjects;
|
|
5
|
+
exports.loadRoamingHermesSnapshotData = loadRoamingHermesSnapshotData;
|
|
6
|
+
exports.summarizeHermesSessionForTest = summarizeHermesSessionForTest;
|
|
7
|
+
exports.summarizeHermesHookSessionForTest = summarizeHermesHookSessionForTest;
|
|
8
|
+
exports.isHermesGatewayDaemonForTest = isHermesGatewayDaemonForTest;
|
|
9
|
+
exports.isHermesRuntimeHomeProcessForTest = isHermesRuntimeHomeProcessForTest;
|
|
10
|
+
exports.isHermesCompressionContinuationForTest = isHermesCompressionContinuationForTest;
|
|
11
|
+
exports.currentHermesSessionProjectRootForTest = currentHermesSessionProjectRootForTest;
|
|
12
|
+
const node_child_process_1 = require("node:child_process");
|
|
13
|
+
const node_fs_1 = require("node:fs");
|
|
14
|
+
const promises_1 = require("node:fs/promises");
|
|
15
|
+
const node_os_1 = require("node:os");
|
|
16
|
+
const node_path_1 = require("node:path");
|
|
17
|
+
const node_process_1 = require("node:process");
|
|
18
|
+
const node_util_1 = require("node:util");
|
|
19
|
+
const appearance_1 = require("./appearance");
|
|
20
|
+
const hermes_hook_install_1 = require("./hermes-hook-install");
|
|
21
|
+
const project_paths_1 = require("./project-paths");
|
|
22
|
+
const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
23
|
+
const DEFAULT_HERMES_SESSION_LIMIT = 12;
|
|
24
|
+
const HERMES_SESSION_SCAN_LIMIT = 30;
|
|
25
|
+
const HERMES_PROJECT_DISCOVERY_HOOK_WINDOW_MS = 2 * 60 * 60 * 1000;
|
|
26
|
+
const HERMES_RECENT_DONE_WINDOW_MS = 5 * 60 * 1000;
|
|
27
|
+
const HERMES_RECENT_OPEN_WINDOW_MS = 20 * 60 * 1000;
|
|
28
|
+
const HERMES_SQLITE_MESSAGE_LIMIT = 10;
|
|
29
|
+
const HERMES_SQLITE_TEXT_LIMIT = 2000;
|
|
30
|
+
const HERMES_SQLITE_TIMEOUT_MS = 5000;
|
|
31
|
+
const HERMES_SQLITE_MAX_BUFFER = 8 * 1024 * 1024;
|
|
32
|
+
const HERMES_HOOK_FILE_BYTE_LIMIT = 1024 * 1024;
|
|
33
|
+
const HERMES_HOOK_LINE_BYTE_LIMIT = 64 * 1024;
|
|
34
|
+
const HERMES_HOOK_RECORD_LIMIT = 80;
|
|
35
|
+
// Keep a project relation through 20 rootless hook actions, then treat the stream as projectless.
|
|
36
|
+
const HERMES_PROJECT_RELATION_ACTION_WINDOW = 21;
|
|
37
|
+
const HERMES_HOOK_TEXT_LIMIT = 1500;
|
|
38
|
+
const HERMES_HOOK_PATH_LIMIT = 32;
|
|
39
|
+
const HERMES_TRANSIENT_PROJECT_ROOTS = new Set(["/tmp", "/var/tmp", "/dev/shm"]);
|
|
40
|
+
const HERMES_CRON_SESSION_ID_RE = /^cron_[A-Za-z0-9_-]+_\d{8}_\d{6}$/i;
|
|
41
|
+
function hermesLocalSessionLimit() {
|
|
42
|
+
const raw = Number.parseInt(process.env.HERMES_LOCAL_SESSION_LIMIT ?? "", 10);
|
|
43
|
+
if (!Number.isFinite(raw)) {
|
|
44
|
+
return DEFAULT_HERMES_SESSION_LIMIT;
|
|
45
|
+
}
|
|
46
|
+
return Math.max(1, Math.min(50, raw));
|
|
47
|
+
}
|
|
48
|
+
function hermesScanLimit(limit) {
|
|
49
|
+
return Math.max(1, Math.min(HERMES_SESSION_SCAN_LIMIT, limit));
|
|
50
|
+
}
|
|
51
|
+
async function looksLikeHermesRuntimeProjectRoot(root) {
|
|
52
|
+
const normalized = root.replace(/\\/g, "/").toLowerCase();
|
|
53
|
+
if (!normalized.endsWith("/hermes-agent")) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
return await pathExists((0, node_path_1.join)(root, "hermes_cli", "main.py"))
|
|
57
|
+
&& await pathExists((0, node_path_1.join)(root, "gateway"))
|
|
58
|
+
&& await pathExists((0, node_path_1.join)(root, "pyproject.toml"));
|
|
59
|
+
}
|
|
60
|
+
async function pathExists(path) {
|
|
61
|
+
try {
|
|
62
|
+
await (0, promises_1.access)(hostFilesystemPath(path), node_fs_1.constants.F_OK);
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function expandHomePath(path) {
|
|
70
|
+
const trimmed = path.trim();
|
|
71
|
+
if (trimmed === "~") {
|
|
72
|
+
return (0, node_os_1.homedir)();
|
|
73
|
+
}
|
|
74
|
+
if (trimmed.startsWith("~/") || trimmed.startsWith("~\\")) {
|
|
75
|
+
return (0, node_path_1.join)((0, node_os_1.homedir)(), trimmed.slice(2));
|
|
76
|
+
}
|
|
77
|
+
return trimmed;
|
|
78
|
+
}
|
|
79
|
+
function normalizeFilesystemPath(path) {
|
|
80
|
+
if (typeof path !== "string" || path.trim().length === 0) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
return (0, project_paths_1.canonicalizeProjectPath)(expandHomePath(path)) ?? null;
|
|
84
|
+
}
|
|
85
|
+
function hostFilesystemPath(path) {
|
|
86
|
+
const canonical = (0, project_paths_1.canonicalizeProjectPath)(path) ?? path;
|
|
87
|
+
const match = canonical.match(/^\/mnt\/([a-zA-Z])\/(.*)$/);
|
|
88
|
+
if (node_process_1.platform !== "win32" || !match) {
|
|
89
|
+
return path;
|
|
90
|
+
}
|
|
91
|
+
const drive = match[1].toUpperCase();
|
|
92
|
+
const rest = match[2].replace(/\//g, "\\");
|
|
93
|
+
return `${drive}:\\${rest}`;
|
|
94
|
+
}
|
|
95
|
+
function hermesHookDirectories() {
|
|
96
|
+
const explicit = normalizeFilesystemPath(process.env.CODEX_AGENTS_OFFICE_HERMES_HOOK_DIR);
|
|
97
|
+
if (explicit) {
|
|
98
|
+
return [explicit];
|
|
99
|
+
}
|
|
100
|
+
const dirs = [];
|
|
101
|
+
const seen = new Set();
|
|
102
|
+
const add = (path) => {
|
|
103
|
+
const normalized = normalizeFilesystemPath(path);
|
|
104
|
+
const key = (0, project_paths_1.projectPathIdentityKey)(normalized);
|
|
105
|
+
if (!normalized || !key || seen.has(key)) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
seen.add(key);
|
|
109
|
+
dirs.push(normalized);
|
|
110
|
+
};
|
|
111
|
+
add((0, hermes_hook_install_1.getHermesHooksDir)());
|
|
112
|
+
add((0, node_path_1.join)((0, node_os_1.homedir)(), ".codex", "codex-agents-office", "hermes-hooks"));
|
|
113
|
+
return dirs;
|
|
114
|
+
}
|
|
115
|
+
async function hermesHomeCandidatesFromDisk() {
|
|
116
|
+
const roots = new Set();
|
|
117
|
+
const explicitHome = normalizeFilesystemPath(process.env.HERMES_HOME);
|
|
118
|
+
if (explicitHome) {
|
|
119
|
+
roots.add(explicitHome);
|
|
120
|
+
}
|
|
121
|
+
const defaultRoot = normalizeFilesystemPath((0, node_path_1.join)((0, node_os_1.homedir)(), ".hermes"));
|
|
122
|
+
if (defaultRoot) {
|
|
123
|
+
roots.add(defaultRoot);
|
|
124
|
+
}
|
|
125
|
+
const rootCandidates = [...roots];
|
|
126
|
+
for (const root of rootCandidates) {
|
|
127
|
+
const profilesDir = (0, node_path_1.join)(root, "profiles");
|
|
128
|
+
const profiles = await (0, promises_1.readdir)(hostFilesystemPath(profilesDir), { withFileTypes: true }).catch(() => []);
|
|
129
|
+
for (const entry of profiles) {
|
|
130
|
+
if (!entry.isDirectory()) {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const profileHome = normalizeFilesystemPath((0, node_path_1.join)(profilesDir, entry.name));
|
|
134
|
+
if (profileHome) {
|
|
135
|
+
roots.add(profileHome);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const explicitStateDb = normalizeFilesystemPath(process.env.HERMES_STATE_DB);
|
|
140
|
+
if (explicitStateDb) {
|
|
141
|
+
roots.add((0, node_path_1.resolve)(explicitStateDb, ".."));
|
|
142
|
+
}
|
|
143
|
+
return [...roots];
|
|
144
|
+
}
|
|
145
|
+
async function readProcFile(path) {
|
|
146
|
+
try {
|
|
147
|
+
return await (0, promises_1.readFile)(path);
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function splitNulFile(raw) {
|
|
154
|
+
if (!raw) {
|
|
155
|
+
return [];
|
|
156
|
+
}
|
|
157
|
+
return raw
|
|
158
|
+
.toString("utf8")
|
|
159
|
+
.split("\0")
|
|
160
|
+
.map((entry) => entry.trim())
|
|
161
|
+
.filter(Boolean);
|
|
162
|
+
}
|
|
163
|
+
function parseProcEnviron(raw) {
|
|
164
|
+
const env = {};
|
|
165
|
+
for (const entry of splitNulFile(raw)) {
|
|
166
|
+
const equalsIndex = entry.indexOf("=");
|
|
167
|
+
if (equalsIndex <= 0) {
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
env[entry.slice(0, equalsIndex)] = entry.slice(equalsIndex + 1);
|
|
171
|
+
}
|
|
172
|
+
return env;
|
|
173
|
+
}
|
|
174
|
+
function looksLikeHermesProcess(argv) {
|
|
175
|
+
if (argv.length === 0) {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
return argv.some((arg) => {
|
|
179
|
+
const normalized = arg.replace(/\\/g, "/").toLowerCase();
|
|
180
|
+
const base = (0, node_path_1.basename)(normalized);
|
|
181
|
+
return base === "hermes"
|
|
182
|
+
|| base === "hermes.exe"
|
|
183
|
+
|| base === "hermes.cmd"
|
|
184
|
+
|| normalized.endsWith("/hermes_cli/main.py")
|
|
185
|
+
|| normalized.endsWith("/gateway/run.py")
|
|
186
|
+
|| normalized.endsWith("/run_agent.py")
|
|
187
|
+
|| (normalized.endsWith("/cli.py") && normalized.includes("hermes"));
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
function looksLikeHermesGatewayDaemon(argv) {
|
|
191
|
+
const normalized = argv.map((arg) => arg.replace(/\\/g, "/").toLowerCase());
|
|
192
|
+
const usesHermesEntrypoint = normalized.some((arg) => {
|
|
193
|
+
const base = (0, node_path_1.basename)(arg);
|
|
194
|
+
return base === "hermes"
|
|
195
|
+
|| base === "hermes.exe"
|
|
196
|
+
|| base === "hermes.cmd"
|
|
197
|
+
|| arg.endsWith("/hermes_cli/main.py")
|
|
198
|
+
|| arg === "hermes_cli.main";
|
|
199
|
+
});
|
|
200
|
+
return usesHermesEntrypoint
|
|
201
|
+
&& normalized.includes("gateway")
|
|
202
|
+
&& normalized.includes("run");
|
|
203
|
+
}
|
|
204
|
+
function looksLikeHermesRuntimeHomeProcess(argv, cwd) {
|
|
205
|
+
const normalizedCwd = (cwd ?? "").replace(/\\/g, "/").toLowerCase();
|
|
206
|
+
if (!normalizedCwd.endsWith("/hermes-agent")) {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
const normalized = argv.map((arg) => arg.replace(/\\/g, "/").toLowerCase());
|
|
210
|
+
const launcherIndex = normalized.findIndex((arg) => {
|
|
211
|
+
const base = (0, node_path_1.basename)(arg);
|
|
212
|
+
return base === "hermes"
|
|
213
|
+
|| base === "hermes.exe"
|
|
214
|
+
|| base === "hermes.cmd";
|
|
215
|
+
});
|
|
216
|
+
return launcherIndex >= 0 && launcherIndex === normalized.length - 1;
|
|
217
|
+
}
|
|
218
|
+
async function scanHermesProcesses() {
|
|
219
|
+
if (process.platform !== "linux") {
|
|
220
|
+
return [];
|
|
221
|
+
}
|
|
222
|
+
const procEntries = await (0, promises_1.readdir)("/proc", { withFileTypes: true }).catch(() => []);
|
|
223
|
+
const processes = [];
|
|
224
|
+
for (const entry of procEntries) {
|
|
225
|
+
if (!entry.isDirectory() || !/^\d+$/.test(entry.name)) {
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
const pid = Number.parseInt(entry.name, 10);
|
|
229
|
+
if (!Number.isFinite(pid) || pid === process.pid) {
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
const procRoot = (0, node_path_1.join)("/proc", entry.name);
|
|
233
|
+
const argv = splitNulFile(await readProcFile((0, node_path_1.join)(procRoot, "cmdline")));
|
|
234
|
+
const env = parseProcEnviron(await readProcFile((0, node_path_1.join)(procRoot, "environ")));
|
|
235
|
+
if (looksLikeHermesGatewayDaemon(argv)) {
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
if (!looksLikeHermesProcess(argv)) {
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
const cwd = await (0, promises_1.readlink)((0, node_path_1.join)(procRoot, "cwd")).catch(() => null);
|
|
242
|
+
const normalizedCwd = normalizeFilesystemPath(env.TERMINAL_CWD)
|
|
243
|
+
?? normalizeFilesystemPath(env.HERMES_CWD)
|
|
244
|
+
?? normalizeFilesystemPath(cwd);
|
|
245
|
+
if (looksLikeHermesRuntimeHomeProcess(argv, normalizedCwd)) {
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
const normalizedHome = normalizeFilesystemPath(env.HERMES_HOME);
|
|
249
|
+
const procStats = await (0, promises_1.stat)(procRoot).catch(() => null);
|
|
250
|
+
processes.push({
|
|
251
|
+
pid,
|
|
252
|
+
cwd: normalizedCwd,
|
|
253
|
+
hermesHome: normalizedHome,
|
|
254
|
+
command: argv.join(" "),
|
|
255
|
+
updatedAtMs: procStats?.mtimeMs ?? Date.now()
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
return processes;
|
|
259
|
+
}
|
|
260
|
+
function pythonCandidates() {
|
|
261
|
+
const candidates = [
|
|
262
|
+
process.env.HERMES_SQLITE_PYTHON,
|
|
263
|
+
"python3",
|
|
264
|
+
"python"
|
|
265
|
+
].filter((entry) => typeof entry === "string" && entry.trim().length > 0);
|
|
266
|
+
return Array.from(new Set(candidates));
|
|
267
|
+
}
|
|
268
|
+
const HERMES_SQLITE_EXPORT_SCRIPT = `
|
|
269
|
+
import json
|
|
270
|
+
import sqlite3
|
|
271
|
+
import sys
|
|
272
|
+
import urllib.parse
|
|
273
|
+
|
|
274
|
+
db_path = sys.argv[1]
|
|
275
|
+
limit = int(sys.argv[2])
|
|
276
|
+
message_limit = int(sys.argv[3])
|
|
277
|
+
text_limit = int(sys.argv[4])
|
|
278
|
+
uri = "file:" + urllib.parse.quote(db_path) + "?mode=ro"
|
|
279
|
+
conn = sqlite3.connect(uri, uri=True, timeout=1.0)
|
|
280
|
+
conn.row_factory = sqlite3.Row
|
|
281
|
+
conn.execute("PRAGMA busy_timeout=1000")
|
|
282
|
+
sessions = conn.execute("""
|
|
283
|
+
SELECT
|
|
284
|
+
s.id,
|
|
285
|
+
substr(s.source, 1, ?) AS source,
|
|
286
|
+
substr(s.model, 1, ?) AS model,
|
|
287
|
+
s.parent_session_id,
|
|
288
|
+
p.ended_at AS parent_ended_at,
|
|
289
|
+
substr(p.end_reason, 1, ?) AS parent_end_reason,
|
|
290
|
+
s.started_at,
|
|
291
|
+
s.ended_at,
|
|
292
|
+
substr(s.end_reason, 1, ?) AS end_reason,
|
|
293
|
+
s.message_count,
|
|
294
|
+
s.tool_call_count,
|
|
295
|
+
substr(s.title, 1, ?) AS title,
|
|
296
|
+
substr(s.system_prompt, 1, ?) AS system_prompt,
|
|
297
|
+
COALESCE((SELECT MAX(m.timestamp) FROM messages m WHERE m.session_id = s.id), s.started_at) AS last_active
|
|
298
|
+
FROM sessions s
|
|
299
|
+
LEFT JOIN sessions p ON p.id = s.parent_session_id
|
|
300
|
+
ORDER BY last_active DESC
|
|
301
|
+
LIMIT ?
|
|
302
|
+
""", (text_limit, text_limit, text_limit, text_limit, text_limit, text_limit, limit)).fetchall()
|
|
303
|
+
|
|
304
|
+
result = []
|
|
305
|
+
for session in sessions:
|
|
306
|
+
messages = conn.execute("""
|
|
307
|
+
SELECT
|
|
308
|
+
id,
|
|
309
|
+
role,
|
|
310
|
+
substr(content, 1, ?) AS content,
|
|
311
|
+
substr(tool_calls, 1, ?) AS tool_calls,
|
|
312
|
+
tool_name,
|
|
313
|
+
tool_call_id,
|
|
314
|
+
timestamp,
|
|
315
|
+
finish_reason,
|
|
316
|
+
NULL AS reasoning,
|
|
317
|
+
NULL AS reasoning_content
|
|
318
|
+
FROM messages
|
|
319
|
+
WHERE session_id = ?
|
|
320
|
+
ORDER BY timestamp DESC, id DESC
|
|
321
|
+
LIMIT ?
|
|
322
|
+
""", (text_limit, text_limit, session["id"], message_limit)).fetchall()
|
|
323
|
+
result.append({
|
|
324
|
+
"id": session["id"],
|
|
325
|
+
"source": session["source"],
|
|
326
|
+
"model": session["model"],
|
|
327
|
+
"parentSessionId": session["parent_session_id"],
|
|
328
|
+
"parentEndedAt": session["parent_ended_at"],
|
|
329
|
+
"parentEndReason": session["parent_end_reason"],
|
|
330
|
+
"startedAt": session["started_at"],
|
|
331
|
+
"endedAt": session["ended_at"],
|
|
332
|
+
"endReason": session["end_reason"],
|
|
333
|
+
"messageCount": session["message_count"],
|
|
334
|
+
"toolCallCount": session["tool_call_count"],
|
|
335
|
+
"title": session["title"],
|
|
336
|
+
"systemPrompt": session["system_prompt"],
|
|
337
|
+
"lastActive": session["last_active"],
|
|
338
|
+
"messages": [dict(row) for row in reversed(messages)],
|
|
339
|
+
})
|
|
340
|
+
print(json.dumps({"sessions": result}, ensure_ascii=False))
|
|
341
|
+
`;
|
|
342
|
+
async function exportHermesSqlite(dbPath, limit) {
|
|
343
|
+
let lastError = null;
|
|
344
|
+
for (const python of pythonCandidates()) {
|
|
345
|
+
try {
|
|
346
|
+
const { stdout } = await execFileAsync(python, ["-c", HERMES_SQLITE_EXPORT_SCRIPT, dbPath, String(limit), String(HERMES_SQLITE_MESSAGE_LIMIT), String(HERMES_SQLITE_TEXT_LIMIT)], {
|
|
347
|
+
timeout: HERMES_SQLITE_TIMEOUT_MS,
|
|
348
|
+
maxBuffer: HERMES_SQLITE_MAX_BUFFER
|
|
349
|
+
});
|
|
350
|
+
return JSON.parse(stdout);
|
|
351
|
+
}
|
|
352
|
+
catch (error) {
|
|
353
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
354
|
+
const code = typeof error === "object" && error !== null && "code" in error
|
|
355
|
+
? String(error.code)
|
|
356
|
+
: "";
|
|
357
|
+
if (code !== "ENOENT") {
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
throw lastError ?? new Error("No Python executable available for Hermes SQLite reads");
|
|
363
|
+
}
|
|
364
|
+
function normalizeSqliteMessage(raw) {
|
|
365
|
+
return {
|
|
366
|
+
id: typeof raw.id === "number" || typeof raw.id === "string" ? raw.id : "",
|
|
367
|
+
role: typeof raw.role === "string" ? raw.role : null,
|
|
368
|
+
content: raw.content,
|
|
369
|
+
toolCalls: raw.tool_calls,
|
|
370
|
+
toolName: typeof raw.tool_name === "string" ? raw.tool_name : null,
|
|
371
|
+
toolCallId: typeof raw.tool_call_id === "string" ? raw.tool_call_id : null,
|
|
372
|
+
timestamp: typeof raw.timestamp === "number" ? raw.timestamp : 0,
|
|
373
|
+
finishReason: typeof raw.finish_reason === "string" ? raw.finish_reason : null,
|
|
374
|
+
reasoning: typeof raw.reasoning === "string" ? raw.reasoning : null,
|
|
375
|
+
reasoningContent: typeof raw.reasoning_content === "string" ? raw.reasoning_content : null
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
async function loadHermesSqliteSessions(home, limit) {
|
|
379
|
+
const explicitDb = normalizeFilesystemPath(process.env.HERMES_STATE_DB);
|
|
380
|
+
const dbPath = explicitDb ?? (0, node_path_1.join)(home, "state.db");
|
|
381
|
+
if (!await pathExists(dbPath)) {
|
|
382
|
+
return [];
|
|
383
|
+
}
|
|
384
|
+
const exported = await exportHermesSqlite(hostFilesystemPath(dbPath), limit);
|
|
385
|
+
return exported.sessions.map((session) => ({
|
|
386
|
+
...session,
|
|
387
|
+
startedAt: Number(session.startedAt) || 0,
|
|
388
|
+
endedAt: typeof session.endedAt === "number" && Number.isFinite(session.endedAt) && session.endedAt > 0
|
|
389
|
+
? session.endedAt
|
|
390
|
+
: null,
|
|
391
|
+
parentEndedAt: typeof session.parentEndedAt === "number" && Number.isFinite(session.parentEndedAt) && session.parentEndedAt > 0
|
|
392
|
+
? session.parentEndedAt
|
|
393
|
+
: null,
|
|
394
|
+
parentEndReason: typeof session.parentEndReason === "string" ? session.parentEndReason : null,
|
|
395
|
+
messageCount: Number(session.messageCount) || 0,
|
|
396
|
+
toolCallCount: Number(session.toolCallCount) || 0,
|
|
397
|
+
systemPrompt: typeof session.systemPrompt === "string" ? session.systemPrompt : null,
|
|
398
|
+
lastActive: Number(session.lastActive) || Number(session.startedAt) || 0,
|
|
399
|
+
home,
|
|
400
|
+
storage: "sqlite",
|
|
401
|
+
messages: session.messages.map((message) => normalizeSqliteMessage(message))
|
|
402
|
+
}));
|
|
403
|
+
}
|
|
404
|
+
async function loadHermesSessions(home, limit, notes) {
|
|
405
|
+
try {
|
|
406
|
+
const sqliteSessions = await loadHermesSqliteSessions(home, limit);
|
|
407
|
+
return sqliteSessions;
|
|
408
|
+
}
|
|
409
|
+
catch (error) {
|
|
410
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
411
|
+
notes.push(`Hermes SQLite sessions unavailable for ${home}: ${message}`);
|
|
412
|
+
return [];
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
function parseMaybeJson(value) {
|
|
416
|
+
if (typeof value !== "string") {
|
|
417
|
+
return value;
|
|
418
|
+
}
|
|
419
|
+
const trimmed = value.trim();
|
|
420
|
+
if (!trimmed || !/^[{[]/.test(trimmed)) {
|
|
421
|
+
return value;
|
|
422
|
+
}
|
|
423
|
+
try {
|
|
424
|
+
return JSON.parse(trimmed);
|
|
425
|
+
}
|
|
426
|
+
catch {
|
|
427
|
+
return value;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
function textFromContent(value) {
|
|
431
|
+
const parsed = parseMaybeJson(value);
|
|
432
|
+
if (typeof parsed === "string") {
|
|
433
|
+
return parsed.replace(/\s+/g, " ").trim();
|
|
434
|
+
}
|
|
435
|
+
if (Array.isArray(parsed)) {
|
|
436
|
+
return parsed
|
|
437
|
+
.map((entry) => {
|
|
438
|
+
if (typeof entry === "string") {
|
|
439
|
+
return entry;
|
|
440
|
+
}
|
|
441
|
+
if (entry && typeof entry === "object" && "text" in entry) {
|
|
442
|
+
return String(entry.text ?? "");
|
|
443
|
+
}
|
|
444
|
+
return "";
|
|
445
|
+
})
|
|
446
|
+
.filter(Boolean)
|
|
447
|
+
.join(" ")
|
|
448
|
+
.replace(/\s+/g, " ")
|
|
449
|
+
.trim();
|
|
450
|
+
}
|
|
451
|
+
if (parsed && typeof parsed === "object") {
|
|
452
|
+
const object = parsed;
|
|
453
|
+
for (const key of ["output", "error", "content", "message", "text"]) {
|
|
454
|
+
const candidate = object[key];
|
|
455
|
+
if (typeof candidate === "string" && candidate.trim()) {
|
|
456
|
+
return candidate.replace(/\s+/g, " ").trim();
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
return "";
|
|
461
|
+
}
|
|
462
|
+
function shorten(text, maxLength) {
|
|
463
|
+
const normalized = text.replace(/\s+/g, " ").trim();
|
|
464
|
+
if (normalized.length <= maxLength) {
|
|
465
|
+
return normalized;
|
|
466
|
+
}
|
|
467
|
+
return `${normalized.slice(0, Math.max(0, maxLength - 3))}...`;
|
|
468
|
+
}
|
|
469
|
+
function parseToolCalls(value) {
|
|
470
|
+
const parsed = parseMaybeJson(value);
|
|
471
|
+
const list = Array.isArray(parsed) ? parsed : parsed ? [parsed] : [];
|
|
472
|
+
const calls = [];
|
|
473
|
+
for (const entry of list) {
|
|
474
|
+
if (!entry || typeof entry !== "object") {
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
const record = entry;
|
|
478
|
+
const functionRecord = record.function && typeof record.function === "object"
|
|
479
|
+
? record.function
|
|
480
|
+
: null;
|
|
481
|
+
const name = typeof record.name === "string"
|
|
482
|
+
? record.name
|
|
483
|
+
: typeof functionRecord?.name === "string"
|
|
484
|
+
? functionRecord.name
|
|
485
|
+
: typeof record.tool_name === "string"
|
|
486
|
+
? record.tool_name
|
|
487
|
+
: "";
|
|
488
|
+
const rawArgs = record.arguments ?? functionRecord?.arguments ?? record.args ?? {};
|
|
489
|
+
const args = parseMaybeJson(rawArgs);
|
|
490
|
+
calls.push({
|
|
491
|
+
name,
|
|
492
|
+
args: args && typeof args === "object" && !Array.isArray(args)
|
|
493
|
+
? args
|
|
494
|
+
: {}
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
return calls.filter((call) => call.name.length > 0);
|
|
498
|
+
}
|
|
499
|
+
function latestAssistantText(messages) {
|
|
500
|
+
for (const message of [...messages].reverse()) {
|
|
501
|
+
if (message.role !== "assistant") {
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
const text = textFromContent(message.content);
|
|
505
|
+
if (text) {
|
|
506
|
+
return shorten(text, 240);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
return null;
|
|
510
|
+
}
|
|
511
|
+
function latestUserContent(messages) {
|
|
512
|
+
for (const message of [...messages].reverse()) {
|
|
513
|
+
if (message.role !== "user") {
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
const text = textFromContent(message.content);
|
|
517
|
+
if (text) {
|
|
518
|
+
return text;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
return null;
|
|
522
|
+
}
|
|
523
|
+
function hermesProjectDisplayLabel(projectRoot) {
|
|
524
|
+
const label = (0, project_paths_1.projectLabelFromRoot)(projectRoot) || "Hermes";
|
|
525
|
+
return label === "Ika Bot" ? "IkaBot" : label;
|
|
526
|
+
}
|
|
527
|
+
function titleCaseHermesWord(word) {
|
|
528
|
+
const lower = word.toLowerCase();
|
|
529
|
+
if (lower === "ikabot") {
|
|
530
|
+
return "IkaBot";
|
|
531
|
+
}
|
|
532
|
+
if (["api", "cli", "ui", "sdk", "mcp"].includes(lower)) {
|
|
533
|
+
return lower.toUpperCase();
|
|
534
|
+
}
|
|
535
|
+
return lower.charAt(0).toUpperCase() + lower.slice(1);
|
|
536
|
+
}
|
|
537
|
+
function humanizeHermesSkillName(skillName) {
|
|
538
|
+
const normalized = skillName
|
|
539
|
+
.replace(/[_-]+/g, " ")
|
|
540
|
+
.replace(/\s+/g, " ")
|
|
541
|
+
.trim();
|
|
542
|
+
if (!normalized) {
|
|
543
|
+
return "";
|
|
544
|
+
}
|
|
545
|
+
return normalized.split(" ").map(titleCaseHermesWord).join(" ");
|
|
546
|
+
}
|
|
547
|
+
function hermesSkillNameFromText(text) {
|
|
548
|
+
const invoked = text.match(/invoked the "([^"]+)" skill/i)?.[1];
|
|
549
|
+
if (invoked && invoked.trim()) {
|
|
550
|
+
return invoked.trim();
|
|
551
|
+
}
|
|
552
|
+
const frontmatter = text.match(/^name:\s*([A-Za-z0-9_-]+)\s*$/m)?.[1];
|
|
553
|
+
return frontmatter && frontmatter.trim() ? frontmatter.trim() : null;
|
|
554
|
+
}
|
|
555
|
+
function hermesTitleFromText(text, maxLength = 54) {
|
|
556
|
+
let normalized = text
|
|
557
|
+
.replace(/`([^`]+)`/g, "$1")
|
|
558
|
+
.replace(/\*\*([^*]+)\*\*/g, "$1")
|
|
559
|
+
.replace(/\s+/g, " ")
|
|
560
|
+
.trim();
|
|
561
|
+
if (!normalized) {
|
|
562
|
+
return null;
|
|
563
|
+
}
|
|
564
|
+
const skillName = hermesSkillNameFromText(normalized);
|
|
565
|
+
if (skillName) {
|
|
566
|
+
return shorten(humanizeHermesSkillName(skillName), maxLength);
|
|
567
|
+
}
|
|
568
|
+
normalized = normalized
|
|
569
|
+
.replace(/^got it\s*[-:\u2014]\s*/i, "")
|
|
570
|
+
.replace(/^done\s*[-:\u2014]\s*/i, "")
|
|
571
|
+
.replace(/^updated\s*[-:\u2014]\s*/i, "Updated: ")
|
|
572
|
+
.replace(/^\s*[-*]\s+/, "");
|
|
573
|
+
const firstBoundary = [
|
|
574
|
+
normalized.indexOf(" - "),
|
|
575
|
+
normalized.indexOf(" \u2014 "),
|
|
576
|
+
normalized.indexOf(". "),
|
|
577
|
+
normalized.indexOf("; ")
|
|
578
|
+
].filter((index) => index > 12);
|
|
579
|
+
const boundary = firstBoundary.length > 0 ? Math.min(...firstBoundary) : -1;
|
|
580
|
+
let title = boundary > 0 ? normalized.slice(0, boundary) : normalized;
|
|
581
|
+
title = title.replace(/^[a-z]/, (letter) => letter.toUpperCase());
|
|
582
|
+
return shorten(title.replace(/\s+/g, " ").trim(), maxLength) || null;
|
|
583
|
+
}
|
|
584
|
+
function hermesTitleLooksGenericPrompt(title) {
|
|
585
|
+
return /^review the conversation above\b/i.test(title)
|
|
586
|
+
|| /^continue\b/i.test(title)
|
|
587
|
+
|| /^please continue\b/i.test(title);
|
|
588
|
+
}
|
|
589
|
+
function isHermesCronSessionId(sessionId) {
|
|
590
|
+
return HERMES_CRON_SESSION_ID_RE.test(sessionId);
|
|
591
|
+
}
|
|
592
|
+
function isHermesCronSession(session) {
|
|
593
|
+
const source = (session.source ?? "").trim().toLowerCase();
|
|
594
|
+
return isHermesCronSessionId(session.id) || source === "cron";
|
|
595
|
+
}
|
|
596
|
+
function hermesCronPromptText(text) {
|
|
597
|
+
let normalized = text
|
|
598
|
+
.replace(/^\[IMPORTANT:\s*You are running as a scheduled cron job\.[\s\S]*?\]\s*/i, "")
|
|
599
|
+
.replace(/^Cronjob Response:[\s\S]*?-------------\s*/i, "")
|
|
600
|
+
.trim();
|
|
601
|
+
if (!normalized || normalized === "[SILENT]") {
|
|
602
|
+
return null;
|
|
603
|
+
}
|
|
604
|
+
normalized = normalized.replace(/^#\s*Cron Job:\s*/i, "").trim();
|
|
605
|
+
return normalized || null;
|
|
606
|
+
}
|
|
607
|
+
function latestHermesSessionUserText(session, maxLength = 160) {
|
|
608
|
+
const text = latestUserContent(session.messages);
|
|
609
|
+
if (!text) {
|
|
610
|
+
return null;
|
|
611
|
+
}
|
|
612
|
+
const displayText = isHermesCronSession(session) ? hermesCronPromptText(text) : text;
|
|
613
|
+
return displayText ? shorten(displayText, maxLength) : null;
|
|
614
|
+
}
|
|
615
|
+
function hermesFallbackSessionLabel(projectRoot, sessionId) {
|
|
616
|
+
const projectLabel = hermesProjectDisplayLabel(projectRoot);
|
|
617
|
+
if (isHermesCronSessionId(sessionId)) {
|
|
618
|
+
return `${projectLabel} tick`;
|
|
619
|
+
}
|
|
620
|
+
return `${projectLabel} Hermes`;
|
|
621
|
+
}
|
|
622
|
+
function hermesHookSessionLabel(records, projectRoot, latestMessage) {
|
|
623
|
+
const latest = records[records.length - 1];
|
|
624
|
+
const sessionId = latest?.sessionId ?? "";
|
|
625
|
+
if (isHermesCronSessionId(sessionId)) {
|
|
626
|
+
return hermesFallbackSessionLabel(projectRoot, sessionId);
|
|
627
|
+
}
|
|
628
|
+
const messageTitle = latestMessage ? hermesTitleFromText(latestMessage) : null;
|
|
629
|
+
for (let index = records.length - 1; index >= 0; index -= 1) {
|
|
630
|
+
const record = records[index];
|
|
631
|
+
const userText = hermesHookText(record, ["user_message"]);
|
|
632
|
+
const userTitle = userText ? hermesTitleFromText(userText) : null;
|
|
633
|
+
if (userTitle) {
|
|
634
|
+
if (messageTitle && hermesTitleLooksGenericPrompt(userTitle)) {
|
|
635
|
+
return messageTitle;
|
|
636
|
+
}
|
|
637
|
+
return userTitle;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
if (messageTitle) {
|
|
641
|
+
return messageTitle;
|
|
642
|
+
}
|
|
643
|
+
return hermesFallbackSessionLabel(projectRoot, sessionId);
|
|
644
|
+
}
|
|
645
|
+
function toolKind(toolName) {
|
|
646
|
+
const normalized = toolName.toLowerCase();
|
|
647
|
+
if (normalized === "todo") {
|
|
648
|
+
return { state: "planning", eventType: "plan", eventKind: "turn" };
|
|
649
|
+
}
|
|
650
|
+
if (normalized === "terminal"
|
|
651
|
+
|| normalized === "process"
|
|
652
|
+
|| normalized === "execute_code"
|
|
653
|
+
|| normalized.includes("shell")
|
|
654
|
+
|| normalized.includes("bash")) {
|
|
655
|
+
return { state: "running", eventType: "commandExecution", eventKind: "command" };
|
|
656
|
+
}
|
|
657
|
+
if (normalized.startsWith("mcp_")) {
|
|
658
|
+
return { state: "running", eventType: "mcpToolCall", eventKind: "tool" };
|
|
659
|
+
}
|
|
660
|
+
if (normalized === "write_file" || normalized === "patch") {
|
|
661
|
+
return { state: "editing", eventType: "fileChange", eventKind: "fileChange" };
|
|
662
|
+
}
|
|
663
|
+
if (normalized.includes("delegate") || normalized.includes("subagent")) {
|
|
664
|
+
return { state: "delegating", eventType: "collabAgentToolCall", eventKind: "subagent" };
|
|
665
|
+
}
|
|
666
|
+
if (normalized === "web_search" || normalized.includes("browser_search")) {
|
|
667
|
+
return { state: "scanning", eventType: "webSearch", eventKind: "tool" };
|
|
668
|
+
}
|
|
669
|
+
if (normalized === "read_file"
|
|
670
|
+
|| normalized === "search_files"
|
|
671
|
+
|| normalized === "skills_list"
|
|
672
|
+
|| normalized === "skill_view"
|
|
673
|
+
|| normalized === "web_extract"
|
|
674
|
+
|| normalized.includes("search")
|
|
675
|
+
|| normalized.includes("read")
|
|
676
|
+
|| normalized.includes("list")) {
|
|
677
|
+
return { state: "scanning", eventType: "dynamicToolCall", eventKind: "tool" };
|
|
678
|
+
}
|
|
679
|
+
return { state: "running", eventType: "dynamicToolCall", eventKind: "tool" };
|
|
680
|
+
}
|
|
681
|
+
function toolTitle(toolName, args) {
|
|
682
|
+
if (toolName === "terminal" && typeof args.command === "string") {
|
|
683
|
+
return shorten(args.command, 120);
|
|
684
|
+
}
|
|
685
|
+
if (toolName === "process") {
|
|
686
|
+
const action = typeof args.action === "string" && args.action.trim() ? args.action.trim() : "manage";
|
|
687
|
+
const sessionId = typeof args.session_id === "string" && args.session_id.trim()
|
|
688
|
+
? ` ${shorten(args.session_id.trim(), 40)}`
|
|
689
|
+
: "";
|
|
690
|
+
return `process ${action}${sessionId}`;
|
|
691
|
+
}
|
|
692
|
+
if (toolName === "todo") {
|
|
693
|
+
const todos = Array.isArray(args.todos) ? args.todos : null;
|
|
694
|
+
if (!todos) {
|
|
695
|
+
return "todo: reading task list";
|
|
696
|
+
}
|
|
697
|
+
return `todo: ${args.merge === true ? "updating" : "planning"} ${todos.length} task(s)`;
|
|
698
|
+
}
|
|
699
|
+
for (const key of ["path", "file_path", "filepath", "workdir", "query", "pattern", "name", "category"]) {
|
|
700
|
+
if (typeof args[key] === "string" && String(args[key]).trim()) {
|
|
701
|
+
return `${toolName}: ${shorten(String(args[key]), 90)}`;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
return toolName;
|
|
705
|
+
}
|
|
706
|
+
function toolPath(projectRoot, cwd, args) {
|
|
707
|
+
for (const key of ["path", "file_path", "filepath", "workdir", "cwd", "dir", "directory"]) {
|
|
708
|
+
const value = args[key];
|
|
709
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
710
|
+
continue;
|
|
711
|
+
}
|
|
712
|
+
const normalized = normalizeCandidatePath(value, cwd ?? projectRoot);
|
|
713
|
+
if (normalized) {
|
|
714
|
+
return normalized;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
return null;
|
|
718
|
+
}
|
|
719
|
+
function hermesHookChangedPaths(record, projectRoot) {
|
|
720
|
+
const value = record.payload.changed_paths;
|
|
721
|
+
if (!Array.isArray(value)) {
|
|
722
|
+
return [];
|
|
723
|
+
}
|
|
724
|
+
const paths = [];
|
|
725
|
+
for (const entry of value) {
|
|
726
|
+
if (typeof entry !== "string" || entry.trim().length === 0) {
|
|
727
|
+
continue;
|
|
728
|
+
}
|
|
729
|
+
const normalized = normalizeCandidatePath(entry, record.cwd ?? record.processCwd ?? projectRoot);
|
|
730
|
+
if (normalized && !paths.includes(normalized)) {
|
|
731
|
+
paths.push(normalized);
|
|
732
|
+
}
|
|
733
|
+
if (paths.length >= HERMES_HOOK_PATH_LIMIT) {
|
|
734
|
+
break;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
return paths;
|
|
738
|
+
}
|
|
739
|
+
function hermesHookPreVerifyDetail(record, projectRoot) {
|
|
740
|
+
const paths = hermesHookChangedPaths(record, projectRoot);
|
|
741
|
+
const changedCount = paths.length;
|
|
742
|
+
const pathDetail = changedCount === 1
|
|
743
|
+
? `Verifying ${(0, node_path_1.basename)(paths[0])}`
|
|
744
|
+
: changedCount > 1
|
|
745
|
+
? `Verifying ${changedCount} changed files`
|
|
746
|
+
: "Hermes verification gate";
|
|
747
|
+
const attempt = hermesHookPayloadNumber(record, "attempt");
|
|
748
|
+
return attempt && attempt > 0 ? `${pathDetail} (attempt ${attempt + 1})` : pathDetail;
|
|
749
|
+
}
|
|
750
|
+
function toolActivityEvent(projectRoot, cwd, call) {
|
|
751
|
+
const kind = toolKind(call.name);
|
|
752
|
+
const path = toolPath(projectRoot, cwd, call.args);
|
|
753
|
+
return {
|
|
754
|
+
type: kind.eventType,
|
|
755
|
+
action: kind.eventKind === "command" ? "ran"
|
|
756
|
+
: kind.eventKind === "fileChange" ? "edited"
|
|
757
|
+
: "updated",
|
|
758
|
+
path,
|
|
759
|
+
title: toolTitle(call.name, call.args),
|
|
760
|
+
isImage: false
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
function messageLooksFailed(message) {
|
|
764
|
+
const parsed = parseMaybeJson(message.content);
|
|
765
|
+
if (parsed && typeof parsed === "object") {
|
|
766
|
+
const record = parsed;
|
|
767
|
+
if (typeof record.exit_code === "number" && record.exit_code !== 0) {
|
|
768
|
+
return true;
|
|
769
|
+
}
|
|
770
|
+
if (typeof record.returncode === "number" && record.returncode !== 0) {
|
|
771
|
+
return true;
|
|
772
|
+
}
|
|
773
|
+
if (typeof record.error === "string" && record.error.trim().length > 0) {
|
|
774
|
+
return true;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
return false;
|
|
778
|
+
}
|
|
779
|
+
function latestMessageToolCall(message) {
|
|
780
|
+
if (!message) {
|
|
781
|
+
return null;
|
|
782
|
+
}
|
|
783
|
+
const calls = parseToolCalls(message.toolCalls);
|
|
784
|
+
if (calls.length > 0) {
|
|
785
|
+
return calls[calls.length - 1];
|
|
786
|
+
}
|
|
787
|
+
return null;
|
|
788
|
+
}
|
|
789
|
+
function normalizeCandidatePath(value, cwd) {
|
|
790
|
+
const cleaned = value
|
|
791
|
+
.trim()
|
|
792
|
+
.replace(/^["'`]+|["'`),.;:]+$/g, "");
|
|
793
|
+
if (!cleaned || cleaned.length > 400) {
|
|
794
|
+
return null;
|
|
795
|
+
}
|
|
796
|
+
const absolute = /^([a-zA-Z]:[\\/]|\/)/.test(cleaned)
|
|
797
|
+
? cleaned
|
|
798
|
+
: cleaned.startsWith("./") || cleaned.startsWith("../")
|
|
799
|
+
? (0, node_path_1.join)(cwd, cleaned)
|
|
800
|
+
: null;
|
|
801
|
+
return normalizeFilesystemPath(absolute);
|
|
802
|
+
}
|
|
803
|
+
function extractAbsolutePathCandidates(text) {
|
|
804
|
+
const matches = text.match(/(?:[a-zA-Z]:[\\/][^\s"'`<>]+|\/(?:mnt\/[a-zA-Z]|home|Users|workspace|tmp|var|opt|srv)\/[^\s"'`<>]+)/g);
|
|
805
|
+
return matches ?? [];
|
|
806
|
+
}
|
|
807
|
+
function cwdFromSystemPrompt(systemPrompt) {
|
|
808
|
+
if (!systemPrompt) {
|
|
809
|
+
return null;
|
|
810
|
+
}
|
|
811
|
+
for (const pattern of [
|
|
812
|
+
/^Current working directory:\s*(.+)$/im,
|
|
813
|
+
/^Working directory:\s*(.+)$/im,
|
|
814
|
+
/^cwd\s*[:=]\s*(.+)$/im
|
|
815
|
+
]) {
|
|
816
|
+
const match = systemPrompt.match(pattern);
|
|
817
|
+
if (!match) {
|
|
818
|
+
continue;
|
|
819
|
+
}
|
|
820
|
+
const value = match[1]
|
|
821
|
+
.trim()
|
|
822
|
+
.split(/\s+/)[0]
|
|
823
|
+
?.replace(/^["'`]+|["'`),.;:]+$/g, "");
|
|
824
|
+
const normalized = normalizeFilesystemPath(value);
|
|
825
|
+
if (normalized) {
|
|
826
|
+
return normalized;
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
return null;
|
|
830
|
+
}
|
|
831
|
+
function collectSessionPathCandidates(session, fallbackCwd) {
|
|
832
|
+
const paths = new Set();
|
|
833
|
+
const promptCwd = cwdFromSystemPrompt(session.systemPrompt);
|
|
834
|
+
if (promptCwd) {
|
|
835
|
+
paths.add(promptCwd);
|
|
836
|
+
}
|
|
837
|
+
for (const message of session.messages) {
|
|
838
|
+
for (const candidate of collectMessagePathCandidates(message, fallbackCwd)) {
|
|
839
|
+
paths.add(candidate);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
if (fallbackCwd) {
|
|
843
|
+
paths.add(fallbackCwd);
|
|
844
|
+
}
|
|
845
|
+
return [...paths];
|
|
846
|
+
}
|
|
847
|
+
function collectMessagePathCandidates(message, fallbackCwd) {
|
|
848
|
+
const paths = new Set();
|
|
849
|
+
for (const call of parseToolCalls(message.toolCalls)) {
|
|
850
|
+
const fromTool = toolPath("/", fallbackCwd, call.args);
|
|
851
|
+
if (fromTool) {
|
|
852
|
+
paths.add(fromTool);
|
|
853
|
+
}
|
|
854
|
+
for (const candidate of extractAbsolutePathCandidates(JSON.stringify(call.args))) {
|
|
855
|
+
const normalized = normalizeCandidatePath(candidate, fallbackCwd ?? "/");
|
|
856
|
+
if (normalized) {
|
|
857
|
+
paths.add(normalized);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
const text = textFromContent(message.content);
|
|
862
|
+
for (const candidate of extractAbsolutePathCandidates(text)) {
|
|
863
|
+
const normalized = normalizeCandidatePath(candidate, fallbackCwd ?? "/");
|
|
864
|
+
if (normalized) {
|
|
865
|
+
paths.add(normalized);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
return [...paths];
|
|
869
|
+
}
|
|
870
|
+
function sortedLatestMessages(messages) {
|
|
871
|
+
return messages
|
|
872
|
+
.map((message, index) => ({ message, index }))
|
|
873
|
+
.sort((left, right) => {
|
|
874
|
+
const timeDelta = right.message.timestamp - left.message.timestamp;
|
|
875
|
+
if (timeDelta !== 0) {
|
|
876
|
+
return timeDelta;
|
|
877
|
+
}
|
|
878
|
+
return right.index - left.index;
|
|
879
|
+
})
|
|
880
|
+
.map((entry) => entry.message);
|
|
881
|
+
}
|
|
882
|
+
async function currentProjectRootForHermesSession(session, activeProcess) {
|
|
883
|
+
if (activeProcess?.cwd) {
|
|
884
|
+
const activeRoot = await resolveProjectRootForPath(activeProcess.cwd);
|
|
885
|
+
if (activeRoot) {
|
|
886
|
+
return activeRoot;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
const sessionCwd = cwdFromSystemPrompt(session.systemPrompt);
|
|
890
|
+
for (const message of sortedLatestMessages(session.messages)) {
|
|
891
|
+
for (const candidate of collectMessagePathCandidates(message, sessionCwd)) {
|
|
892
|
+
const root = await resolveProjectRootForPath(candidate);
|
|
893
|
+
if (root) {
|
|
894
|
+
return root;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
if (sessionCwd) {
|
|
899
|
+
return resolveProjectRootForPath(sessionCwd);
|
|
900
|
+
}
|
|
901
|
+
return null;
|
|
902
|
+
}
|
|
903
|
+
function pathWithinProject(projectRoot, candidate) {
|
|
904
|
+
if (!candidate) {
|
|
905
|
+
return false;
|
|
906
|
+
}
|
|
907
|
+
if ((0, project_paths_1.sameProjectPath)(projectRoot, candidate)) {
|
|
908
|
+
return true;
|
|
909
|
+
}
|
|
910
|
+
const rootKey = (0, project_paths_1.projectPathIdentityKey)(projectRoot);
|
|
911
|
+
const candidateKey = (0, project_paths_1.projectPathIdentityKey)(candidate);
|
|
912
|
+
return Boolean(rootKey && candidateKey && candidateKey.startsWith(`${rootKey}/`));
|
|
913
|
+
}
|
|
914
|
+
async function resolveProjectRootForPath(path) {
|
|
915
|
+
const normalized = normalizeFilesystemPath(path);
|
|
916
|
+
if (!normalized) {
|
|
917
|
+
return null;
|
|
918
|
+
}
|
|
919
|
+
const statResult = await (0, promises_1.stat)(hostFilesystemPath(normalized)).catch(() => null);
|
|
920
|
+
let current = statResult && statResult.isDirectory() ? normalized : (0, node_path_1.dirname)(normalized);
|
|
921
|
+
current = (0, project_paths_1.canonicalizeProjectPath)(current) ?? current;
|
|
922
|
+
while (current && current !== (0, node_path_1.dirname)(current)) {
|
|
923
|
+
if (await pathExists((0, node_path_1.join)(current, ".git"))) {
|
|
924
|
+
return current;
|
|
925
|
+
}
|
|
926
|
+
current = (0, node_path_1.dirname)(current);
|
|
927
|
+
}
|
|
928
|
+
return statResult ? (0, project_paths_1.canonicalizeProjectPath)(normalized) : null;
|
|
929
|
+
}
|
|
930
|
+
function sessionLabel(session, projectRoot) {
|
|
931
|
+
if (session.title && session.title.trim()) {
|
|
932
|
+
return shorten(session.title, 42);
|
|
933
|
+
}
|
|
934
|
+
if (isHermesCronSession(session)) {
|
|
935
|
+
return hermesFallbackSessionLabel(projectRoot, session.id);
|
|
936
|
+
}
|
|
937
|
+
const prompt = latestHermesSessionUserText(session, 160);
|
|
938
|
+
if (prompt) {
|
|
939
|
+
return shorten(prompt, 42);
|
|
940
|
+
}
|
|
941
|
+
const model = session.model ? session.model.replace(/[-_]+/g, " ") : "";
|
|
942
|
+
return model ? `Hermes ${shorten(model, 24)}` : `Hermes ${session.id.slice(0, 8)}`;
|
|
943
|
+
}
|
|
944
|
+
function sessionSourceKind(session) {
|
|
945
|
+
if (isHermesCronSession(session)) {
|
|
946
|
+
return "hermes:cron";
|
|
947
|
+
}
|
|
948
|
+
const source = session.source && session.source.trim() ? session.source.trim() : "local";
|
|
949
|
+
return session.model ? `hermes:${source}:${session.model}` : `hermes:${source}`;
|
|
950
|
+
}
|
|
951
|
+
function sessionRole(session) {
|
|
952
|
+
return isHermesCronSession(session) ? "temporary" : "hermes";
|
|
953
|
+
}
|
|
954
|
+
function sessionStatusText(session, isOngoing) {
|
|
955
|
+
if (isOngoing) {
|
|
956
|
+
return "active";
|
|
957
|
+
}
|
|
958
|
+
if (isHermesCronSession(session)) {
|
|
959
|
+
return "temporary";
|
|
960
|
+
}
|
|
961
|
+
return session.endReason ?? (session.endedAt ? "ended" : "open");
|
|
962
|
+
}
|
|
963
|
+
function isCliLikeHermesSource(session) {
|
|
964
|
+
const source = (session.source ?? "").toLowerCase();
|
|
965
|
+
return source === "" || source === "cli" || source === "local" || source === "unknown";
|
|
966
|
+
}
|
|
967
|
+
function isHermesCompressionContinuation(session) {
|
|
968
|
+
return Boolean(session.parentSessionId
|
|
969
|
+
&& session.parentEndReason === "compression"
|
|
970
|
+
&& session.parentEndedAt !== null
|
|
971
|
+
&& session.startedAt >= session.parentEndedAt);
|
|
972
|
+
}
|
|
973
|
+
function hermesHookRecordTimestampMs(record, fallback) {
|
|
974
|
+
const value = record.timestamp;
|
|
975
|
+
if (typeof value === "string") {
|
|
976
|
+
const parsed = Date.parse(value);
|
|
977
|
+
if (Number.isFinite(parsed)) {
|
|
978
|
+
return parsed;
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
return fallback;
|
|
982
|
+
}
|
|
983
|
+
function hermesHookString(record, key) {
|
|
984
|
+
const value = record[key];
|
|
985
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
986
|
+
}
|
|
987
|
+
function hermesHookPayloadString(record, key) {
|
|
988
|
+
const value = record.payload[key];
|
|
989
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
990
|
+
}
|
|
991
|
+
function hermesHookPayloadNumber(record, key) {
|
|
992
|
+
const value = record.payload[key];
|
|
993
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
994
|
+
}
|
|
995
|
+
function hermesHookEventTurnId(record) {
|
|
996
|
+
return hermesHookPayloadString(record, "turn_id")
|
|
997
|
+
?? hermesHookPayloadString(record, "parent_turn_id");
|
|
998
|
+
}
|
|
999
|
+
function hermesHookEventItemId(record) {
|
|
1000
|
+
return hermesHookPayloadString(record, "tool_call_id")
|
|
1001
|
+
?? hermesHookPayloadString(record, "child_subagent_id")
|
|
1002
|
+
?? hermesHookPayloadString(record, "child_session_id")
|
|
1003
|
+
?? hermesHookPayloadString(record, "task_id");
|
|
1004
|
+
}
|
|
1005
|
+
function limitHermesHookValue(value, depth = 0) {
|
|
1006
|
+
if (depth > 5) {
|
|
1007
|
+
return typeof value === "string" ? shorten(value, HERMES_HOOK_TEXT_LIMIT) : String(value);
|
|
1008
|
+
}
|
|
1009
|
+
if (typeof value === "string") {
|
|
1010
|
+
return value.length <= HERMES_HOOK_TEXT_LIMIT
|
|
1011
|
+
? value
|
|
1012
|
+
: `${value.slice(0, HERMES_HOOK_TEXT_LIMIT)}...[truncated]`;
|
|
1013
|
+
}
|
|
1014
|
+
if (value === null || typeof value === "number" || typeof value === "boolean") {
|
|
1015
|
+
return value;
|
|
1016
|
+
}
|
|
1017
|
+
if (Array.isArray(value)) {
|
|
1018
|
+
return value.slice(0, 40).map((entry) => limitHermesHookValue(entry, depth + 1));
|
|
1019
|
+
}
|
|
1020
|
+
if (value && typeof value === "object") {
|
|
1021
|
+
return Object.fromEntries(Object.entries(value)
|
|
1022
|
+
.slice(0, 60)
|
|
1023
|
+
.map(([key, entry]) => [key, limitHermesHookValue(entry, depth + 1)]));
|
|
1024
|
+
}
|
|
1025
|
+
return undefined;
|
|
1026
|
+
}
|
|
1027
|
+
async function readHermesHookFileTail(path, size) {
|
|
1028
|
+
const hostPath = hostFilesystemPath(path);
|
|
1029
|
+
if (size <= HERMES_HOOK_FILE_BYTE_LIMIT) {
|
|
1030
|
+
return (0, promises_1.readFile)(hostPath, "utf8").catch(() => "");
|
|
1031
|
+
}
|
|
1032
|
+
const handle = await (0, promises_1.open)(hostPath, "r").catch(() => null);
|
|
1033
|
+
if (!handle) {
|
|
1034
|
+
return "";
|
|
1035
|
+
}
|
|
1036
|
+
try {
|
|
1037
|
+
const length = HERMES_HOOK_FILE_BYTE_LIMIT;
|
|
1038
|
+
const position = Math.max(0, size - length);
|
|
1039
|
+
const buffer = Buffer.alloc(length);
|
|
1040
|
+
const { bytesRead } = await handle.read(buffer, 0, length, position);
|
|
1041
|
+
const text = buffer.subarray(0, bytesRead).toString("utf8");
|
|
1042
|
+
const firstNewline = text.indexOf("\n");
|
|
1043
|
+
return firstNewline >= 0 ? text.slice(firstNewline + 1) : text;
|
|
1044
|
+
}
|
|
1045
|
+
finally {
|
|
1046
|
+
await handle.close().catch(() => undefined);
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
function normalizeHermesHookRecord(raw, fallback) {
|
|
1050
|
+
const payload = raw.payload && typeof raw.payload === "object" && !Array.isArray(raw.payload)
|
|
1051
|
+
? limitHermesHookValue(raw.payload)
|
|
1052
|
+
: {};
|
|
1053
|
+
const sessionId = hermesHookString(raw, "session_id")
|
|
1054
|
+
?? hermesHookString(payload, "session_id")
|
|
1055
|
+
?? hermesHookString(payload, "parent_session_id")
|
|
1056
|
+
?? hermesHookString(payload, "child_session_id")
|
|
1057
|
+
?? fallback.sessionId;
|
|
1058
|
+
return {
|
|
1059
|
+
sessionId,
|
|
1060
|
+
eventName: hermesHookString(raw, "hook_event_name") ?? "unknown",
|
|
1061
|
+
timestampMs: hermesHookRecordTimestampMs(raw, fallback.updatedAtMs),
|
|
1062
|
+
cwd: normalizeFilesystemPath(hermesHookString(raw, "cwd")),
|
|
1063
|
+
processCwd: normalizeFilesystemPath(hermesHookString(raw, "process_cwd")),
|
|
1064
|
+
payload,
|
|
1065
|
+
raw: {}
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
async function loadHermesHookSessions(limit) {
|
|
1069
|
+
const files = [];
|
|
1070
|
+
for (const hooksDir of hermesHookDirectories()) {
|
|
1071
|
+
const entries = await (0, promises_1.readdir)(hostFilesystemPath(hooksDir), { withFileTypes: true }).catch(() => []);
|
|
1072
|
+
for (const entry of entries) {
|
|
1073
|
+
if (!entry.isFile() || !entry.name.endsWith(".jsonl")) {
|
|
1074
|
+
continue;
|
|
1075
|
+
}
|
|
1076
|
+
const filePath = (0, node_path_1.join)(hooksDir, entry.name);
|
|
1077
|
+
const fileStats = await (0, promises_1.stat)(hostFilesystemPath(filePath)).catch(() => null);
|
|
1078
|
+
if (!fileStats?.isFile()) {
|
|
1079
|
+
continue;
|
|
1080
|
+
}
|
|
1081
|
+
files.push({
|
|
1082
|
+
sessionId: entry.name.replace(/\.jsonl$/i, ""),
|
|
1083
|
+
path: filePath,
|
|
1084
|
+
updatedAtMs: fileStats.mtimeMs,
|
|
1085
|
+
size: fileStats.size
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
const groups = new Map();
|
|
1090
|
+
for (const file of files
|
|
1091
|
+
.sort((left, right) => right.updatedAtMs - left.updatedAtMs)
|
|
1092
|
+
.slice(0, Math.max(limit, HERMES_SESSION_SCAN_LIMIT))) {
|
|
1093
|
+
const raw = await readHermesHookFileTail(file.path, file.size);
|
|
1094
|
+
const records = raw
|
|
1095
|
+
.split(/\r?\n/)
|
|
1096
|
+
.map((line) => line.trim())
|
|
1097
|
+
.filter(Boolean)
|
|
1098
|
+
.slice(-HERMES_HOOK_RECORD_LIMIT)
|
|
1099
|
+
.map((line) => {
|
|
1100
|
+
if (Buffer.byteLength(line, "utf8") > HERMES_HOOK_LINE_BYTE_LIMIT) {
|
|
1101
|
+
return null;
|
|
1102
|
+
}
|
|
1103
|
+
try {
|
|
1104
|
+
return normalizeHermesHookRecord(JSON.parse(line), file);
|
|
1105
|
+
}
|
|
1106
|
+
catch {
|
|
1107
|
+
return null;
|
|
1108
|
+
}
|
|
1109
|
+
})
|
|
1110
|
+
.filter((record) => record !== null)
|
|
1111
|
+
.sort((left, right) => left.timestampMs - right.timestampMs);
|
|
1112
|
+
if (records.length > 0) {
|
|
1113
|
+
const sessionId = records[records.length - 1].sessionId;
|
|
1114
|
+
const existing = groups.get(sessionId) ?? [];
|
|
1115
|
+
groups.set(sessionId, [...existing, ...records].sort((left, right) => left.timestampMs - right.timestampMs));
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
return groups;
|
|
1119
|
+
}
|
|
1120
|
+
function collectHermesHookPathCandidates(records) {
|
|
1121
|
+
const paths = new Set();
|
|
1122
|
+
for (const record of records) {
|
|
1123
|
+
for (const candidate of hermesHookPayloadPathCandidates(record)) {
|
|
1124
|
+
paths.add(candidate);
|
|
1125
|
+
}
|
|
1126
|
+
if (record.cwd) {
|
|
1127
|
+
paths.add(record.cwd);
|
|
1128
|
+
}
|
|
1129
|
+
if (record.processCwd) {
|
|
1130
|
+
paths.add(record.processCwd);
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
return [...paths].slice(0, HERMES_HOOK_PATH_LIMIT);
|
|
1134
|
+
}
|
|
1135
|
+
function hermesHookPayloadPathCandidates(record) {
|
|
1136
|
+
const paths = new Set();
|
|
1137
|
+
const text = JSON.stringify(record.payload).slice(0, HERMES_HOOK_TEXT_LIMIT * 4);
|
|
1138
|
+
for (const candidate of extractAbsolutePathCandidates(text)) {
|
|
1139
|
+
const normalized = normalizeCandidatePath(candidate, record.cwd ?? record.processCwd ?? "/");
|
|
1140
|
+
if (normalized) {
|
|
1141
|
+
paths.add(normalized);
|
|
1142
|
+
}
|
|
1143
|
+
if (paths.size >= HERMES_HOOK_PATH_LIMIT) {
|
|
1144
|
+
break;
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
return [...paths];
|
|
1148
|
+
}
|
|
1149
|
+
function recentHermesHookRecordsForProjectRelation(records) {
|
|
1150
|
+
return records.slice(-HERMES_PROJECT_RELATION_ACTION_WINDOW);
|
|
1151
|
+
}
|
|
1152
|
+
async function currentProjectRootForHermesHookSession(records) {
|
|
1153
|
+
const recentRecords = recentHermesHookRecordsForProjectRelation(records);
|
|
1154
|
+
for (const record of [...recentRecords].reverse()) {
|
|
1155
|
+
const candidates = hermesHookPayloadPathCandidates(record);
|
|
1156
|
+
for (const candidate of candidates) {
|
|
1157
|
+
const root = await resolveProjectRootForPath(candidate);
|
|
1158
|
+
if (root) {
|
|
1159
|
+
return root;
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
for (const record of [...recentRecords].reverse()) {
|
|
1164
|
+
const candidates = [record.cwd, record.processCwd].filter((entry) => Boolean(entry));
|
|
1165
|
+
for (const candidate of candidates) {
|
|
1166
|
+
const root = await resolveProjectRootForPath(candidate);
|
|
1167
|
+
if (root) {
|
|
1168
|
+
return root;
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
return null;
|
|
1173
|
+
}
|
|
1174
|
+
function hermesHookNestedString(record, path) {
|
|
1175
|
+
let value = record.payload;
|
|
1176
|
+
for (const key of path) {
|
|
1177
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1178
|
+
return null;
|
|
1179
|
+
}
|
|
1180
|
+
value = value[key];
|
|
1181
|
+
}
|
|
1182
|
+
return typeof value === "string" && value.trim() ? shorten(value, 240) : null;
|
|
1183
|
+
}
|
|
1184
|
+
function hermesHookText(record, keys) {
|
|
1185
|
+
for (const key of keys) {
|
|
1186
|
+
const value = record.payload[key];
|
|
1187
|
+
if (typeof value === "string" && value.trim()) {
|
|
1188
|
+
return shorten(value, 240);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
if (keys.includes("user_message")) {
|
|
1192
|
+
return hermesHookNestedString(record, ["event", "text"])
|
|
1193
|
+
?? hermesHookNestedString(record, ["event", "message"]);
|
|
1194
|
+
}
|
|
1195
|
+
return null;
|
|
1196
|
+
}
|
|
1197
|
+
function hermesHookToolName(record) {
|
|
1198
|
+
const value = record.payload.tool_name;
|
|
1199
|
+
if (typeof value === "string" && value.trim()) {
|
|
1200
|
+
return value.trim();
|
|
1201
|
+
}
|
|
1202
|
+
if (record.eventName === "transform_terminal_output") {
|
|
1203
|
+
return "terminal";
|
|
1204
|
+
}
|
|
1205
|
+
return "tool";
|
|
1206
|
+
}
|
|
1207
|
+
function hermesHookPlatform(record) {
|
|
1208
|
+
return hermesHookPayloadString(record, "platform");
|
|
1209
|
+
}
|
|
1210
|
+
function isNonSessionHermesHookId(sessionId) {
|
|
1211
|
+
return sessionId === "default"
|
|
1212
|
+
|| sessionId.startsWith("process-")
|
|
1213
|
+
|| /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(sessionId);
|
|
1214
|
+
}
|
|
1215
|
+
function isDurableHermesHookSessionId(sessionId) {
|
|
1216
|
+
return /^\d{8}_\d{6}_[0-9a-f]+$/i.test(sessionId)
|
|
1217
|
+
|| isHermesCronSessionId(sessionId);
|
|
1218
|
+
}
|
|
1219
|
+
function hermesHookSourceKind(sessionId) {
|
|
1220
|
+
return isHermesCronSessionId(sessionId) ? "hermes:cron" : "hermes:hook";
|
|
1221
|
+
}
|
|
1222
|
+
function hermesHookRole(sessionId) {
|
|
1223
|
+
return isHermesCronSessionId(sessionId) ? "temporary" : "hermes";
|
|
1224
|
+
}
|
|
1225
|
+
function hermesHookStatusText(sessionId, isOngoing) {
|
|
1226
|
+
if (isOngoing) {
|
|
1227
|
+
return "active";
|
|
1228
|
+
}
|
|
1229
|
+
return isHermesCronSessionId(sessionId) ? "temporary" : "hook";
|
|
1230
|
+
}
|
|
1231
|
+
function canonicalHermesSessionIdForHookRecords(records, sessions) {
|
|
1232
|
+
const sessionById = new Map(sessions.map((session) => [session.id, session]));
|
|
1233
|
+
for (const record of [...records].reverse()) {
|
|
1234
|
+
if (sessionById.has(record.sessionId)) {
|
|
1235
|
+
return record.sessionId;
|
|
1236
|
+
}
|
|
1237
|
+
for (const key of ["session_id", "parent_session_id", "child_session_id", "conversation_id"]) {
|
|
1238
|
+
const candidate = hermesHookPayloadString(record, key);
|
|
1239
|
+
if (candidate && sessionById.has(candidate)) {
|
|
1240
|
+
return candidate;
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
const latest = records[records.length - 1];
|
|
1245
|
+
if (!latest || !isNonSessionHermesHookId(latest.sessionId)) {
|
|
1246
|
+
return null;
|
|
1247
|
+
}
|
|
1248
|
+
const latestSeconds = latest.timestampMs / 1000;
|
|
1249
|
+
const platform = hermesHookPlatform(latest);
|
|
1250
|
+
const candidates = sessions
|
|
1251
|
+
.filter((session) => {
|
|
1252
|
+
const started = Number(session.startedAt);
|
|
1253
|
+
const ended = session.endedAt === null ? Number.POSITIVE_INFINITY : Number(session.endedAt);
|
|
1254
|
+
return Number.isFinite(started)
|
|
1255
|
+
&& latestSeconds >= started - 60
|
|
1256
|
+
&& latestSeconds <= ended + 10 * 60;
|
|
1257
|
+
})
|
|
1258
|
+
.sort((left, right) => {
|
|
1259
|
+
const leftPlatformScore = platform && left.source === platform ? 1 : 0;
|
|
1260
|
+
const rightPlatformScore = platform && right.source === platform ? 1 : 0;
|
|
1261
|
+
if (rightPlatformScore !== leftPlatformScore) {
|
|
1262
|
+
return rightPlatformScore - leftPlatformScore;
|
|
1263
|
+
}
|
|
1264
|
+
const leftDistance = Math.min(Math.abs(left.lastActive - latestSeconds), Math.abs(left.startedAt - latestSeconds), left.endedAt === null ? Number.POSITIVE_INFINITY : Math.abs(left.endedAt - latestSeconds));
|
|
1265
|
+
const rightDistance = Math.min(Math.abs(right.lastActive - latestSeconds), Math.abs(right.startedAt - latestSeconds), right.endedAt === null ? Number.POSITIVE_INFINITY : Math.abs(right.endedAt - latestSeconds));
|
|
1266
|
+
if (leftDistance !== rightDistance) {
|
|
1267
|
+
return leftDistance - rightDistance;
|
|
1268
|
+
}
|
|
1269
|
+
return right.lastActive - left.lastActive;
|
|
1270
|
+
});
|
|
1271
|
+
return candidates[0]?.id ?? null;
|
|
1272
|
+
}
|
|
1273
|
+
function hermesHookToolArgs(record) {
|
|
1274
|
+
const value = record.payload.args;
|
|
1275
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
1276
|
+
return value;
|
|
1277
|
+
}
|
|
1278
|
+
if (record.eventName === "transform_terminal_output" && typeof record.payload.command === "string") {
|
|
1279
|
+
return { command: record.payload.command };
|
|
1280
|
+
}
|
|
1281
|
+
return {};
|
|
1282
|
+
}
|
|
1283
|
+
function hermesHookIsToolActivity(eventName) {
|
|
1284
|
+
return eventName === "pre_tool_call"
|
|
1285
|
+
|| eventName === "post_tool_call"
|
|
1286
|
+
|| eventName === "transform_tool_result"
|
|
1287
|
+
|| eventName === "transform_terminal_output";
|
|
1288
|
+
}
|
|
1289
|
+
function hermesHookIsToolCompletion(eventName) {
|
|
1290
|
+
return eventName === "post_tool_call"
|
|
1291
|
+
|| eventName === "transform_tool_result"
|
|
1292
|
+
|| eventName === "transform_terminal_output";
|
|
1293
|
+
}
|
|
1294
|
+
function hermesHookResponseText(record) {
|
|
1295
|
+
return hermesHookText(record, ["assistant_response", "response_text"]);
|
|
1296
|
+
}
|
|
1297
|
+
function hermesHookTerminalOutput(record) {
|
|
1298
|
+
return hermesHookText(record, ["output", "result"]);
|
|
1299
|
+
}
|
|
1300
|
+
function hermesHookApiDetail(record) {
|
|
1301
|
+
const provider = typeof record.payload.provider === "string" ? record.payload.provider : "";
|
|
1302
|
+
const model = typeof record.payload.model === "string" ? record.payload.model : "";
|
|
1303
|
+
const finishReason = typeof record.payload.finish_reason === "string" ? record.payload.finish_reason : "";
|
|
1304
|
+
const modelLabel = model.trim() || provider.trim();
|
|
1305
|
+
if (record.eventName === "pre_api_request") {
|
|
1306
|
+
return modelLabel ? `Thinking with ${modelLabel}` : "Thinking with model";
|
|
1307
|
+
}
|
|
1308
|
+
if (/tool/i.test(finishReason)) {
|
|
1309
|
+
return modelLabel ? `Using tools after ${modelLabel}` : "Using tools";
|
|
1310
|
+
}
|
|
1311
|
+
if (/stop|complete|success/i.test(finishReason)) {
|
|
1312
|
+
return modelLabel ? `Answered with ${modelLabel}` : "Answered";
|
|
1313
|
+
}
|
|
1314
|
+
return modelLabel ? `Model request ${modelLabel}` : "Model request";
|
|
1315
|
+
}
|
|
1316
|
+
function hermesHookResultLooksFailed(record) {
|
|
1317
|
+
if (typeof record.payload.returncode === "number"
|
|
1318
|
+
&& record.payload.returncode !== 0) {
|
|
1319
|
+
return true;
|
|
1320
|
+
}
|
|
1321
|
+
if (typeof record.payload.status === "string"
|
|
1322
|
+
&& /fail|error/i.test(record.payload.status)) {
|
|
1323
|
+
return true;
|
|
1324
|
+
}
|
|
1325
|
+
const result = parseMaybeJson(record.payload.result ?? record.payload.output);
|
|
1326
|
+
if (result && typeof result === "object") {
|
|
1327
|
+
const object = result;
|
|
1328
|
+
if (typeof object.error === "string" && object.error.trim()) {
|
|
1329
|
+
return true;
|
|
1330
|
+
}
|
|
1331
|
+
if (typeof object.exit_code === "number" && object.exit_code !== 0) {
|
|
1332
|
+
return true;
|
|
1333
|
+
}
|
|
1334
|
+
if (typeof object.returncode === "number" && object.returncode !== 0) {
|
|
1335
|
+
return true;
|
|
1336
|
+
}
|
|
1337
|
+
if (typeof object.status === "string" && /fail|error/i.test(object.status)) {
|
|
1338
|
+
return true;
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
return false;
|
|
1342
|
+
}
|
|
1343
|
+
function hermesHookToolActivityEvent(input) {
|
|
1344
|
+
const toolName = hermesHookToolName(input.record);
|
|
1345
|
+
const args = hermesHookToolArgs(input.record);
|
|
1346
|
+
const kind = toolKind(toolName);
|
|
1347
|
+
const title = input.failed ? `${toolName} failed` : toolTitle(toolName, args);
|
|
1348
|
+
return {
|
|
1349
|
+
type: kind.eventType,
|
|
1350
|
+
action: kind.eventKind === "command" ? "ran"
|
|
1351
|
+
: kind.eventKind === "fileChange" ? "edited"
|
|
1352
|
+
: "updated",
|
|
1353
|
+
path: toolPath(input.projectRoot, input.record.cwd, args) ?? input.paths[0] ?? input.projectRoot,
|
|
1354
|
+
title,
|
|
1355
|
+
isImage: false
|
|
1356
|
+
};
|
|
1357
|
+
}
|
|
1358
|
+
function hermesHookSubagentStatus(record) {
|
|
1359
|
+
const value = record.payload.child_status;
|
|
1360
|
+
return typeof value === "string" && value.trim() ? value.trim().toLowerCase() : null;
|
|
1361
|
+
}
|
|
1362
|
+
function hermesHookSubagentRole(record) {
|
|
1363
|
+
return hermesHookText(record, ["child_role"]);
|
|
1364
|
+
}
|
|
1365
|
+
function hermesHookSubagentGoal(record) {
|
|
1366
|
+
return hermesHookText(record, ["child_goal"]);
|
|
1367
|
+
}
|
|
1368
|
+
function hermesHookSubagentTitle(record) {
|
|
1369
|
+
const role = hermesHookSubagentRole(record);
|
|
1370
|
+
return role ? `Subagent ${role}` : "Subagent";
|
|
1371
|
+
}
|
|
1372
|
+
function hermesHookToolSummary(record) {
|
|
1373
|
+
const toolName = hermesHookToolName(record);
|
|
1374
|
+
const args = hermesHookToolArgs(record);
|
|
1375
|
+
const failed = hermesHookIsToolCompletion(record.eventName) && hermesHookResultLooksFailed(record);
|
|
1376
|
+
const kind = toolKind(toolName);
|
|
1377
|
+
return {
|
|
1378
|
+
toolName,
|
|
1379
|
+
args,
|
|
1380
|
+
failed,
|
|
1381
|
+
kind,
|
|
1382
|
+
title: failed ? `${toolName} failed` : toolTitle(toolName, args),
|
|
1383
|
+
latestMessage: hermesHookResponseText(record) ?? hermesHookTerminalOutput(record)
|
|
1384
|
+
};
|
|
1385
|
+
}
|
|
1386
|
+
function hermesHookMeaningfulText(record) {
|
|
1387
|
+
const assistantText = hermesHookResponseText(record);
|
|
1388
|
+
if (assistantText) {
|
|
1389
|
+
return assistantText;
|
|
1390
|
+
}
|
|
1391
|
+
const childSummary = hermesHookText(record, ["child_summary"]);
|
|
1392
|
+
if (childSummary) {
|
|
1393
|
+
return childSummary;
|
|
1394
|
+
}
|
|
1395
|
+
const terminalOutput = hermesHookTerminalOutput(record);
|
|
1396
|
+
if (terminalOutput) {
|
|
1397
|
+
return terminalOutput;
|
|
1398
|
+
}
|
|
1399
|
+
if (hermesHookIsToolActivity(record.eventName)) {
|
|
1400
|
+
return hermesHookToolSummary(record).title;
|
|
1401
|
+
}
|
|
1402
|
+
return null;
|
|
1403
|
+
}
|
|
1404
|
+
function hermesHookDisplayUserText(record) {
|
|
1405
|
+
const userText = hermesHookText(record, ["user_message"]);
|
|
1406
|
+
if (!userText) {
|
|
1407
|
+
return null;
|
|
1408
|
+
}
|
|
1409
|
+
if (/^\[IMPORTANT:/i.test(userText) && hermesSkillNameFromText(userText)) {
|
|
1410
|
+
return null;
|
|
1411
|
+
}
|
|
1412
|
+
const title = hermesTitleFromText(userText);
|
|
1413
|
+
if (title && hermesTitleLooksGenericPrompt(title)) {
|
|
1414
|
+
return null;
|
|
1415
|
+
}
|
|
1416
|
+
return userText;
|
|
1417
|
+
}
|
|
1418
|
+
function hermesHookConversationText(record) {
|
|
1419
|
+
return hermesHookResponseText(record)
|
|
1420
|
+
?? hermesHookText(record, ["child_summary"]);
|
|
1421
|
+
}
|
|
1422
|
+
function latestHermesHookMeaningfulText(records) {
|
|
1423
|
+
for (let index = records.length - 1; index >= 0; index -= 1) {
|
|
1424
|
+
const text = hermesHookMeaningfulText(records[index]);
|
|
1425
|
+
if (text) {
|
|
1426
|
+
return text;
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
return null;
|
|
1430
|
+
}
|
|
1431
|
+
function latestHermesHookConversationText(records) {
|
|
1432
|
+
for (let index = records.length - 1; index >= 0; index -= 1) {
|
|
1433
|
+
const text = hermesHookConversationText(records[index]);
|
|
1434
|
+
if (text) {
|
|
1435
|
+
return text;
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
return null;
|
|
1439
|
+
}
|
|
1440
|
+
function summarizeHermesHookSession(input) {
|
|
1441
|
+
const latest = input.records[input.records.length - 1];
|
|
1442
|
+
const ageMs = input.now - latest.timestampMs;
|
|
1443
|
+
const recentOpen = ageMs <= HERMES_RECENT_OPEN_WINDOW_MS;
|
|
1444
|
+
const recentDone = ageMs <= HERMES_RECENT_DONE_WINDOW_MS;
|
|
1445
|
+
const isFinalized = latest.eventName === "on_session_finalize";
|
|
1446
|
+
const assistantText = hermesHookResponseText(latest);
|
|
1447
|
+
const userText = hermesHookText(latest, ["user_message"]);
|
|
1448
|
+
const conversationText = latestHermesHookConversationText(input.records);
|
|
1449
|
+
if (hermesHookIsToolActivity(latest.eventName)) {
|
|
1450
|
+
const toolSummary = hermesHookToolSummary(latest);
|
|
1451
|
+
return {
|
|
1452
|
+
state: toolSummary.failed ? "blocked" : toolSummary.kind.state,
|
|
1453
|
+
isOngoing: recentOpen && !isFinalized,
|
|
1454
|
+
detail: toolSummary.title,
|
|
1455
|
+
paths: input.paths,
|
|
1456
|
+
activityEvent: hermesHookToolActivityEvent({
|
|
1457
|
+
record: latest,
|
|
1458
|
+
projectRoot: input.projectRoot,
|
|
1459
|
+
paths: input.paths,
|
|
1460
|
+
failed: toolSummary.failed
|
|
1461
|
+
}),
|
|
1462
|
+
latestMessage: conversationText,
|
|
1463
|
+
updatedAtMs: latest.timestampMs,
|
|
1464
|
+
stoppedAtMs: toolSummary.failed || isFinalized ? latest.timestampMs : null
|
|
1465
|
+
};
|
|
1466
|
+
}
|
|
1467
|
+
if (latest.eventName === "post_llm_call" || latest.eventName === "transform_llm_output") {
|
|
1468
|
+
return {
|
|
1469
|
+
state: recentDone ? "done" : "idle",
|
|
1470
|
+
isOngoing: recentDone && !isFinalized,
|
|
1471
|
+
detail: assistantText ?? "Hermes reply",
|
|
1472
|
+
paths: input.paths,
|
|
1473
|
+
activityEvent: assistantText
|
|
1474
|
+
? {
|
|
1475
|
+
type: "agentMessage",
|
|
1476
|
+
action: "said",
|
|
1477
|
+
path: input.paths[0] ?? input.projectRoot,
|
|
1478
|
+
title: assistantText,
|
|
1479
|
+
isImage: false
|
|
1480
|
+
}
|
|
1481
|
+
: null,
|
|
1482
|
+
latestMessage: conversationText ?? assistantText,
|
|
1483
|
+
updatedAtMs: latest.timestampMs,
|
|
1484
|
+
stoppedAtMs: recentDone || isFinalized ? latest.timestampMs : null
|
|
1485
|
+
};
|
|
1486
|
+
}
|
|
1487
|
+
if (latest.eventName === "pre_api_request" || latest.eventName === "post_api_request") {
|
|
1488
|
+
const detail = hermesHookApiDetail(latest);
|
|
1489
|
+
return {
|
|
1490
|
+
state: latest.eventName === "pre_api_request" ? "thinking" : (recentOpen ? "thinking" : "idle"),
|
|
1491
|
+
isOngoing: recentOpen && !isFinalized,
|
|
1492
|
+
detail,
|
|
1493
|
+
paths: input.paths,
|
|
1494
|
+
activityEvent: {
|
|
1495
|
+
type: "reasoning",
|
|
1496
|
+
action: "updated",
|
|
1497
|
+
path: input.paths[0] ?? input.projectRoot,
|
|
1498
|
+
title: detail,
|
|
1499
|
+
isImage: false
|
|
1500
|
+
},
|
|
1501
|
+
latestMessage: conversationText,
|
|
1502
|
+
updatedAtMs: latest.timestampMs,
|
|
1503
|
+
stoppedAtMs: recentOpen && !isFinalized ? null : latest.timestampMs
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
if (latest.eventName === "pre_verify") {
|
|
1507
|
+
const changedPaths = hermesHookChangedPaths(latest, input.projectRoot);
|
|
1508
|
+
const detail = hermesHookPreVerifyDetail(latest, input.projectRoot);
|
|
1509
|
+
return {
|
|
1510
|
+
state: "validating",
|
|
1511
|
+
isOngoing: recentOpen && !isFinalized,
|
|
1512
|
+
detail,
|
|
1513
|
+
paths: [...new Set([...changedPaths, ...input.paths])].slice(0, HERMES_HOOK_PATH_LIMIT),
|
|
1514
|
+
activityEvent: {
|
|
1515
|
+
type: "other",
|
|
1516
|
+
action: "updated",
|
|
1517
|
+
path: changedPaths[0] ?? input.paths[0] ?? input.projectRoot,
|
|
1518
|
+
title: detail,
|
|
1519
|
+
isImage: false
|
|
1520
|
+
},
|
|
1521
|
+
latestMessage: conversationText,
|
|
1522
|
+
updatedAtMs: latest.timestampMs,
|
|
1523
|
+
stoppedAtMs: recentOpen && !isFinalized ? null : latest.timestampMs
|
|
1524
|
+
};
|
|
1525
|
+
}
|
|
1526
|
+
if (latest.eventName === "subagent_start") {
|
|
1527
|
+
const title = hermesHookSubagentTitle(latest);
|
|
1528
|
+
const childGoal = hermesHookSubagentGoal(latest);
|
|
1529
|
+
return {
|
|
1530
|
+
state: "delegating",
|
|
1531
|
+
isOngoing: recentOpen && !isFinalized,
|
|
1532
|
+
detail: childGoal ?? `${title} starting`,
|
|
1533
|
+
paths: input.paths,
|
|
1534
|
+
activityEvent: {
|
|
1535
|
+
type: "collabAgentToolCall",
|
|
1536
|
+
action: "updated",
|
|
1537
|
+
path: input.paths[0] ?? input.projectRoot,
|
|
1538
|
+
title: childGoal ?? title,
|
|
1539
|
+
isImage: false
|
|
1540
|
+
},
|
|
1541
|
+
latestMessage: conversationText,
|
|
1542
|
+
updatedAtMs: latest.timestampMs,
|
|
1543
|
+
stoppedAtMs: recentOpen && !isFinalized ? null : latest.timestampMs
|
|
1544
|
+
};
|
|
1545
|
+
}
|
|
1546
|
+
if (latest.eventName === "subagent_stop") {
|
|
1547
|
+
const childRole = hermesHookSubagentRole(latest);
|
|
1548
|
+
const childSummary = hermesHookText(latest, ["child_summary"]);
|
|
1549
|
+
const status = hermesHookSubagentStatus(latest);
|
|
1550
|
+
const failed = status ? /fail|error|interrupt/.test(status) : false;
|
|
1551
|
+
const title = childRole ? `Subagent ${childRole}` : "Subagent";
|
|
1552
|
+
return {
|
|
1553
|
+
state: failed ? "blocked" : "delegating",
|
|
1554
|
+
isOngoing: recentOpen && !isFinalized,
|
|
1555
|
+
detail: childSummary ?? (status ? `${title} ${status}` : `${title} stopped`),
|
|
1556
|
+
paths: input.paths,
|
|
1557
|
+
activityEvent: {
|
|
1558
|
+
type: "collabAgentToolCall",
|
|
1559
|
+
action: "updated",
|
|
1560
|
+
path: input.paths[0] ?? input.projectRoot,
|
|
1561
|
+
title: childSummary ?? title,
|
|
1562
|
+
isImage: false
|
|
1563
|
+
},
|
|
1564
|
+
latestMessage: conversationText ?? childSummary,
|
|
1565
|
+
updatedAtMs: latest.timestampMs,
|
|
1566
|
+
stoppedAtMs: failed || !recentOpen ? latest.timestampMs : null
|
|
1567
|
+
};
|
|
1568
|
+
}
|
|
1569
|
+
if (latest.eventName === "on_session_end" || latest.eventName === "on_session_finalize") {
|
|
1570
|
+
const meaningfulText = conversationText ?? latestHermesHookMeaningfulText(input.records);
|
|
1571
|
+
return {
|
|
1572
|
+
state: "done",
|
|
1573
|
+
isOngoing: false,
|
|
1574
|
+
detail: meaningfulText ?? (latest.eventName === "on_session_end" ? "Hermes session ended" : "Hermes session finalized"),
|
|
1575
|
+
paths: input.paths,
|
|
1576
|
+
activityEvent: null,
|
|
1577
|
+
latestMessage: meaningfulText ?? assistantText,
|
|
1578
|
+
updatedAtMs: latest.timestampMs,
|
|
1579
|
+
stoppedAtMs: latest.timestampMs
|
|
1580
|
+
};
|
|
1581
|
+
}
|
|
1582
|
+
return {
|
|
1583
|
+
state: "planning",
|
|
1584
|
+
isOngoing: recentOpen,
|
|
1585
|
+
detail: userText ?? "Hermes session activity",
|
|
1586
|
+
paths: input.paths,
|
|
1587
|
+
activityEvent: {
|
|
1588
|
+
type: latest.eventName === "pre_gateway_dispatch" ? "userMessage" : "plan",
|
|
1589
|
+
action: "updated",
|
|
1590
|
+
path: input.paths[0] ?? input.projectRoot,
|
|
1591
|
+
title: userText ?? latest.eventName,
|
|
1592
|
+
isImage: false
|
|
1593
|
+
},
|
|
1594
|
+
latestMessage: conversationText ?? assistantText,
|
|
1595
|
+
updatedAtMs: latest.timestampMs,
|
|
1596
|
+
stoppedAtMs: recentOpen ? null : latest.timestampMs
|
|
1597
|
+
};
|
|
1598
|
+
}
|
|
1599
|
+
function buildHermesHookEvents(input) {
|
|
1600
|
+
const records = input.records.slice(-20);
|
|
1601
|
+
const startIndex = input.records.length - records.length;
|
|
1602
|
+
return records.map((record, index) => {
|
|
1603
|
+
const toolName = hermesHookToolName(record);
|
|
1604
|
+
const kind = hermesHookIsToolActivity(record.eventName) ? toolKind(toolName) : null;
|
|
1605
|
+
const args = kind ? hermesHookToolArgs(record) : {};
|
|
1606
|
+
const toolEventTitle = kind ? toolTitle(toolName, args) : "";
|
|
1607
|
+
const toolEventPath = kind
|
|
1608
|
+
? toolPath(input.projectRoot, record.cwd, args) ?? input.projectRoot
|
|
1609
|
+
: input.projectRoot;
|
|
1610
|
+
const command = kind?.eventKind === "command"
|
|
1611
|
+
? (typeof args.command === "string" && args.command.trim()
|
|
1612
|
+
? shorten(args.command, 240)
|
|
1613
|
+
: toolName === "process" ? toolEventTitle : undefined)
|
|
1614
|
+
: undefined;
|
|
1615
|
+
const assistantText = hermesHookResponseText(record) ?? hermesHookTerminalOutput(record);
|
|
1616
|
+
const userText = hermesHookText(record, ["user_message"]);
|
|
1617
|
+
const lifecycleText = record.eventName === "on_session_end" || record.eventName === "on_session_finalize"
|
|
1618
|
+
? latestHermesHookMeaningfulText(input.records.slice(0, startIndex + index + 1))
|
|
1619
|
+
: null;
|
|
1620
|
+
const changedPaths = record.eventName === "pre_verify"
|
|
1621
|
+
? hermesHookChangedPaths(record, input.projectRoot)
|
|
1622
|
+
: [];
|
|
1623
|
+
const preVerifyDetail = record.eventName === "pre_verify"
|
|
1624
|
+
? hermesHookPreVerifyDetail(record, input.projectRoot)
|
|
1625
|
+
: null;
|
|
1626
|
+
const subagentLifecycle = record.eventName === "subagent_start" || record.eventName === "subagent_stop";
|
|
1627
|
+
const subagentTitle = subagentLifecycle ? hermesHookSubagentTitle(record) : null;
|
|
1628
|
+
const subagentDetail = record.eventName === "subagent_start"
|
|
1629
|
+
? hermesHookSubagentGoal(record) ?? `${subagentTitle ?? "Subagent"} starting`
|
|
1630
|
+
: record.eventName === "subagent_stop"
|
|
1631
|
+
? hermesHookText(record, ["child_summary"])
|
|
1632
|
+
?? (hermesHookSubagentStatus(record)
|
|
1633
|
+
? `${subagentTitle ?? "Subagent"} ${hermesHookSubagentStatus(record)}`
|
|
1634
|
+
: `${subagentTitle ?? "Subagent"} stopped`)
|
|
1635
|
+
: null;
|
|
1636
|
+
const eventPath = changedPaths[0] ?? toolEventPath;
|
|
1637
|
+
const method = kind?.eventType === "plan"
|
|
1638
|
+
? "turn/plan/updated"
|
|
1639
|
+
: kind?.eventKind === "command"
|
|
1640
|
+
? record.eventName === "pre_tool_call" ? "item/started" : "item/commandExecution/outputDelta"
|
|
1641
|
+
: kind?.eventKind === "fileChange"
|
|
1642
|
+
? record.eventName === "pre_tool_call" ? "item/started" : "item/fileChange/outputDelta"
|
|
1643
|
+
: kind?.eventKind === "tool"
|
|
1644
|
+
? toolName.startsWith("mcp_") && record.eventName !== "pre_tool_call" ? "item/mcpToolCall/progress" : "item/tool/call"
|
|
1645
|
+
: record.eventName === "pre_llm_call" || record.eventName === "pre_gateway_dispatch"
|
|
1646
|
+
? "hermes/userMessage"
|
|
1647
|
+
: record.eventName === "post_llm_call" || record.eventName === "transform_llm_output"
|
|
1648
|
+
? "hermes/agentMessage"
|
|
1649
|
+
: record.eventName === "pre_verify"
|
|
1650
|
+
? "hermes/preVerify"
|
|
1651
|
+
: record.eventName === "subagent_start"
|
|
1652
|
+
? "hermes/subagentStart"
|
|
1653
|
+
: record.eventName === "subagent_stop"
|
|
1654
|
+
? "hermes/subagentStop"
|
|
1655
|
+
: `hermes/${record.eventName}`;
|
|
1656
|
+
const eventKind = kind?.eventKind
|
|
1657
|
+
?? (subagentLifecycle ? "subagent"
|
|
1658
|
+
: record.eventName === "pre_api_request" || record.eventName === "post_api_request" || record.eventName === "pre_verify" ? "status" : "message");
|
|
1659
|
+
const apiDetail = record.eventName === "pre_api_request" || record.eventName === "post_api_request"
|
|
1660
|
+
? hermesHookApiDetail(record)
|
|
1661
|
+
: null;
|
|
1662
|
+
const detail = kind ? toolEventTitle
|
|
1663
|
+
: preVerifyDetail ?? subagentDetail ?? apiDetail ?? lifecycleText ?? assistantText ?? userText ?? record.eventName;
|
|
1664
|
+
const action = kind?.eventKind === "command" ? "ran"
|
|
1665
|
+
: kind?.eventKind === "fileChange" ? "edited"
|
|
1666
|
+
: kind?.eventType === "plan" ? "updated"
|
|
1667
|
+
: eventKind === "message" ? "said"
|
|
1668
|
+
: "updated";
|
|
1669
|
+
return {
|
|
1670
|
+
id: `${input.projectRoot}::hermes-hook::${record.sessionId}::${record.timestampMs}::${index}`,
|
|
1671
|
+
source: "hermes",
|
|
1672
|
+
confidence: "typed",
|
|
1673
|
+
threadId: input.threadId ?? record.sessionId,
|
|
1674
|
+
createdAt: new Date(record.timestampMs).toISOString(),
|
|
1675
|
+
method,
|
|
1676
|
+
turnId: hermesHookEventTurnId(record) ?? undefined,
|
|
1677
|
+
itemId: hermesHookEventItemId(record) ?? undefined,
|
|
1678
|
+
requestId: hermesHookPayloadString(record, "api_request_id") ?? undefined,
|
|
1679
|
+
itemType: kind?.eventType === "mcpToolCall" ? "mcpToolCall"
|
|
1680
|
+
: kind?.eventType === "dynamicToolCall" ? "dynamicToolCall"
|
|
1681
|
+
: subagentLifecycle ? "collabAgentToolCall"
|
|
1682
|
+
: eventKind === "message" && (record.eventName === "pre_llm_call" || record.eventName === "pre_gateway_dispatch") ? "userMessage"
|
|
1683
|
+
: eventKind === "message" ? "agentMessage"
|
|
1684
|
+
: undefined,
|
|
1685
|
+
kind: eventKind,
|
|
1686
|
+
phase: kind?.eventType === "plan"
|
|
1687
|
+
? "updated"
|
|
1688
|
+
: hermesHookIsToolCompletion(record.eventName)
|
|
1689
|
+
? hermesHookResultLooksFailed(record) ? "failed" : "completed"
|
|
1690
|
+
: record.eventName === "pre_tool_call" || record.eventName === "pre_verify" || record.eventName === "subagent_start" ? "started" : "updated",
|
|
1691
|
+
title: kind ? toolEventTitle : preVerifyDetail ?? subagentTitle ?? record.eventName,
|
|
1692
|
+
detail,
|
|
1693
|
+
path: eventPath,
|
|
1694
|
+
action,
|
|
1695
|
+
command,
|
|
1696
|
+
cwd: record.cwd ?? undefined,
|
|
1697
|
+
isImage: false
|
|
1698
|
+
};
|
|
1699
|
+
});
|
|
1700
|
+
}
|
|
1701
|
+
function summarizeHermesSession(input) {
|
|
1702
|
+
const { session, projectRoot, cwd, activeProcess, paths, now } = input;
|
|
1703
|
+
const latest = [...session.messages].reverse().find((message) => message.role) ?? null;
|
|
1704
|
+
const latestCall = latestMessageToolCall(latest);
|
|
1705
|
+
const latestMessage = latestAssistantText(session.messages);
|
|
1706
|
+
const latestUser = latestHermesSessionUserText(session);
|
|
1707
|
+
const promptDetail = latestUser ?? (isHermesCronSession(session) ? "Scheduled cron job" : "Hermes prompt");
|
|
1708
|
+
const lastActiveMs = Math.max(session.lastActive * 1000, session.startedAt * 1000);
|
|
1709
|
+
const ageMs = now - lastActiveMs;
|
|
1710
|
+
const isEnded = session.endedAt !== null;
|
|
1711
|
+
const active = Boolean(activeProcess);
|
|
1712
|
+
const freshOpen = !isEnded && ageMs <= HERMES_RECENT_OPEN_WINDOW_MS;
|
|
1713
|
+
const hasUnfinishedTurn = latest?.role === "user"
|
|
1714
|
+
|| latest?.role === "tool"
|
|
1715
|
+
|| (latest?.role === "assistant" && latestCall !== null);
|
|
1716
|
+
const isOngoing = !isEnded && (freshOpen || (hasUnfinishedTurn && active));
|
|
1717
|
+
if (latest?.role === "tool" && messageLooksFailed(latest)) {
|
|
1718
|
+
const toolName = latest.toolName ?? "tool";
|
|
1719
|
+
return {
|
|
1720
|
+
state: "blocked",
|
|
1721
|
+
isOngoing,
|
|
1722
|
+
detail: `${toolName} failed`,
|
|
1723
|
+
paths,
|
|
1724
|
+
activityEvent: {
|
|
1725
|
+
type: toolKind(toolName).eventType,
|
|
1726
|
+
action: "updated",
|
|
1727
|
+
path: paths[0] ?? projectRoot,
|
|
1728
|
+
title: `${toolName} failed`,
|
|
1729
|
+
isImage: false
|
|
1730
|
+
},
|
|
1731
|
+
latestMessage,
|
|
1732
|
+
updatedAtMs: lastActiveMs,
|
|
1733
|
+
stoppedAtMs: isOngoing ? null : lastActiveMs
|
|
1734
|
+
};
|
|
1735
|
+
}
|
|
1736
|
+
if (latestCall && isOngoing) {
|
|
1737
|
+
const kind = toolKind(latestCall.name);
|
|
1738
|
+
return {
|
|
1739
|
+
state: kind.state,
|
|
1740
|
+
isOngoing: true,
|
|
1741
|
+
detail: toolTitle(latestCall.name, latestCall.args),
|
|
1742
|
+
paths,
|
|
1743
|
+
activityEvent: toolActivityEvent(projectRoot, cwd, latestCall),
|
|
1744
|
+
latestMessage,
|
|
1745
|
+
updatedAtMs: lastActiveMs,
|
|
1746
|
+
stoppedAtMs: null
|
|
1747
|
+
};
|
|
1748
|
+
}
|
|
1749
|
+
if (latest?.role === "tool" && isOngoing) {
|
|
1750
|
+
const toolName = latest.toolName ?? "tool";
|
|
1751
|
+
return {
|
|
1752
|
+
state: "thinking",
|
|
1753
|
+
isOngoing: true,
|
|
1754
|
+
detail: `${toolName} result`,
|
|
1755
|
+
paths,
|
|
1756
|
+
activityEvent: {
|
|
1757
|
+
type: toolKind(toolName).eventType,
|
|
1758
|
+
action: "updated",
|
|
1759
|
+
path: paths[0] ?? projectRoot,
|
|
1760
|
+
title: `${toolName} result`,
|
|
1761
|
+
isImage: false
|
|
1762
|
+
},
|
|
1763
|
+
latestMessage,
|
|
1764
|
+
updatedAtMs: lastActiveMs,
|
|
1765
|
+
stoppedAtMs: null
|
|
1766
|
+
};
|
|
1767
|
+
}
|
|
1768
|
+
if (latest?.role === "user" && isOngoing) {
|
|
1769
|
+
return {
|
|
1770
|
+
state: "planning",
|
|
1771
|
+
isOngoing: true,
|
|
1772
|
+
detail: promptDetail,
|
|
1773
|
+
paths,
|
|
1774
|
+
activityEvent: {
|
|
1775
|
+
type: "userMessage",
|
|
1776
|
+
action: "said",
|
|
1777
|
+
path: paths[0] ?? projectRoot,
|
|
1778
|
+
title: promptDetail,
|
|
1779
|
+
isImage: false
|
|
1780
|
+
},
|
|
1781
|
+
latestMessage,
|
|
1782
|
+
updatedAtMs: lastActiveMs,
|
|
1783
|
+
stoppedAtMs: null
|
|
1784
|
+
};
|
|
1785
|
+
}
|
|
1786
|
+
if (active && session.messages.length === 0) {
|
|
1787
|
+
return {
|
|
1788
|
+
state: "thinking",
|
|
1789
|
+
isOngoing: true,
|
|
1790
|
+
detail: latestMessage ?? latestUser ?? "Hermes is active",
|
|
1791
|
+
paths,
|
|
1792
|
+
activityEvent: latestMessage
|
|
1793
|
+
? {
|
|
1794
|
+
type: "agentMessage",
|
|
1795
|
+
action: "said",
|
|
1796
|
+
path: paths[0] ?? projectRoot,
|
|
1797
|
+
title: latestMessage,
|
|
1798
|
+
isImage: false
|
|
1799
|
+
}
|
|
1800
|
+
: null,
|
|
1801
|
+
latestMessage,
|
|
1802
|
+
updatedAtMs: lastActiveMs,
|
|
1803
|
+
stoppedAtMs: null
|
|
1804
|
+
};
|
|
1805
|
+
}
|
|
1806
|
+
if (freshOpen) {
|
|
1807
|
+
return {
|
|
1808
|
+
state: "waiting",
|
|
1809
|
+
isOngoing: true,
|
|
1810
|
+
detail: latestMessage ?? latestUser ?? (isHermesCronSession(session) ? "Scheduled cron job open" : "Hermes session open"),
|
|
1811
|
+
paths,
|
|
1812
|
+
activityEvent: latestMessage
|
|
1813
|
+
? {
|
|
1814
|
+
type: "agentMessage",
|
|
1815
|
+
action: "said",
|
|
1816
|
+
path: paths[0] ?? projectRoot,
|
|
1817
|
+
title: latestMessage,
|
|
1818
|
+
isImage: false
|
|
1819
|
+
}
|
|
1820
|
+
: null,
|
|
1821
|
+
latestMessage,
|
|
1822
|
+
updatedAtMs: lastActiveMs,
|
|
1823
|
+
stoppedAtMs: null
|
|
1824
|
+
};
|
|
1825
|
+
}
|
|
1826
|
+
const recentlyDone = ageMs <= HERMES_RECENT_DONE_WINDOW_MS;
|
|
1827
|
+
return {
|
|
1828
|
+
state: recentlyDone ? "done" : "idle",
|
|
1829
|
+
isOngoing: false,
|
|
1830
|
+
detail: latestMessage ?? latestUser ?? (isEnded ? "Finished" : isHermesCronSession(session) ? "Scheduled cron job" : "Hermes session"),
|
|
1831
|
+
paths,
|
|
1832
|
+
activityEvent: latestMessage
|
|
1833
|
+
? {
|
|
1834
|
+
type: "agentMessage",
|
|
1835
|
+
action: "said",
|
|
1836
|
+
path: paths[0] ?? projectRoot,
|
|
1837
|
+
title: latestMessage,
|
|
1838
|
+
isImage: false
|
|
1839
|
+
}
|
|
1840
|
+
: null,
|
|
1841
|
+
latestMessage,
|
|
1842
|
+
updatedAtMs: lastActiveMs,
|
|
1843
|
+
stoppedAtMs: isEnded || recentlyDone ? lastActiveMs : null
|
|
1844
|
+
};
|
|
1845
|
+
}
|
|
1846
|
+
function buildHermesMessageEvents(input) {
|
|
1847
|
+
const events = [];
|
|
1848
|
+
for (const message of input.session.messages) {
|
|
1849
|
+
const createdAt = new Date((message.timestamp || input.session.lastActive) * 1000).toISOString();
|
|
1850
|
+
const baseId = `${input.projectRoot}::hermes::${input.session.id}::${message.id}`;
|
|
1851
|
+
if (message.role === "user") {
|
|
1852
|
+
const detail = textFromContent(message.content);
|
|
1853
|
+
if (!detail) {
|
|
1854
|
+
continue;
|
|
1855
|
+
}
|
|
1856
|
+
events.push({
|
|
1857
|
+
id: `${baseId}::user`,
|
|
1858
|
+
source: "hermes",
|
|
1859
|
+
confidence: "inferred",
|
|
1860
|
+
threadId: input.session.id,
|
|
1861
|
+
createdAt,
|
|
1862
|
+
method: "hermes/userMessage",
|
|
1863
|
+
kind: "message",
|
|
1864
|
+
phase: "updated",
|
|
1865
|
+
title: "Hermes prompt",
|
|
1866
|
+
detail: shorten(detail, 240),
|
|
1867
|
+
path: input.projectRoot,
|
|
1868
|
+
action: "said",
|
|
1869
|
+
isImage: false
|
|
1870
|
+
});
|
|
1871
|
+
continue;
|
|
1872
|
+
}
|
|
1873
|
+
if (message.role === "assistant") {
|
|
1874
|
+
const calls = parseToolCalls(message.toolCalls);
|
|
1875
|
+
calls.forEach((call, index) => {
|
|
1876
|
+
const kind = toolKind(call.name);
|
|
1877
|
+
const path = toolPath(input.projectRoot, input.cwd, call.args);
|
|
1878
|
+
events.push({
|
|
1879
|
+
id: `${baseId}::tool-${index}`,
|
|
1880
|
+
source: "hermes",
|
|
1881
|
+
confidence: "inferred",
|
|
1882
|
+
threadId: input.session.id,
|
|
1883
|
+
createdAt,
|
|
1884
|
+
method: "item/started",
|
|
1885
|
+
kind: kind.eventKind,
|
|
1886
|
+
phase: "started",
|
|
1887
|
+
title: call.name,
|
|
1888
|
+
detail: toolTitle(call.name, call.args),
|
|
1889
|
+
path,
|
|
1890
|
+
command: typeof call.args.command === "string" ? call.args.command : undefined,
|
|
1891
|
+
action: kind.eventKind === "command" ? "ran" : "updated",
|
|
1892
|
+
isImage: false
|
|
1893
|
+
});
|
|
1894
|
+
});
|
|
1895
|
+
const detail = textFromContent(message.content);
|
|
1896
|
+
if (detail) {
|
|
1897
|
+
events.push({
|
|
1898
|
+
id: `${baseId}::assistant`,
|
|
1899
|
+
source: "hermes",
|
|
1900
|
+
confidence: "inferred",
|
|
1901
|
+
threadId: input.session.id,
|
|
1902
|
+
createdAt,
|
|
1903
|
+
method: "hermes/agentMessage",
|
|
1904
|
+
kind: "message",
|
|
1905
|
+
phase: "updated",
|
|
1906
|
+
title: "Hermes reply",
|
|
1907
|
+
detail: shorten(detail, 240),
|
|
1908
|
+
path: input.projectRoot,
|
|
1909
|
+
action: "said",
|
|
1910
|
+
isImage: false
|
|
1911
|
+
});
|
|
1912
|
+
}
|
|
1913
|
+
continue;
|
|
1914
|
+
}
|
|
1915
|
+
if (message.role === "tool" && message.toolName) {
|
|
1916
|
+
const kind = toolKind(message.toolName);
|
|
1917
|
+
const failed = messageLooksFailed(message);
|
|
1918
|
+
const detail = textFromContent(message.content);
|
|
1919
|
+
events.push({
|
|
1920
|
+
id: `${baseId}::tool-result`,
|
|
1921
|
+
source: "hermes",
|
|
1922
|
+
confidence: "inferred",
|
|
1923
|
+
threadId: input.session.id,
|
|
1924
|
+
createdAt,
|
|
1925
|
+
method: "item/completed",
|
|
1926
|
+
kind: kind.eventKind,
|
|
1927
|
+
phase: failed ? "failed" : "completed",
|
|
1928
|
+
title: message.toolName,
|
|
1929
|
+
detail: shorten(detail || message.toolName, 240),
|
|
1930
|
+
path: input.projectRoot,
|
|
1931
|
+
action: kind.eventKind === "command" ? "ran" : "updated",
|
|
1932
|
+
isImage: false
|
|
1933
|
+
});
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
return events;
|
|
1937
|
+
}
|
|
1938
|
+
async function gitInfoForProject(projectRoot) {
|
|
1939
|
+
void projectRoot;
|
|
1940
|
+
return {
|
|
1941
|
+
sha: null,
|
|
1942
|
+
branch: null,
|
|
1943
|
+
originUrl: null
|
|
1944
|
+
};
|
|
1945
|
+
}
|
|
1946
|
+
async function allHermesHomes(processes) {
|
|
1947
|
+
const homes = new Set(await hermesHomeCandidatesFromDisk());
|
|
1948
|
+
for (const processInfo of processes) {
|
|
1949
|
+
if (processInfo.hermesHome) {
|
|
1950
|
+
homes.add(processInfo.hermesHome);
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
return [...homes].filter(Boolean);
|
|
1954
|
+
}
|
|
1955
|
+
function latestActiveSessionForHome(sessions, home) {
|
|
1956
|
+
if (!home) {
|
|
1957
|
+
return null;
|
|
1958
|
+
}
|
|
1959
|
+
return sessions
|
|
1960
|
+
.filter((session) => session.home === home
|
|
1961
|
+
&& !session.endedAt
|
|
1962
|
+
&& isCliLikeHermesSource(session))
|
|
1963
|
+
.sort((left, right) => right.lastActive - left.lastActive)[0] ?? null;
|
|
1964
|
+
}
|
|
1965
|
+
async function loadHermesProjectSnapshotData(projectRoot, limit = hermesLocalSessionLimit()) {
|
|
1966
|
+
const canonicalRoot = (0, project_paths_1.canonicalizeProjectPath)(projectRoot);
|
|
1967
|
+
if (!canonicalRoot) {
|
|
1968
|
+
return { agents: [], events: [], notes: [] };
|
|
1969
|
+
}
|
|
1970
|
+
const notes = [];
|
|
1971
|
+
const processes = await scanHermesProcesses();
|
|
1972
|
+
const projectProcesses = processes.filter((processInfo) => pathWithinProject(canonicalRoot, processInfo.cwd));
|
|
1973
|
+
const scanLimit = hermesScanLimit(limit);
|
|
1974
|
+
const hookSessions = await loadHermesHookSessions(scanLimit);
|
|
1975
|
+
const homes = await allHermesHomes(processes);
|
|
1976
|
+
const sessionGroups = await Promise.all(homes.map((home) => loadHermesSessions(home, scanLimit, notes)));
|
|
1977
|
+
const sessions = sessionGroups.flat().sort((left, right) => right.lastActive - left.lastActive);
|
|
1978
|
+
const activeSessionIds = new Set();
|
|
1979
|
+
for (const processInfo of projectProcesses) {
|
|
1980
|
+
const activeSession = latestActiveSessionForHome(sessions, processInfo.hermesHome);
|
|
1981
|
+
if (activeSession) {
|
|
1982
|
+
activeSessionIds.add(activeSession.id);
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
const now = Date.now();
|
|
1986
|
+
const git = await gitInfoForProject(canonicalRoot);
|
|
1987
|
+
const agents = [];
|
|
1988
|
+
const events = [];
|
|
1989
|
+
const includedSessionIds = new Set();
|
|
1990
|
+
const hookRecordsBySessionId = new Map();
|
|
1991
|
+
for (const records of hookSessions.values()) {
|
|
1992
|
+
const currentRoot = await currentProjectRootForHermesHookSession(records);
|
|
1993
|
+
if (!currentRoot || !(0, project_paths_1.sameProjectPath)(currentRoot, canonicalRoot)) {
|
|
1994
|
+
continue;
|
|
1995
|
+
}
|
|
1996
|
+
const canonicalSessionId = canonicalHermesSessionIdForHookRecords(records, sessions);
|
|
1997
|
+
const latest = records[records.length - 1];
|
|
1998
|
+
const hookOnlySessionId = latest && isDurableHermesHookSessionId(latest.sessionId)
|
|
1999
|
+
? latest.sessionId
|
|
2000
|
+
: null;
|
|
2001
|
+
const visibleSessionId = canonicalSessionId ?? hookOnlySessionId;
|
|
2002
|
+
if (!visibleSessionId) {
|
|
2003
|
+
continue;
|
|
2004
|
+
}
|
|
2005
|
+
const existing = hookRecordsBySessionId.get(visibleSessionId) ?? [];
|
|
2006
|
+
hookRecordsBySessionId.set(visibleSessionId, [...existing, ...records].sort((left, right) => left.timestampMs - right.timestampMs));
|
|
2007
|
+
}
|
|
2008
|
+
for (const session of sessions) {
|
|
2009
|
+
if (includedSessionIds.has(session.id)) {
|
|
2010
|
+
continue;
|
|
2011
|
+
}
|
|
2012
|
+
const activeProcess = projectProcesses.find((processInfo) => processInfo.hermesHome === session.home && activeSessionIds.has(session.id)) ?? null;
|
|
2013
|
+
const sessionCwd = cwdFromSystemPrompt(session.systemPrompt);
|
|
2014
|
+
const fallbackCwd = activeProcess?.cwd ?? sessionCwd;
|
|
2015
|
+
const currentRoot = await currentProjectRootForHermesSession(session, activeProcess);
|
|
2016
|
+
if (!currentRoot || !(0, project_paths_1.sameProjectPath)(currentRoot, canonicalRoot)) {
|
|
2017
|
+
continue;
|
|
2018
|
+
}
|
|
2019
|
+
const candidatePaths = collectSessionPathCandidates(session, fallbackCwd);
|
|
2020
|
+
const projectPaths = candidatePaths.filter((candidate) => pathWithinProject(canonicalRoot, candidate));
|
|
2021
|
+
const cwd = activeProcess?.cwd ?? (pathWithinProject(canonicalRoot, sessionCwd) ? sessionCwd : null) ?? currentRoot;
|
|
2022
|
+
const paths = projectPaths.length > 0 ? projectPaths : [cwd];
|
|
2023
|
+
const summary = summarizeHermesSession({
|
|
2024
|
+
session,
|
|
2025
|
+
projectRoot: canonicalRoot,
|
|
2026
|
+
cwd,
|
|
2027
|
+
activeProcess,
|
|
2028
|
+
paths,
|
|
2029
|
+
now
|
|
2030
|
+
});
|
|
2031
|
+
const hookRecords = hookRecordsBySessionId.get(session.id) ?? [];
|
|
2032
|
+
const hookPaths = collectHermesHookPathCandidates(hookRecords).filter((candidate) => pathWithinProject(canonicalRoot, candidate));
|
|
2033
|
+
const hookSummary = hookRecords.length > 0
|
|
2034
|
+
? summarizeHermesHookSession({
|
|
2035
|
+
records: hookRecords,
|
|
2036
|
+
projectRoot: canonicalRoot,
|
|
2037
|
+
paths: hookPaths.length > 0 ? hookPaths : paths,
|
|
2038
|
+
now
|
|
2039
|
+
})
|
|
2040
|
+
: null;
|
|
2041
|
+
const visibleSummary = hookSummary && hookSummary.updatedAtMs >= summary.updatedAtMs
|
|
2042
|
+
? {
|
|
2043
|
+
...hookSummary,
|
|
2044
|
+
latestMessage: hookSummary.latestMessage ?? summary.latestMessage,
|
|
2045
|
+
paths: [...new Set([...hookSummary.paths, ...summary.paths])].slice(0, HERMES_HOOK_PATH_LIMIT)
|
|
2046
|
+
}
|
|
2047
|
+
: summary;
|
|
2048
|
+
if (visibleSummary.state === "idle" && !visibleSummary.isOngoing) {
|
|
2049
|
+
continue;
|
|
2050
|
+
}
|
|
2051
|
+
const appearance = await (0, appearance_1.ensureAgentAppearance)(canonicalRoot, `hermes:${session.id}`);
|
|
2052
|
+
const isContinuation = isHermesCompressionContinuation(session);
|
|
2053
|
+
agents.push({
|
|
2054
|
+
id: `hermes:${session.id}`,
|
|
2055
|
+
label: sessionLabel(session, canonicalRoot),
|
|
2056
|
+
source: "hermes",
|
|
2057
|
+
sourceKind: sessionSourceKind(session),
|
|
2058
|
+
parentThreadId: !isContinuation && session.parentSessionId ? `hermes:${session.parentSessionId}` : null,
|
|
2059
|
+
depth: !isContinuation && session.parentSessionId ? 1 : 0,
|
|
2060
|
+
isCurrent: visibleSummary.isOngoing,
|
|
2061
|
+
isOngoing: visibleSummary.isOngoing,
|
|
2062
|
+
statusText: sessionStatusText(session, visibleSummary.isOngoing),
|
|
2063
|
+
role: sessionRole(session),
|
|
2064
|
+
nickname: null,
|
|
2065
|
+
isSubagent: Boolean(!isContinuation && session.parentSessionId),
|
|
2066
|
+
state: visibleSummary.state,
|
|
2067
|
+
detail: visibleSummary.detail,
|
|
2068
|
+
cwd,
|
|
2069
|
+
roomId: null,
|
|
2070
|
+
appearance,
|
|
2071
|
+
updatedAt: new Date(visibleSummary.updatedAtMs).toISOString(),
|
|
2072
|
+
stoppedAt: visibleSummary.stoppedAtMs ? new Date(visibleSummary.stoppedAtMs).toISOString() : null,
|
|
2073
|
+
paths: visibleSummary.paths,
|
|
2074
|
+
activityEvent: visibleSummary.activityEvent,
|
|
2075
|
+
latestMessage: visibleSummary.latestMessage,
|
|
2076
|
+
threadId: session.id,
|
|
2077
|
+
taskId: null,
|
|
2078
|
+
resumeCommand: null,
|
|
2079
|
+
url: null,
|
|
2080
|
+
git,
|
|
2081
|
+
provenance: "hermes",
|
|
2082
|
+
confidence: hookSummary ? "typed" : "inferred",
|
|
2083
|
+
needsUser: null,
|
|
2084
|
+
liveSubscription: "readOnly",
|
|
2085
|
+
network: null
|
|
2086
|
+
});
|
|
2087
|
+
includedSessionIds.add(session.id);
|
|
2088
|
+
events.push(...buildHermesMessageEvents({
|
|
2089
|
+
session,
|
|
2090
|
+
projectRoot: canonicalRoot,
|
|
2091
|
+
cwd
|
|
2092
|
+
}));
|
|
2093
|
+
if (hookRecords.length > 0) {
|
|
2094
|
+
events.push(...buildHermesHookEvents({ records: hookRecords, projectRoot: canonicalRoot, threadId: session.id }));
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
for (const [sessionId, hookRecords] of hookRecordsBySessionId) {
|
|
2098
|
+
if (includedSessionIds.has(sessionId) || !isDurableHermesHookSessionId(sessionId)) {
|
|
2099
|
+
continue;
|
|
2100
|
+
}
|
|
2101
|
+
const hookPaths = collectHermesHookPathCandidates(hookRecords).filter((candidate) => pathWithinProject(canonicalRoot, candidate));
|
|
2102
|
+
const latest = hookRecords[hookRecords.length - 1];
|
|
2103
|
+
const cwd = pathWithinProject(canonicalRoot, latest?.cwd) ? latest?.cwd ?? canonicalRoot : canonicalRoot;
|
|
2104
|
+
const paths = hookPaths.length > 0 ? hookPaths : [cwd];
|
|
2105
|
+
const summary = summarizeHermesHookSession({
|
|
2106
|
+
records: hookRecords,
|
|
2107
|
+
projectRoot: canonicalRoot,
|
|
2108
|
+
paths,
|
|
2109
|
+
now
|
|
2110
|
+
});
|
|
2111
|
+
if (summary.state === "idle" && !summary.isOngoing) {
|
|
2112
|
+
continue;
|
|
2113
|
+
}
|
|
2114
|
+
const appearance = await (0, appearance_1.ensureAgentAppearance)(canonicalRoot, `hermes:${sessionId}`);
|
|
2115
|
+
agents.push({
|
|
2116
|
+
id: `hermes:${sessionId}`,
|
|
2117
|
+
label: hermesHookSessionLabel(hookRecords, canonicalRoot, summary.latestMessage),
|
|
2118
|
+
source: "hermes",
|
|
2119
|
+
sourceKind: hermesHookSourceKind(sessionId),
|
|
2120
|
+
parentThreadId: null,
|
|
2121
|
+
depth: 0,
|
|
2122
|
+
isCurrent: summary.isOngoing,
|
|
2123
|
+
isOngoing: summary.isOngoing,
|
|
2124
|
+
statusText: hermesHookStatusText(sessionId, summary.isOngoing),
|
|
2125
|
+
role: hermesHookRole(sessionId),
|
|
2126
|
+
nickname: null,
|
|
2127
|
+
isSubagent: false,
|
|
2128
|
+
state: summary.state,
|
|
2129
|
+
detail: summary.detail,
|
|
2130
|
+
cwd,
|
|
2131
|
+
roomId: null,
|
|
2132
|
+
appearance,
|
|
2133
|
+
updatedAt: new Date(summary.updatedAtMs).toISOString(),
|
|
2134
|
+
stoppedAt: summary.stoppedAtMs ? new Date(summary.stoppedAtMs).toISOString() : null,
|
|
2135
|
+
paths: summary.paths,
|
|
2136
|
+
activityEvent: summary.activityEvent,
|
|
2137
|
+
latestMessage: summary.latestMessage,
|
|
2138
|
+
threadId: sessionId,
|
|
2139
|
+
taskId: null,
|
|
2140
|
+
resumeCommand: null,
|
|
2141
|
+
url: null,
|
|
2142
|
+
git,
|
|
2143
|
+
provenance: "hermes",
|
|
2144
|
+
confidence: "typed",
|
|
2145
|
+
needsUser: null,
|
|
2146
|
+
liveSubscription: "readOnly",
|
|
2147
|
+
network: null
|
|
2148
|
+
});
|
|
2149
|
+
includedSessionIds.add(sessionId);
|
|
2150
|
+
events.push(...buildHermesHookEvents({ records: hookRecords, projectRoot: canonicalRoot, threadId: sessionId }));
|
|
2151
|
+
}
|
|
2152
|
+
for (const processInfo of projectProcesses) {
|
|
2153
|
+
const hasIncludedSession = sessions.some((session) => includedSessionIds.has(session.id)
|
|
2154
|
+
&& session.home === processInfo.hermesHome
|
|
2155
|
+
&& activeSessionIds.has(session.id));
|
|
2156
|
+
if (hasIncludedSession || !processInfo.cwd) {
|
|
2157
|
+
continue;
|
|
2158
|
+
}
|
|
2159
|
+
const appearance = await (0, appearance_1.ensureAgentAppearance)(canonicalRoot, `hermes-process:${processInfo.pid}`);
|
|
2160
|
+
agents.push({
|
|
2161
|
+
id: `hermes-process:${processInfo.pid}`,
|
|
2162
|
+
label: "Hermes CLI",
|
|
2163
|
+
source: "hermes",
|
|
2164
|
+
sourceKind: "hermes:process",
|
|
2165
|
+
parentThreadId: null,
|
|
2166
|
+
depth: 0,
|
|
2167
|
+
isCurrent: true,
|
|
2168
|
+
isOngoing: true,
|
|
2169
|
+
statusText: "active",
|
|
2170
|
+
role: "hermes",
|
|
2171
|
+
nickname: null,
|
|
2172
|
+
isSubagent: false,
|
|
2173
|
+
state: "planning",
|
|
2174
|
+
detail: "Hermes process in workspace",
|
|
2175
|
+
cwd: processInfo.cwd,
|
|
2176
|
+
roomId: null,
|
|
2177
|
+
appearance,
|
|
2178
|
+
updatedAt: new Date(processInfo.updatedAtMs).toISOString(),
|
|
2179
|
+
stoppedAt: null,
|
|
2180
|
+
paths: [processInfo.cwd],
|
|
2181
|
+
activityEvent: null,
|
|
2182
|
+
latestMessage: null,
|
|
2183
|
+
threadId: `process:${processInfo.pid}`,
|
|
2184
|
+
taskId: null,
|
|
2185
|
+
resumeCommand: null,
|
|
2186
|
+
url: null,
|
|
2187
|
+
git,
|
|
2188
|
+
provenance: "hermes",
|
|
2189
|
+
confidence: "inferred",
|
|
2190
|
+
needsUser: null,
|
|
2191
|
+
liveSubscription: "readOnly",
|
|
2192
|
+
network: null
|
|
2193
|
+
});
|
|
2194
|
+
}
|
|
2195
|
+
return {
|
|
2196
|
+
agents: agents
|
|
2197
|
+
.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt))
|
|
2198
|
+
.slice(0, limit),
|
|
2199
|
+
events: events.sort((left, right) => right.createdAt.localeCompare(left.createdAt)),
|
|
2200
|
+
notes
|
|
2201
|
+
};
|
|
2202
|
+
}
|
|
2203
|
+
async function discoverHermesProjects(limit = 20) {
|
|
2204
|
+
const projects = new Map();
|
|
2205
|
+
const processes = await scanHermesProcesses();
|
|
2206
|
+
const scanLimit = hermesScanLimit(limit);
|
|
2207
|
+
const hookSessions = await loadHermesHookSessions(scanLimit);
|
|
2208
|
+
const now = Date.now();
|
|
2209
|
+
const notes = [];
|
|
2210
|
+
const upsertProject = async (root, updatedAtSeconds) => {
|
|
2211
|
+
const key = (0, project_paths_1.projectPathIdentityKey)(root);
|
|
2212
|
+
if (!root || !key) {
|
|
2213
|
+
return;
|
|
2214
|
+
}
|
|
2215
|
+
if (HERMES_TRANSIENT_PROJECT_ROOTS.has(root.replace(/\\/g, "/").toLowerCase())) {
|
|
2216
|
+
return;
|
|
2217
|
+
}
|
|
2218
|
+
if (await looksLikeHermesRuntimeProjectRoot(root)) {
|
|
2219
|
+
return;
|
|
2220
|
+
}
|
|
2221
|
+
if (!await pathExists((0, node_path_1.join)(root, ".git"))) {
|
|
2222
|
+
return;
|
|
2223
|
+
}
|
|
2224
|
+
const existing = projects.get(key);
|
|
2225
|
+
if (existing) {
|
|
2226
|
+
existing.updatedAt = Math.max(existing.updatedAt, updatedAtSeconds);
|
|
2227
|
+
existing.count += 1;
|
|
2228
|
+
return;
|
|
2229
|
+
}
|
|
2230
|
+
projects.set(key, {
|
|
2231
|
+
root,
|
|
2232
|
+
label: (0, project_paths_1.projectLabelFromRoot)(root),
|
|
2233
|
+
updatedAt: updatedAtSeconds,
|
|
2234
|
+
count: 1
|
|
2235
|
+
});
|
|
2236
|
+
};
|
|
2237
|
+
for (const processInfo of processes) {
|
|
2238
|
+
if (!processInfo.cwd) {
|
|
2239
|
+
continue;
|
|
2240
|
+
}
|
|
2241
|
+
const root = await resolveProjectRootForPath(processInfo.cwd);
|
|
2242
|
+
await upsertProject(root, Math.floor(processInfo.updatedAtMs / 1000));
|
|
2243
|
+
}
|
|
2244
|
+
for (const records of hookSessions.values()) {
|
|
2245
|
+
const latest = records[records.length - 1];
|
|
2246
|
+
if (!latest || now - latest.timestampMs > HERMES_PROJECT_DISCOVERY_HOOK_WINDOW_MS) {
|
|
2247
|
+
continue;
|
|
2248
|
+
}
|
|
2249
|
+
const summary = summarizeHermesHookSession({
|
|
2250
|
+
records,
|
|
2251
|
+
projectRoot: latest.cwd ?? latest.processCwd ?? "/",
|
|
2252
|
+
paths: collectHermesHookPathCandidates(records),
|
|
2253
|
+
now
|
|
2254
|
+
});
|
|
2255
|
+
if (summary.state === "idle" && !summary.isOngoing) {
|
|
2256
|
+
continue;
|
|
2257
|
+
}
|
|
2258
|
+
const currentRoot = await currentProjectRootForHermesHookSession(records);
|
|
2259
|
+
await upsertProject(currentRoot, Math.floor(latest.timestampMs / 1000));
|
|
2260
|
+
}
|
|
2261
|
+
const homes = await allHermesHomes(processes);
|
|
2262
|
+
const sessionGroups = await Promise.all(homes.map((home) => loadHermesSessions(home, scanLimit, notes)));
|
|
2263
|
+
for (const session of sessionGroups.flat()) {
|
|
2264
|
+
if (session.endedAt !== null) {
|
|
2265
|
+
continue;
|
|
2266
|
+
}
|
|
2267
|
+
const lastActiveMs = Math.max(session.lastActive * 1000, session.startedAt * 1000);
|
|
2268
|
+
if (now - lastActiveMs > HERMES_RECENT_OPEN_WINDOW_MS) {
|
|
2269
|
+
continue;
|
|
2270
|
+
}
|
|
2271
|
+
const root = await currentProjectRootForHermesSession(session, null);
|
|
2272
|
+
await upsertProject(root, Math.floor(lastActiveMs / 1000));
|
|
2273
|
+
}
|
|
2274
|
+
return [...projects.values()]
|
|
2275
|
+
.sort((left, right) => right.updatedAt - left.updatedAt)
|
|
2276
|
+
.slice(0, limit);
|
|
2277
|
+
}
|
|
2278
|
+
async function loadRoamingHermesSnapshotData(input) {
|
|
2279
|
+
const anchorRoot = (0, project_paths_1.canonicalizeProjectPath)(input.anchorProjectRoot);
|
|
2280
|
+
if (!anchorRoot) {
|
|
2281
|
+
return { agents: [], events: [], notes: [] };
|
|
2282
|
+
}
|
|
2283
|
+
const knownRoots = input.knownProjectRoots
|
|
2284
|
+
.map((root) => (0, project_paths_1.canonicalizeProjectPath)(root))
|
|
2285
|
+
.filter((root) => Boolean(root));
|
|
2286
|
+
const limit = input.limit ?? 4;
|
|
2287
|
+
const hookSessions = await loadHermesHookSessions(hermesScanLimit(limit));
|
|
2288
|
+
const now = Date.now();
|
|
2289
|
+
const gitByRoot = new Map();
|
|
2290
|
+
const gitForRoot = async (root) => {
|
|
2291
|
+
const key = (0, project_paths_1.projectPathIdentityKey)(root) ?? root;
|
|
2292
|
+
const existing = gitByRoot.get(key);
|
|
2293
|
+
if (existing) {
|
|
2294
|
+
return existing;
|
|
2295
|
+
}
|
|
2296
|
+
const git = await gitInfoForProject(root);
|
|
2297
|
+
gitByRoot.set(key, git);
|
|
2298
|
+
return git;
|
|
2299
|
+
};
|
|
2300
|
+
const agents = [];
|
|
2301
|
+
const events = [];
|
|
2302
|
+
for (const [sessionId, records] of hookSessions.entries()) {
|
|
2303
|
+
if (isNonSessionHermesHookId(sessionId)) {
|
|
2304
|
+
continue;
|
|
2305
|
+
}
|
|
2306
|
+
const currentRoot = await currentProjectRootForHermesHookSession(records);
|
|
2307
|
+
const candidatePaths = collectHermesHookPathCandidates(records);
|
|
2308
|
+
const isInsideKnownWorkspace = Boolean(currentRoot
|
|
2309
|
+
&& knownRoots.some((root) => (0, project_paths_1.sameProjectPath)(root, currentRoot) || pathWithinProject(root, currentRoot)));
|
|
2310
|
+
if (isInsideKnownWorkspace) {
|
|
2311
|
+
continue;
|
|
2312
|
+
}
|
|
2313
|
+
const sourceRoot = currentRoot ?? anchorRoot;
|
|
2314
|
+
const summary = summarizeHermesHookSession({
|
|
2315
|
+
records,
|
|
2316
|
+
projectRoot: sourceRoot,
|
|
2317
|
+
paths: candidatePaths,
|
|
2318
|
+
now
|
|
2319
|
+
});
|
|
2320
|
+
if (summary.state === "idle" && !summary.isOngoing) {
|
|
2321
|
+
continue;
|
|
2322
|
+
}
|
|
2323
|
+
const latest = records[records.length - 1];
|
|
2324
|
+
const appearance = await (0, appearance_1.ensureAgentAppearance)(sourceRoot, `hermes:${sessionId}`);
|
|
2325
|
+
const statusText = isHermesCronSessionId(sessionId)
|
|
2326
|
+
? hermesHookStatusText(sessionId, summary.isOngoing)
|
|
2327
|
+
: summary.isOngoing ? "roaming" : summary.state;
|
|
2328
|
+
agents.push({
|
|
2329
|
+
id: `hermes:${sessionId}`,
|
|
2330
|
+
label: hermesHookSessionLabel(records, sourceRoot, summary.latestMessage),
|
|
2331
|
+
source: "hermes",
|
|
2332
|
+
sourceKind: "hermes:roaming",
|
|
2333
|
+
parentThreadId: null,
|
|
2334
|
+
depth: 0,
|
|
2335
|
+
isCurrent: false,
|
|
2336
|
+
isOngoing: summary.isOngoing,
|
|
2337
|
+
statusText,
|
|
2338
|
+
role: hermesHookRole(sessionId),
|
|
2339
|
+
nickname: null,
|
|
2340
|
+
isSubagent: false,
|
|
2341
|
+
state: summary.state,
|
|
2342
|
+
detail: summary.detail,
|
|
2343
|
+
cwd: latest.cwd ?? latest.processCwd ?? null,
|
|
2344
|
+
sourceProjectRoot: sourceRoot,
|
|
2345
|
+
roomId: null,
|
|
2346
|
+
appearance,
|
|
2347
|
+
updatedAt: new Date(summary.updatedAtMs).toISOString(),
|
|
2348
|
+
stoppedAt: summary.stoppedAtMs ? new Date(summary.stoppedAtMs).toISOString() : null,
|
|
2349
|
+
paths: [],
|
|
2350
|
+
activityEvent: summary.activityEvent,
|
|
2351
|
+
latestMessage: summary.latestMessage,
|
|
2352
|
+
threadId: sessionId,
|
|
2353
|
+
taskId: null,
|
|
2354
|
+
resumeCommand: null,
|
|
2355
|
+
url: null,
|
|
2356
|
+
git: await gitForRoot(sourceRoot),
|
|
2357
|
+
provenance: "hermes",
|
|
2358
|
+
confidence: "typed",
|
|
2359
|
+
needsUser: null,
|
|
2360
|
+
liveSubscription: "readOnly",
|
|
2361
|
+
network: null
|
|
2362
|
+
});
|
|
2363
|
+
events.push(...buildHermesHookEvents({ records, projectRoot: sourceRoot }));
|
|
2364
|
+
}
|
|
2365
|
+
return {
|
|
2366
|
+
agents: agents
|
|
2367
|
+
.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt))
|
|
2368
|
+
.slice(0, limit),
|
|
2369
|
+
events: events.sort((left, right) => right.createdAt.localeCompare(left.createdAt)),
|
|
2370
|
+
notes: []
|
|
2371
|
+
};
|
|
2372
|
+
}
|
|
2373
|
+
function summarizeHermesSessionForTest(input) {
|
|
2374
|
+
return summarizeHermesSession({
|
|
2375
|
+
session: input.session,
|
|
2376
|
+
projectRoot: input.projectRoot,
|
|
2377
|
+
cwd: input.cwd ?? input.projectRoot,
|
|
2378
|
+
activeProcess: input.active
|
|
2379
|
+
? {
|
|
2380
|
+
pid: 1,
|
|
2381
|
+
cwd: input.cwd ?? input.projectRoot,
|
|
2382
|
+
hermesHome: input.session.home,
|
|
2383
|
+
command: "hermes",
|
|
2384
|
+
updatedAtMs: input.now ?? Date.now()
|
|
2385
|
+
}
|
|
2386
|
+
: null,
|
|
2387
|
+
paths: input.paths ?? [input.projectRoot],
|
|
2388
|
+
now: input.now ?? Date.now()
|
|
2389
|
+
});
|
|
2390
|
+
}
|
|
2391
|
+
function summarizeHermesHookSessionForTest(input) {
|
|
2392
|
+
const now = input.now ?? Date.now();
|
|
2393
|
+
return summarizeHermesHookSession({
|
|
2394
|
+
records: input.records.map((record, index) => ({
|
|
2395
|
+
sessionId: record.sessionId ?? "test-session",
|
|
2396
|
+
eventName: record.eventName,
|
|
2397
|
+
timestampMs: record.timestampMs ?? now + index,
|
|
2398
|
+
cwd: record.cwd ?? input.projectRoot,
|
|
2399
|
+
processCwd: record.processCwd ?? input.projectRoot,
|
|
2400
|
+
payload: record.payload,
|
|
2401
|
+
raw: record.raw ?? {}
|
|
2402
|
+
})),
|
|
2403
|
+
projectRoot: input.projectRoot,
|
|
2404
|
+
paths: input.paths ?? [input.projectRoot],
|
|
2405
|
+
now
|
|
2406
|
+
});
|
|
2407
|
+
}
|
|
2408
|
+
function isHermesGatewayDaemonForTest(argv) {
|
|
2409
|
+
return looksLikeHermesGatewayDaemon(argv);
|
|
2410
|
+
}
|
|
2411
|
+
function isHermesRuntimeHomeProcessForTest(argv, cwd) {
|
|
2412
|
+
return looksLikeHermesRuntimeHomeProcess(argv, cwd);
|
|
2413
|
+
}
|
|
2414
|
+
function isHermesCompressionContinuationForTest(input) {
|
|
2415
|
+
return isHermesCompressionContinuation({
|
|
2416
|
+
id: "test-session",
|
|
2417
|
+
source: "cli",
|
|
2418
|
+
model: null,
|
|
2419
|
+
parentSessionId: input.parentSessionId ?? null,
|
|
2420
|
+
parentEndedAt: input.parentEndedAt ?? null,
|
|
2421
|
+
parentEndReason: input.parentEndReason ?? null,
|
|
2422
|
+
startedAt: input.startedAt ?? 0,
|
|
2423
|
+
endedAt: null,
|
|
2424
|
+
endReason: null,
|
|
2425
|
+
messageCount: 0,
|
|
2426
|
+
toolCallCount: 0,
|
|
2427
|
+
title: null,
|
|
2428
|
+
systemPrompt: null,
|
|
2429
|
+
lastActive: input.startedAt ?? 0,
|
|
2430
|
+
home: "/tmp/hermes",
|
|
2431
|
+
storage: "sqlite",
|
|
2432
|
+
messages: []
|
|
2433
|
+
});
|
|
2434
|
+
}
|
|
2435
|
+
async function currentHermesSessionProjectRootForTest(input) {
|
|
2436
|
+
return currentProjectRootForHermesSession(input.session, input.activeCwd
|
|
2437
|
+
? {
|
|
2438
|
+
pid: 1,
|
|
2439
|
+
cwd: input.activeCwd,
|
|
2440
|
+
hermesHome: input.session.home,
|
|
2441
|
+
command: "hermes",
|
|
2442
|
+
updatedAtMs: Date.now()
|
|
2443
|
+
}
|
|
2444
|
+
: null);
|
|
2445
|
+
}
|
|
2446
|
+
//# sourceMappingURL=hermes.js.map
|