@saleso.innovations/bridge 0.1.26 → 0.1.27

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.
@@ -1,5 +1,5 @@
1
1
  import { type ShellDeltaEmitter } from "./shellSession.js";
2
- export declare const HERMES_COMMAND_NAMES: readonly ["runtime.health", "runtime.detailedHealth", "runtime.version", "runtime.capabilities", "models.list", "model.set", "responses.create", "runs.create", "runs.status", "runs.stop", "jobs.list", "jobs.get", "jobs.create", "jobs.update", "jobs.pause", "jobs.resume", "jobs.runNow", "jobs.delete", "profiles.list", "profiles.create", "gateway.start", "gateway.stop", "gateway.restart", "hermes.update", "sessions.messages.list", "sessions.messages.countSent", "sessions.titles.resolve", "sessions.list", "skills.list", "files.list", "files.read", "files.write", "memories.list", "shell.exec", "shell.session.reset"];
2
+ export declare const HERMES_COMMAND_NAMES: readonly ["runtime.health", "runtime.detailedHealth", "runtime.version", "runtime.capabilities", "models.list", "model.set", "responses.create", "runs.create", "runs.status", "runs.stop", "jobs.list", "jobs.get", "jobs.create", "jobs.update", "jobs.pause", "jobs.resume", "jobs.runNow", "jobs.delete", "profiles.list", "profiles.create", "gateway.start", "gateway.stop", "gateway.restart", "hermes.update", "sessions.messages.list", "sessions.messages.countSent", "sessions.titles.resolve", "sessions.usage.get", "sessions.list", "skills.list", "files.list", "files.read", "files.write", "memories.list", "shell.exec", "shell.session.reset"];
3
3
  export type HermesCommandName = (typeof HERMES_COMMAND_NAMES)[number];
4
4
  export declare function isHermesCommandName(value: string): value is HermesCommandName;
5
5
  export type HermesCommandErrorCode = "command_unsupported" | "hermes_unreachable" | "hermes_request_failed" | "invalid_command_args" | "unsupported_by_http";
@@ -1 +1 @@
1
- {"version":3,"file":"hermesCommands.d.ts","sourceRoot":"","sources":["../src/hermesCommands.ts"],"names":[],"mappings":"AAiBA,OAAO,EAAuC,KAAK,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAGhG,eAAO,MAAM,oBAAoB,6mBAoCvB,CAAC;AAEX,MAAM,MAAM,iBAAiB,GAAG,CAAC,OAAO,oBAAoB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEtE,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,iBAAiB,CAE7E;AAED,MAAM,MAAM,sBAAsB,GAC9B,qBAAqB,GACrB,oBAAoB,GACpB,uBAAuB,GACvB,sBAAsB,GACtB,qBAAqB,CAAC;AAE1B,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,QAAQ,CAAC,IAAI,EAAE,sBAAsB,CAAC;gBAE1B,IAAI,EAAE,sBAAsB,EAAE,OAAO,EAAE,MAAM;CAI1D;AA6GD,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,iBAAiB,EAC1B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,iBAAiB,CAAA;CAAO,GAC9F,OAAO,CAAC,OAAO,CAAC,CAsNlB;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG;IAAE,IAAI,EAAE,sBAAsB,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAetG"}
1
+ {"version":3,"file":"hermesCommands.d.ts","sourceRoot":"","sources":["../src/hermesCommands.ts"],"names":[],"mappings":"AAkBA,OAAO,EAAuC,KAAK,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAGhG,eAAO,MAAM,oBAAoB,moBAqCvB,CAAC;AAEX,MAAM,MAAM,iBAAiB,GAAG,CAAC,OAAO,oBAAoB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEtE,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,iBAAiB,CAE7E;AAED,MAAM,MAAM,sBAAsB,GAC9B,qBAAqB,GACrB,oBAAoB,GACpB,uBAAuB,GACvB,sBAAsB,GACtB,qBAAqB,CAAC;AAE1B,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,QAAQ,CAAC,IAAI,EAAE,sBAAsB,CAAC;gBAE1B,IAAI,EAAE,sBAAsB,EAAE,OAAO,EAAE,MAAM;CAI1D;AA6GD,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,iBAAiB,EAC1B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,iBAAiB,CAAA;CAAO,GAC9F,OAAO,CAAC,OAAO,CAAC,CA0NlB;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG;IAAE,IAAI,EAAE,sBAAsB,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAetG"}
@@ -1,7 +1,7 @@
1
1
  import { resolveHermesApiConfig } from "./hermesForwarder.js";
2
2
  import { restartHermesGateway, startHermesGateway, stopHermesGateway } from "./gatewayControl.js";
3
3
  import { listHermesCronJobs } from "./cronList.js";
4
- import { listHermesSessions, listSessionMessages, countUserMessagesSent, resolveSessionTitles, } from "./hermesSessionDb.js";
4
+ import { listHermesSessions, listSessionMessages, countUserMessagesSent, getSessionUsage, resolveSessionTitles, } from "./hermesSessionDb.js";
5
5
  import { listHermesSkills } from "./skillsList.js";
6
6
  import { runHermesUpdate } from "./hermesUpdate.js";
7
7
  import { executeFilesList, executeFilesRead, executeFilesWrite, executeMemoriesList, } from "./hermesFileCommands.js";
@@ -35,6 +35,7 @@ export const HERMES_COMMAND_NAMES = [
35
35
  "sessions.messages.list",
36
36
  "sessions.messages.countSent",
37
37
  "sessions.titles.resolve",
38
+ "sessions.usage.get",
38
39
  "sessions.list",
39
40
  "skills.list",
40
41
  "files.list",
@@ -311,6 +312,10 @@ export async function executeHermesCommand(command, args, options = {}) {
311
312
  const sessionIds = optionalStringArray(args, "sessionIds") ?? [];
312
313
  return { titles: resolveSessionTitles(sessionIds) };
313
314
  }
315
+ case "sessions.usage.get": {
316
+ const sessionId = requireString(args, "sessionId");
317
+ return getSessionUsage(sessionId);
318
+ }
314
319
  case "sessions.list": {
315
320
  const limit = optionalNumber(args, "limit");
316
321
  return { sessions: listHermesSessions({ limit: limit ?? 200 }) };
@@ -24,9 +24,24 @@ export type HermesSessionListEntry = {
24
24
  previewText: string;
25
25
  messageCount: number;
26
26
  };
27
+ /** Hermes-injected prompts for cron runs and background skills — not user chat. */
28
+ export declare function isAutomatedHermesSessionPrompt(content: string): boolean;
27
29
  export declare function listHermesSessions(options?: {
28
30
  limit?: number;
29
31
  }): HermesSessionListEntry[];
32
+ export type HermesSessionUsage = {
33
+ sessionId: string;
34
+ resolvedSessionId: string;
35
+ model: string | null;
36
+ inputTokens: number;
37
+ outputTokens: number;
38
+ cacheReadTokens: number;
39
+ cacheWriteTokens: number;
40
+ reasoningTokens: number;
41
+ estimatedCostUsd: number | null;
42
+ messageCount: number;
43
+ };
44
+ export declare function getSessionUsage(sessionId: string): HermesSessionUsage;
30
45
  export declare function hermesStateDbExists(): boolean;
31
46
  export declare function countUserMessagesSent(): {
32
47
  count: number;
@@ -1 +1 @@
1
- {"version":3,"file":"hermesSessionDb.d.ts","sourceRoot":"","sources":["../src/hermesSessionDb.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,oBAAoB,GAAG;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAyIF,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAapE;AAED,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,CAuBxF;AAED,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,aAAa,CAAC,EAAE,MAAM,CAAA;CAAO,GACxE,oBAAoB,EAAE,CAwCxB;AAED,wBAAgB,uBAAuB,CACrC,SAAS,EAAE,MAAM,GAChB;IAAE,aAAa,CAAC,EAAE,MAAM,CAAC;IAAC,kBAAkB,CAAC,EAAE,MAAM,CAAA;CAAE,CAmCzD;AAED,MAAM,MAAM,sBAAsB,GAAG;IACnC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AAMF,wBAAgB,kBAAkB,CAAC,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,sBAAsB,EAAE,CAyD7F;AAED,wBAAgB,mBAAmB,IAAI,OAAO,CAO7C;AAED,wBAAgB,qBAAqB,IAAI;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAgBzD"}
1
+ {"version":3,"file":"hermesSessionDb.d.ts","sourceRoot":"","sources":["../src/hermesSessionDb.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,oBAAoB,GAAG;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAyIF,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAapE;AAED,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,CAuBxF;AAED,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,aAAa,CAAC,EAAE,MAAM,CAAA;CAAO,GACxE,oBAAoB,EAAE,CAwCxB;AAED,wBAAgB,uBAAuB,CACrC,SAAS,EAAE,MAAM,GAChB;IAAE,aAAa,CAAC,EAAE,MAAM,CAAC;IAAC,kBAAkB,CAAC,EAAE,MAAM,CAAA;CAAE,CAmCzD;AAED,MAAM,MAAM,sBAAsB,GAAG;IACnC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AAMF,mFAAmF;AACnF,wBAAgB,8BAA8B,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAqBvE;AAcD,wBAAgB,kBAAkB,CAAC,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,sBAAsB,EAAE,CAsE7F;AAED,MAAM,MAAM,kBAAkB,GAAG;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,gBAAgB,EAAE,MAAM,CAAC;IACzB,eAAe,EAAE,MAAM,CAAC;IACxB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AAiBF,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,kBAAkB,CAkErE;AAED,wBAAgB,mBAAmB,IAAI,OAAO,CAO7C;AAED,wBAAgB,qBAAqB,IAAI;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAgBzD"}
@@ -216,11 +216,40 @@ export function getLatestTurnMessageIds(sessionId) {
216
216
  function normalizeTimestampMs(value) {
217
217
  return value < 1_000_000_000_000 ? value * 1000 : value;
218
218
  }
219
+ /** Hermes-injected prompts for cron runs and background skills — not user chat. */
220
+ export function isAutomatedHermesSessionPrompt(content) {
221
+ const trimmed = content.trim();
222
+ if (!trimmed)
223
+ return false;
224
+ if (/\[IMPORTANT:[^\]]*scheduled cron job/i.test(trimmed)) {
225
+ return true;
226
+ }
227
+ if (/you are running as .* background skill/i.test(trimmed)) {
228
+ return true;
229
+ }
230
+ if (/^Cronjob Response:\s*.+\r?\n-+\r?\n/i.test(trimmed)) {
231
+ return true;
232
+ }
233
+ if (/^#\s*Cron Job:/im.test(trimmed) && /\bJob ID:\s*/im.test(trimmed)) {
234
+ return true;
235
+ }
236
+ return false;
237
+ }
238
+ function firstUserMessageContent(db, sessionId) {
239
+ const row = db
240
+ .prepare(`SELECT content FROM messages
241
+ WHERE session_id = ? AND role = 'user'
242
+ ORDER BY timestamp ASC, id ASC
243
+ LIMIT 1`)
244
+ .get(sessionId);
245
+ return decodeMessageContent(row?.content ?? null);
246
+ }
219
247
  export function listHermesSessions(options = {}) {
220
248
  if (!hermesStateDbExists()) {
221
249
  return [];
222
250
  }
223
251
  const limit = Math.min(Math.max(options.limit ?? 200, 1), 500);
252
+ const fetchLimit = Math.min(limit * 4, 500);
224
253
  const db = openReadOnlyDb();
225
254
  try {
226
255
  const rows = db
@@ -235,8 +264,12 @@ export function listHermesSessions(options = {}) {
235
264
  GROUP BY s.id
236
265
  ORDER BY last_message_at DESC
237
266
  LIMIT ?`)
238
- .all(limit);
239
- return rows.map((row) => {
267
+ .all(fetchLimit);
268
+ const entries = [];
269
+ for (const row of rows) {
270
+ if (isAutomatedHermesSessionPrompt(firstUserMessageContent(db, row.id))) {
271
+ continue;
272
+ }
240
273
  const previewRow = db
241
274
  .prepare(`SELECT content FROM messages
242
275
  WHERE session_id = ? AND role IN ('user', 'assistant')
@@ -245,15 +278,19 @@ export function listHermesSessions(options = {}) {
245
278
  .get(row.id);
246
279
  const resolvedTitle = sessionTitleForLookupId(db, row.id);
247
280
  const previewText = decodeMessageContent(previewRow?.content ?? null).trim().slice(0, 120);
248
- return {
281
+ entries.push({
249
282
  id: row.id,
250
283
  title: resolvedTitle ?? row.title?.trim() ?? null,
251
284
  startedAt: normalizeTimestampMs(row.started_at ?? row.last_message_at),
252
285
  lastMessageAt: normalizeTimestampMs(row.last_message_at),
253
286
  previewText,
254
287
  messageCount: row.message_count,
255
- };
256
- });
288
+ });
289
+ if (entries.length >= limit) {
290
+ break;
291
+ }
292
+ }
293
+ return entries;
257
294
  }
258
295
  catch {
259
296
  return [];
@@ -262,6 +299,73 @@ export function listHermesSessions(options = {}) {
262
299
  db.close();
263
300
  }
264
301
  }
302
+ function emptySessionUsage(sessionId, resolvedSessionId) {
303
+ return {
304
+ sessionId,
305
+ resolvedSessionId,
306
+ model: null,
307
+ inputTokens: 0,
308
+ outputTokens: 0,
309
+ cacheReadTokens: 0,
310
+ cacheWriteTokens: 0,
311
+ reasoningTokens: 0,
312
+ estimatedCostUsd: null,
313
+ messageCount: 0,
314
+ };
315
+ }
316
+ export function getSessionUsage(sessionId) {
317
+ const trimmed = sessionId.trim();
318
+ if (!trimmed) {
319
+ return emptySessionUsage(sessionId, sessionId);
320
+ }
321
+ if (!hermesStateDbExists()) {
322
+ return emptySessionUsage(trimmed, trimmed);
323
+ }
324
+ const db = openReadOnlyDb();
325
+ try {
326
+ const resolved = resolveResumeSessionId(db, trimmed);
327
+ const chain = sessionAncestorChain(db, resolved);
328
+ const placeholders = chain.map(() => "?").join(", ");
329
+ const totals = db
330
+ .prepare(`SELECT
331
+ COALESCE(SUM(input_tokens), 0) AS input_tokens,
332
+ COALESCE(SUM(output_tokens), 0) AS output_tokens,
333
+ COALESCE(SUM(cache_read_tokens), 0) AS cache_read_tokens,
334
+ COALESCE(SUM(cache_write_tokens), 0) AS cache_write_tokens,
335
+ COALESCE(SUM(reasoning_tokens), 0) AS reasoning_tokens,
336
+ COALESCE(SUM(estimated_cost_usd), 0) AS estimated_cost_usd,
337
+ COALESCE(SUM(message_count), 0) AS message_count
338
+ FROM sessions
339
+ WHERE id IN (${placeholders})`)
340
+ .get(...chain);
341
+ const modelRow = db
342
+ .prepare("SELECT model FROM sessions WHERE id = ?")
343
+ .get(resolved);
344
+ const model = modelRow?.model?.trim() || null;
345
+ const estimatedCost = totals?.estimated_cost_usd;
346
+ const estimatedCostUsd = estimatedCost != null && Number.isFinite(estimatedCost) && estimatedCost > 0
347
+ ? estimatedCost
348
+ : null;
349
+ return {
350
+ sessionId: trimmed,
351
+ resolvedSessionId: resolved,
352
+ model,
353
+ inputTokens: totals?.input_tokens ?? 0,
354
+ outputTokens: totals?.output_tokens ?? 0,
355
+ cacheReadTokens: totals?.cache_read_tokens ?? 0,
356
+ cacheWriteTokens: totals?.cache_write_tokens ?? 0,
357
+ reasoningTokens: totals?.reasoning_tokens ?? 0,
358
+ estimatedCostUsd,
359
+ messageCount: totals?.message_count ?? 0,
360
+ };
361
+ }
362
+ catch {
363
+ return emptySessionUsage(trimmed, trimmed);
364
+ }
365
+ finally {
366
+ db.close();
367
+ }
368
+ }
265
369
  export function hermesStateDbExists() {
266
370
  try {
267
371
  readFileSync(hermesStateDbPath());
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=hermesSessionDb.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hermesSessionDb.test.d.ts","sourceRoot":"","sources":["../src/hermesSessionDb.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,151 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdirSync, mkdtempSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import Database from "better-sqlite3";
6
+ import test from "node:test";
7
+ import { getSessionUsage, isAutomatedHermesSessionPrompt } from "./hermesSessionDb.js";
8
+ function withHermesStateDb(setup, run) {
9
+ const home = mkdtempSync(join(tmpdir(), "hermes-session-db-test-"));
10
+ const previousHome = process.env.HERMES_HOME;
11
+ process.env.HERMES_HOME = home;
12
+ try {
13
+ mkdirSync(home, { recursive: true });
14
+ const db = new Database(join(home, "state.db"));
15
+ db.exec(`
16
+ CREATE TABLE sessions (
17
+ id TEXT PRIMARY KEY,
18
+ source TEXT NOT NULL,
19
+ user_id TEXT,
20
+ model TEXT,
21
+ model_config TEXT,
22
+ system_prompt TEXT,
23
+ parent_session_id TEXT,
24
+ started_at REAL NOT NULL,
25
+ ended_at REAL,
26
+ end_reason TEXT,
27
+ message_count INTEGER DEFAULT 0,
28
+ tool_call_count INTEGER DEFAULT 0,
29
+ input_tokens INTEGER DEFAULT 0,
30
+ output_tokens INTEGER DEFAULT 0,
31
+ cache_read_tokens INTEGER DEFAULT 0,
32
+ cache_write_tokens INTEGER DEFAULT 0,
33
+ reasoning_tokens INTEGER DEFAULT 0,
34
+ estimated_cost_usd REAL,
35
+ FOREIGN KEY (parent_session_id) REFERENCES sessions(id)
36
+ );
37
+ CREATE TABLE messages (
38
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
39
+ session_id TEXT NOT NULL REFERENCES sessions(id),
40
+ role TEXT NOT NULL,
41
+ content TEXT,
42
+ timestamp REAL NOT NULL
43
+ );
44
+ `);
45
+ setup(db);
46
+ db.close();
47
+ run();
48
+ }
49
+ finally {
50
+ if (previousHome === undefined) {
51
+ delete process.env.HERMES_HOME;
52
+ }
53
+ else {
54
+ process.env.HERMES_HOME = previousHome;
55
+ }
56
+ }
57
+ }
58
+ test("isAutomatedHermesSessionPrompt detects scheduled cron job instructions", () => {
59
+ const content = "[IMPORTANT: You are running as a scheduled cron job. DELIVERY: Your final response is the delivery channel.]";
60
+ assert.equal(isAutomatedHermesSessionPrompt(content), true);
61
+ });
62
+ test("isAutomatedHermesSessionPrompt detects background skill runs", () => {
63
+ const content = "You are running as Hermes' background skill CURATOR. This is an UMBRELLA-BUILDING task.";
64
+ assert.equal(isAutomatedHermesSessionPrompt(content), true);
65
+ });
66
+ test("isAutomatedHermesSessionPrompt detects Hermes cron wrapper prompts", () => {
67
+ const content = `Cronjob Response: Morning feeds
68
+ -------------
69
+ Summarize today's AI news.
70
+
71
+ Note: The agent cannot see this message, and therefore cannot respond to it.`;
72
+ assert.equal(isAutomatedHermesSessionPrompt(content), true);
73
+ });
74
+ test("isAutomatedHermesSessionPrompt detects cron metadata headers", () => {
75
+ const content = `# Cron Job: Morning briefing
76
+ - Job ID: \`abc123\`
77
+ - Run Time: 2026-06-01 08:41:00`;
78
+ assert.equal(isAutomatedHermesSessionPrompt(content), true);
79
+ });
80
+ test("isAutomatedHermesSessionPrompt returns false for normal user chat", () => {
81
+ assert.equal(isAutomatedHermesSessionPrompt("Kan du lage en cron job som kjører hvert 5. minutt?"), false);
82
+ assert.equal(isAutomatedHermesSessionPrompt("Flott"), false);
83
+ assert.equal(isAutomatedHermesSessionPrompt("Sure, I can help with that."), false);
84
+ });
85
+ test("getSessionUsage returns zeros when state.db is missing", () => {
86
+ const previousHome = process.env.HERMES_HOME;
87
+ process.env.HERMES_HOME = mkdtempSync(join(tmpdir(), "hermes-session-db-missing-"));
88
+ try {
89
+ const usage = getSessionUsage("sess_missing");
90
+ assert.equal(usage.sessionId, "sess_missing");
91
+ assert.equal(usage.inputTokens, 0);
92
+ assert.equal(usage.outputTokens, 0);
93
+ }
94
+ finally {
95
+ if (previousHome === undefined) {
96
+ delete process.env.HERMES_HOME;
97
+ }
98
+ else {
99
+ process.env.HERMES_HOME = previousHome;
100
+ }
101
+ }
102
+ });
103
+ test("getSessionUsage sums token counters across parent and child sessions", () => {
104
+ withHermesStateDb((db) => {
105
+ const now = Date.now() / 1000;
106
+ db.prepare(`INSERT INTO sessions (
107
+ id, source, model, parent_session_id, started_at,
108
+ input_tokens, output_tokens, cache_read_tokens, cache_write_tokens,
109
+ reasoning_tokens, estimated_cost_usd, message_count
110
+ ) VALUES (?, 'cli', ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?)`).run("parent_sess", "anthropic/claude-sonnet-4", now, 10_000, 2_000, 500, 100, 50, 0.12, 8);
111
+ db.prepare(`INSERT INTO sessions (
112
+ id, source, model, parent_session_id, started_at,
113
+ input_tokens, output_tokens, cache_read_tokens, cache_write_tokens,
114
+ reasoning_tokens, estimated_cost_usd, message_count
115
+ ) VALUES (?, 'cli', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run("child_sess", "anthropic/claude-sonnet-4", "parent_sess", now + 1, 5_000, 1_000, 200, 40, 20, 0.05, 4);
116
+ db.prepare(`INSERT INTO messages (session_id, role, content, timestamp)
117
+ VALUES (?, 'user', ?, ?)`).run("child_sess", "hello", now + 2);
118
+ }, () => {
119
+ const usage = getSessionUsage("child_sess");
120
+ assert.equal(usage.resolvedSessionId, "child_sess");
121
+ assert.equal(usage.model, "anthropic/claude-sonnet-4");
122
+ assert.equal(usage.inputTokens, 15_000);
123
+ assert.equal(usage.outputTokens, 3_000);
124
+ assert.equal(usage.cacheReadTokens, 700);
125
+ assert.equal(usage.cacheWriteTokens, 140);
126
+ assert.equal(usage.reasoningTokens, 70);
127
+ assert.equal(usage.messageCount, 12);
128
+ assert.ok(usage.estimatedCostUsd != null && Math.abs(usage.estimatedCostUsd - 0.17) < 0.001);
129
+ });
130
+ });
131
+ test("getSessionUsage resolves parent id to child session with messages", () => {
132
+ withHermesStateDb((db) => {
133
+ const now = Date.now() / 1000;
134
+ db.prepare(`INSERT INTO sessions (
135
+ id, source, model, parent_session_id, started_at,
136
+ input_tokens, output_tokens, message_count
137
+ ) VALUES (?, 'cli', ?, NULL, ?, ?, ?, ?)`).run("parent_only", "hermes-agent", now, 100, 50, 2);
138
+ db.prepare(`INSERT INTO sessions (
139
+ id, source, model, parent_session_id, started_at,
140
+ input_tokens, output_tokens, message_count
141
+ ) VALUES (?, 'cli', ?, ?, ?, ?, ?, ?)`).run("child_active", "hermes-agent", "parent_only", now + 1, 400, 200, 3);
142
+ db.prepare(`INSERT INTO messages (session_id, role, content, timestamp)
143
+ VALUES (?, 'user', ?, ?)`).run("child_active", "follow-up", now + 2);
144
+ }, () => {
145
+ const usage = getSessionUsage("parent_only");
146
+ assert.equal(usage.resolvedSessionId, "child_active");
147
+ assert.equal(usage.inputTokens, 500);
148
+ assert.equal(usage.outputTokens, 250);
149
+ assert.equal(usage.messageCount, 5);
150
+ });
151
+ });
package/dist/index.d.ts CHANGED
@@ -7,8 +7,8 @@ export type { PairingInfo } from "./resolve.js";
7
7
  export { pairWithCleos } from "./pairWithCleos.js";
8
8
  export { createHermesMessageHandler, forwardToHermes, resolveHermesApiConfig } from "./hermesForwarder.js";
9
9
  export type { HermesForwarderOptions, HermesForwardResult } from "./hermesForwarder.js";
10
- export { listHermesSessions, listSessionMessages, resolveSessionTitle, resolveSessionTitles, } from "./hermesSessionDb.js";
11
- export type { HermesSessionListEntry } from "./hermesSessionDb.js";
10
+ export { getSessionUsage, listHermesSessions, listSessionMessages, resolveSessionTitle, resolveSessionTitles, } from "./hermesSessionDb.js";
11
+ export type { HermesSessionListEntry, HermesSessionUsage } from "./hermesSessionDb.js";
12
12
  export type { HermesSessionMessage } from "./hermesSessionDb.js";
13
13
  export { executeHermesCommand, HERMES_COMMAND_NAMES, isHermesCommandName, userSafeCommandError, } from "./hermesCommands.js";
14
14
  export type { HermesCommandErrorCode, HermesCommandName } from "./hermesCommands.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACvF,YAAY,EACV,oBAAoB,EACpB,kBAAkB,EAClB,UAAU,EACV,cAAc,EACd,aAAa,EACb,gBAAgB,EAChB,eAAe,GAChB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,yBAAyB,EAAE,MAAM,kBAAkB,CAAC;AAC/F,YAAY,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AAC9D,OAAO,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AACxE,YAAY,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,0BAA0B,EAAE,eAAe,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAC3G,YAAY,EAAE,sBAAsB,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AACxF,OAAO,EACL,kBAAkB,EAClB,mBAAmB,EACnB,mBAAmB,EACnB,oBAAoB,GACrB,MAAM,sBAAsB,CAAC;AAC9B,YAAY,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AACnE,YAAY,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AACjE,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,mBAAmB,EACnB,oBAAoB,GACrB,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACrF,OAAO,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAC/F,OAAO,EAAE,sBAAsB,EAAE,yBAAyB,EAAE,MAAM,mBAAmB,CAAC;AACtF,OAAO,EAAE,2BAA2B,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AAC9F,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,YAAY,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,EACL,0BAA0B,EAC1B,yBAAyB,EACzB,6BAA6B,EAC7B,sBAAsB,EACtB,2BAA2B,EAC3B,0BAA0B,EAC1B,gCAAgC,EAChC,+BAA+B,EAC/B,oBAAoB,EACpB,mBAAmB,GACpB,MAAM,gBAAgB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACvF,YAAY,EACV,oBAAoB,EACpB,kBAAkB,EAClB,UAAU,EACV,cAAc,EACd,aAAa,EACb,gBAAgB,EAChB,eAAe,GAChB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,yBAAyB,EAAE,MAAM,kBAAkB,CAAC;AAC/F,YAAY,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AAC9D,OAAO,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AACxE,YAAY,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,0BAA0B,EAAE,eAAe,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAC3G,YAAY,EAAE,sBAAsB,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AACxF,OAAO,EACL,eAAe,EACf,kBAAkB,EAClB,mBAAmB,EACnB,mBAAmB,EACnB,oBAAoB,GACrB,MAAM,sBAAsB,CAAC;AAC9B,YAAY,EAAE,sBAAsB,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AACvF,YAAY,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AACjE,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,mBAAmB,EACnB,oBAAoB,GACrB,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACrF,OAAO,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAC/F,OAAO,EAAE,sBAAsB,EAAE,yBAAyB,EAAE,MAAM,mBAAmB,CAAC;AACtF,OAAO,EAAE,2BAA2B,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AAC9F,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,YAAY,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,EACL,0BAA0B,EAC1B,yBAAyB,EACzB,6BAA6B,EAC7B,sBAAsB,EACtB,2BAA2B,EAC3B,0BAA0B,EAC1B,gCAAgC,EAChC,+BAA+B,EAC/B,oBAAoB,EACpB,mBAAmB,GACpB,MAAM,gBAAgB,CAAC"}
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ export { loadCredentials, saveCredentials, credentialsPathForDisplay } from "./c
3
3
  export { resolvePairingCode, convexSiteUrlFromEnv } from "./resolve.js";
4
4
  export { pairWithCleos } from "./pairWithCleos.js";
5
5
  export { createHermesMessageHandler, forwardToHermes, resolveHermesApiConfig } from "./hermesForwarder.js";
6
- export { listHermesSessions, listSessionMessages, resolveSessionTitle, resolveSessionTitles, } from "./hermesSessionDb.js";
6
+ export { getSessionUsage, listHermesSessions, listSessionMessages, resolveSessionTitle, resolveSessionTitles, } from "./hermesSessionDb.js";
7
7
  export { executeHermesCommand, HERMES_COMMAND_NAMES, isHermesCommandName, userSafeCommandError, } from "./hermesCommands.js";
8
8
  export { startCronWatcher, listPendingCronFiles, clearDeliveredIndex } from "./cronWatcher.js";
9
9
  export { backfillCronDeliveries, describeCronDeliveryState } from "./cronBackfill.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saleso.innovations/bridge",
3
- "version": "0.1.26",
3
+ "version": "0.1.27",
4
4
  "description": "Connect your Hermes agent to the Cleos iOS app via pairing code.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -36,7 +36,7 @@
36
36
  "lint": "eslint . --max-warnings 0",
37
37
  "check-types": "tsc --noEmit",
38
38
  "prepublishOnly": "npm run build",
39
- "test": "node --import tsx --test src/hermesFiles.test.ts src/shellSession.test.ts"
39
+ "test": "node --import tsx --test src/hermesFiles.test.ts src/hermesSessionDb.test.ts src/shellSession.test.ts"
40
40
  },
41
41
  "dependencies": {
42
42
  "better-sqlite3": "^11.10.0",