@saleso.innovations/bridge 0.1.25 → 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 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
package/dist/client.js CHANGED
@@ -135,7 +135,20 @@ async function handleHermesCommandEnvelope(ws, envelope, fallbackAgentId) {
135
135
  status: "accepted",
136
136
  }));
137
137
  try {
138
- const result = await executeHermesCommand(command, parseCommandArgs(envelope.args));
138
+ const onDelta = command === "shell.exec"
139
+ ? (stream, delta, deltaSequence) => {
140
+ ws.send(JSON.stringify({
141
+ type: "hermes.command.delta",
142
+ requestId,
143
+ agentId,
144
+ command,
145
+ stream,
146
+ delta,
147
+ sequence: deltaSequence,
148
+ }));
149
+ }
150
+ : undefined;
151
+ const result = await executeHermesCommand(command, parseCommandArgs(envelope.args), { onDelta });
139
152
  ws.send(JSON.stringify({
140
153
  type: "hermes.command.result",
141
154
  requestId,
@@ -6,6 +6,10 @@ export declare const DEFAULT_HERMES_API_URL = "http://127.0.0.1:8642/v1/chat/com
6
6
  export declare const HERMES_COMMANDS_CAPABILITY = "hermes.commands.v1";
7
7
  /** Bridge can resolve auto-generated session titles from ~/.hermes/state.db. */
8
8
  export declare const HERMES_SESSION_TITLES_CAPABILITY = "hermes.commands.sessions.titles.v1";
9
+ /** Bridge can list all Hermes sessions from ~/.hermes/state.db (CLI + Cleos + other clients). */
10
+ export declare const HERMES_SESSIONS_LIST_CAPABILITY = "hermes.commands.sessions.list.v1";
11
+ /** Remote shell command execution over relay. */
12
+ export declare const TERMINAL_SHELL_CAPABILITY = "terminal.shell.v1";
9
13
  export declare const DEFAULT_BRIDGE_CAPABILITIES: string[];
10
14
  /** VPS one-line installer (served from public Convex HTTP). */
11
15
  export declare const DEFAULT_BRIDGE_INSTALL_URL = "https://amicable-elephant-407.convex.site/install-bridge.sh";
@@ -1 +1 @@
1
- {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,eAAO,MAAM,6BAA6B,8CAA8C,CAAC;AAEzF,6FAA6F;AAC7F,eAAO,MAAM,sBAAsB,8CAA8C,CAAC;AAElF,4DAA4D;AAC5D,eAAO,MAAM,0BAA0B,uBAAuB,CAAC;AAE/D,gFAAgF;AAChF,eAAO,MAAM,gCAAgC,uCAAuC,CAAC;AAErF,eAAO,MAAM,2BAA2B,UAIvC,CAAC;AAEF,+DAA+D;AAC/D,eAAO,MAAM,0BAA0B,gEACwB,CAAC;AAEhE,6DAA6D;AAC7D,eAAO,MAAM,yBAAyB,+DACwB,CAAC;AAE/D,uGAAuG;AACvG,eAAO,MAAM,mBAAmB,gCAAgC,CAAC;AAEjE,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEzD;AAED,wBAAgB,mBAAmB,IAAI,MAAM,CAE5C"}
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,eAAO,MAAM,6BAA6B,8CAA8C,CAAC;AAEzF,6FAA6F;AAC7F,eAAO,MAAM,sBAAsB,8CAA8C,CAAC;AAElF,4DAA4D;AAC5D,eAAO,MAAM,0BAA0B,uBAAuB,CAAC;AAE/D,gFAAgF;AAChF,eAAO,MAAM,gCAAgC,uCAAuC,CAAC;AAErF,iGAAiG;AACjG,eAAO,MAAM,+BAA+B,qCAAqC,CAAC;AAElF,iDAAiD;AACjD,eAAO,MAAM,yBAAyB,sBAAsB,CAAC;AAE7D,eAAO,MAAM,2BAA2B,UAMvC,CAAC;AAEF,+DAA+D;AAC/D,eAAO,MAAM,0BAA0B,gEACwB,CAAC;AAEhE,6DAA6D;AAC7D,eAAO,MAAM,yBAAyB,+DACwB,CAAC;AAE/D,uGAAuG;AACvG,eAAO,MAAM,mBAAmB,gCAAgC,CAAC;AAEjE,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEzD;AAED,wBAAgB,mBAAmB,IAAI,MAAM,CAE5C"}
package/dist/constants.js CHANGED
@@ -6,10 +6,16 @@ export const DEFAULT_HERMES_API_URL = "http://127.0.0.1:8642/v1/chat/completions
6
6
  export const HERMES_COMMANDS_CAPABILITY = "hermes.commands.v1";
7
7
  /** Bridge can resolve auto-generated session titles from ~/.hermes/state.db. */
8
8
  export const HERMES_SESSION_TITLES_CAPABILITY = "hermes.commands.sessions.titles.v1";
9
+ /** Bridge can list all Hermes sessions from ~/.hermes/state.db (CLI + Cleos + other clients). */
10
+ export const HERMES_SESSIONS_LIST_CAPABILITY = "hermes.commands.sessions.list.v1";
11
+ /** Remote shell command execution over relay. */
12
+ export const TERMINAL_SHELL_CAPABILITY = "terminal.shell.v1";
9
13
  export const DEFAULT_BRIDGE_CAPABILITIES = [
10
14
  "chat",
11
15
  HERMES_COMMANDS_CAPABILITY,
12
16
  HERMES_SESSION_TITLES_CAPABILITY,
17
+ HERMES_SESSIONS_LIST_CAPABILITY,
18
+ TERMINAL_SHELL_CAPABILITY,
13
19
  ];
14
20
  /** VPS one-line installer (served from public Convex HTTP). */
15
21
  export const DEFAULT_BRIDGE_INSTALL_URL = "https://amicable-elephant-407.convex.site/install-bridge.sh";
@@ -1,4 +1,5 @@
1
- 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", "skills.list", "files.list", "files.read", "files.write", "memories.list"];
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.usage.get", "sessions.list", "skills.list", "files.list", "files.read", "files.write", "memories.list", "shell.exec", "shell.session.reset"];
2
3
  export type HermesCommandName = (typeof HERMES_COMMAND_NAMES)[number];
3
4
  export declare function isHermesCommandName(value: string): value is HermesCommandName;
4
5
  export type HermesCommandErrorCode = "command_unsupported" | "hermes_unreachable" | "hermes_request_failed" | "invalid_command_args" | "unsupported_by_http";
@@ -10,6 +11,7 @@ export declare function executeHermesCommand(command: HermesCommandName, args: R
10
11
  apiUrl?: string;
11
12
  apiKey?: string;
12
13
  model?: string;
14
+ onDelta?: ShellDeltaEmitter;
13
15
  }): Promise<unknown>;
14
16
  export declare function userSafeCommandError(error: unknown): {
15
17
  code: HermesCommandErrorCode;
@@ -1 +1 @@
1
- {"version":3,"file":"hermesCommands.d.ts","sourceRoot":"","sources":["../src/hermesCommands.ts"],"names":[],"mappings":"AAcA,eAAO,MAAM,oBAAoB,ujBAiCvB,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,CAAA;CAAO,GACjE,OAAO,CAAC,OAAO,CAAC,CAyMlB;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,10 +1,11 @@
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 { 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";
8
+ import { executeShellExec, resetShellSession } from "./shellSession.js";
8
9
  import { fetchHermesRuntimeVersion } from "./runtimeVersion.js";
9
10
  export const HERMES_COMMAND_NAMES = [
10
11
  "runtime.health",
@@ -34,11 +35,15 @@ export const HERMES_COMMAND_NAMES = [
34
35
  "sessions.messages.list",
35
36
  "sessions.messages.countSent",
36
37
  "sessions.titles.resolve",
38
+ "sessions.usage.get",
39
+ "sessions.list",
37
40
  "skills.list",
38
41
  "files.list",
39
42
  "files.read",
40
43
  "files.write",
41
44
  "memories.list",
45
+ "shell.exec",
46
+ "shell.session.reset",
42
47
  ];
43
48
  export function isHermesCommandName(value) {
44
49
  return HERMES_COMMAND_NAMES.includes(value);
@@ -139,6 +144,7 @@ async function hermesFetchVoid(config, path, init = {}) {
139
144
  }
140
145
  export async function executeHermesCommand(command, args, options = {}) {
141
146
  const config = resolveHermesApiConfig(options);
147
+ const { onDelta } = options;
142
148
  switch (command) {
143
149
  case "runtime.health":
144
150
  return await hermesFetchJson(config, "/health");
@@ -306,6 +312,14 @@ export async function executeHermesCommand(command, args, options = {}) {
306
312
  const sessionIds = optionalStringArray(args, "sessionIds") ?? [];
307
313
  return { titles: resolveSessionTitles(sessionIds) };
308
314
  }
315
+ case "sessions.usage.get": {
316
+ const sessionId = requireString(args, "sessionId");
317
+ return getSessionUsage(sessionId);
318
+ }
319
+ case "sessions.list": {
320
+ const limit = optionalNumber(args, "limit");
321
+ return { sessions: listHermesSessions({ limit: limit ?? 200 }) };
322
+ }
309
323
  case "skills.list":
310
324
  return await listHermesSkills();
311
325
  case "files.list":
@@ -330,6 +344,14 @@ export async function executeHermesCommand(command, args, options = {}) {
330
344
  }
331
345
  case "memories.list":
332
346
  return await executeMemoriesList();
347
+ case "shell.exec": {
348
+ if (!onDelta) {
349
+ throw new HermesCommandError("invalid_command_args", "shell.exec requires streaming support");
350
+ }
351
+ return await executeShellExec(args, onDelta);
352
+ }
353
+ case "shell.session.reset":
354
+ return resetShellSession(requireString(args, "sessionId"));
333
355
  default: {
334
356
  const _exhaustive = command;
335
357
  throw new HermesCommandError("command_unsupported", `Unsupported command: ${String(_exhaustive)}`);
@@ -16,6 +16,32 @@ export declare function getLatestTurnMessageIds(sessionId: string): {
16
16
  userMessageId?: number;
17
17
  assistantMessageId?: number;
18
18
  };
19
+ export type HermesSessionListEntry = {
20
+ id: string;
21
+ title: string | null;
22
+ startedAt: number;
23
+ lastMessageAt: number;
24
+ previewText: string;
25
+ messageCount: number;
26
+ };
27
+ /** Hermes-injected prompts for cron runs and background skills — not user chat. */
28
+ export declare function isAutomatedHermesSessionPrompt(content: string): boolean;
29
+ export declare function listHermesSessions(options?: {
30
+ limit?: number;
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;
19
45
  export declare function hermesStateDbExists(): boolean;
20
46
  export declare function countUserMessagesSent(): {
21
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,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"}
@@ -213,6 +213,159 @@ export function getLatestTurnMessageIds(sessionId) {
213
213
  db.close();
214
214
  }
215
215
  }
216
+ function normalizeTimestampMs(value) {
217
+ return value < 1_000_000_000_000 ? value * 1000 : value;
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
+ }
247
+ export function listHermesSessions(options = {}) {
248
+ if (!hermesStateDbExists()) {
249
+ return [];
250
+ }
251
+ const limit = Math.min(Math.max(options.limit ?? 200, 1), 500);
252
+ const fetchLimit = Math.min(limit * 4, 500);
253
+ const db = openReadOnlyDb();
254
+ try {
255
+ const rows = db
256
+ .prepare(`SELECT
257
+ s.id AS id,
258
+ s.title AS title,
259
+ s.started_at AS started_at,
260
+ MAX(m.timestamp) AS last_message_at,
261
+ COUNT(m.id) AS message_count
262
+ FROM sessions s
263
+ INNER JOIN messages m ON m.session_id = s.id AND m.role IN ('user', 'assistant')
264
+ GROUP BY s.id
265
+ ORDER BY last_message_at DESC
266
+ LIMIT ?`)
267
+ .all(fetchLimit);
268
+ const entries = [];
269
+ for (const row of rows) {
270
+ if (isAutomatedHermesSessionPrompt(firstUserMessageContent(db, row.id))) {
271
+ continue;
272
+ }
273
+ const previewRow = db
274
+ .prepare(`SELECT content FROM messages
275
+ WHERE session_id = ? AND role IN ('user', 'assistant')
276
+ ORDER BY timestamp DESC, id DESC
277
+ LIMIT 1`)
278
+ .get(row.id);
279
+ const resolvedTitle = sessionTitleForLookupId(db, row.id);
280
+ const previewText = decodeMessageContent(previewRow?.content ?? null).trim().slice(0, 120);
281
+ entries.push({
282
+ id: row.id,
283
+ title: resolvedTitle ?? row.title?.trim() ?? null,
284
+ startedAt: normalizeTimestampMs(row.started_at ?? row.last_message_at),
285
+ lastMessageAt: normalizeTimestampMs(row.last_message_at),
286
+ previewText,
287
+ messageCount: row.message_count,
288
+ });
289
+ if (entries.length >= limit) {
290
+ break;
291
+ }
292
+ }
293
+ return entries;
294
+ }
295
+ catch {
296
+ return [];
297
+ }
298
+ finally {
299
+ db.close();
300
+ }
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
+ }
216
369
  export function hermesStateDbExists() {
217
370
  try {
218
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,7 +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 { listSessionMessages, resolveSessionTitle, resolveSessionTitles } from "./hermesSessionDb.js";
10
+ export { getSessionUsage, listHermesSessions, listSessionMessages, resolveSessionTitle, resolveSessionTitles, } from "./hermesSessionDb.js";
11
+ export type { HermesSessionListEntry, HermesSessionUsage } from "./hermesSessionDb.js";
11
12
  export type { HermesSessionMessage } from "./hermesSessionDb.js";
12
13
  export { executeHermesCommand, HERMES_COMMAND_NAMES, isHermesCommandName, userSafeCommandError, } from "./hermesCommands.js";
13
14
  export type { HermesCommandErrorCode, HermesCommandName } from "./hermesCommands.js";
@@ -16,5 +17,5 @@ export { backfillCronDeliveries, describeCronDeliveryState } from "./cronBackfil
16
17
  export { resolveActiveConversationId, rememberConversationId } from "./activeConversation.js";
17
18
  export { runBridgeDaemon } from "./daemon.js";
18
19
  export type { RunBridgeOptions } from "./daemon.js";
19
- export { DEFAULT_BRIDGE_INSTALL_URL, DEFAULT_BRIDGE_UPDATE_URL, DEFAULT_CLEOS_CONVEX_SITE_URL, DEFAULT_HERMES_API_URL, DEFAULT_BRIDGE_CAPABILITIES, HERMES_COMMANDS_CAPABILITY, HERMES_SESSION_TITLES_CAPABILITY, bridgeInstallCommand, bridgeUpdateCommand, } from "./constants.js";
20
+ export { DEFAULT_BRIDGE_INSTALL_URL, DEFAULT_BRIDGE_UPDATE_URL, DEFAULT_CLEOS_CONVEX_SITE_URL, DEFAULT_HERMES_API_URL, DEFAULT_BRIDGE_CAPABILITIES, HERMES_COMMANDS_CAPABILITY, HERMES_SESSION_TITLES_CAPABILITY, HERMES_SESSIONS_LIST_CAPABILITY, bridgeInstallCommand, bridgeUpdateCommand, } from "./constants.js";
20
21
  //# sourceMappingURL=index.d.ts.map
@@ -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,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AACtG,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,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,10 +3,10 @@ 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 { 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";
10
10
  export { resolveActiveConversationId, rememberConversationId } from "./activeConversation.js";
11
11
  export { runBridgeDaemon } from "./daemon.js";
12
- export { DEFAULT_BRIDGE_INSTALL_URL, DEFAULT_BRIDGE_UPDATE_URL, DEFAULT_CLEOS_CONVEX_SITE_URL, DEFAULT_HERMES_API_URL, DEFAULT_BRIDGE_CAPABILITIES, HERMES_COMMANDS_CAPABILITY, HERMES_SESSION_TITLES_CAPABILITY, bridgeInstallCommand, bridgeUpdateCommand, } from "./constants.js";
12
+ export { DEFAULT_BRIDGE_INSTALL_URL, DEFAULT_BRIDGE_UPDATE_URL, DEFAULT_CLEOS_CONVEX_SITE_URL, DEFAULT_HERMES_API_URL, DEFAULT_BRIDGE_CAPABILITIES, HERMES_COMMANDS_CAPABILITY, HERMES_SESSION_TITLES_CAPABILITY, HERMES_SESSIONS_LIST_CAPABILITY, bridgeInstallCommand, bridgeUpdateCommand, } from "./constants.js";
@@ -0,0 +1,21 @@
1
+ export declare const EXIT_SENTINEL_PREFIX = "__CLEOS_EXIT_";
2
+ export declare const EXIT_SENTINEL_SUFFIX = "__";
3
+ export declare const MAX_OUTPUT_BYTES: number;
4
+ export declare const DEFAULT_COMMAND_TIMEOUT_MS = 120000;
5
+ export type ShellDeltaEmitter = (stream: "stdout" | "stderr", delta: string, sequence: number) => void;
6
+ export type ParsedShellFooter = {
7
+ output: string;
8
+ exitCode: number;
9
+ cwd: string;
10
+ };
11
+ export declare function buildWrappedCommand(userCommand: string): string;
12
+ export declare function parseShellFooter(raw: string): ParsedShellFooter | null;
13
+ export declare function executeShellExec(args: Record<string, unknown>, onDelta: ShellDeltaEmitter, timeoutMs?: number): Promise<{
14
+ exitCode: number;
15
+ sessionId: string;
16
+ }>;
17
+ export declare function resetShellSession(sessionId: string): {
18
+ sessionId: string;
19
+ };
20
+ export declare function clearShellSessionsForTests(): void;
21
+ //# sourceMappingURL=shellSession.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shellSession.d.ts","sourceRoot":"","sources":["../src/shellSession.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,oBAAoB,kBAAkB,CAAC;AACpD,eAAO,MAAM,oBAAoB,OAAO,CAAC;AACzC,eAAO,MAAM,gBAAgB,QAAa,CAAC;AAC3C,eAAO,MAAM,0BAA0B,SAAU,CAAC;AAGlD,MAAM,MAAM,iBAAiB,GAAG,CAAC,MAAM,EAAE,QAAQ,GAAG,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;AAEvG,MAAM,MAAM,iBAAiB,GAAG;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;CACb,CAAC;AAmBF,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAG/D;AAED,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,iBAAiB,GAAG,IAAI,CAWtE;AAmJD,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,OAAO,EAAE,iBAAiB,EAC1B,SAAS,GAAE,MAAmC,GAC7C,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC,CAwClD;AAED,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,CAU1E;AAED,wBAAgB,0BAA0B,IAAI,IAAI,CAOjD"}
@@ -0,0 +1,210 @@
1
+ import { spawn } from "node:child_process";
2
+ import { homedir } from "node:os";
3
+ export const EXIT_SENTINEL_PREFIX = "__CLEOS_EXIT_";
4
+ export const EXIT_SENTINEL_SUFFIX = "__";
5
+ export const MAX_OUTPUT_BYTES = 512 * 1024;
6
+ export const DEFAULT_COMMAND_TIMEOUT_MS = 120_000;
7
+ const SENTINEL_TAIL_RESERVE = 64;
8
+ const sessions = new Map();
9
+ export function buildWrappedCommand(userCommand) {
10
+ const encoded = Buffer.from(userCommand, "utf8").toString("base64");
11
+ return `{ eval "$(printf '%s' '${encoded}' | base64 -d)"; ec=$?; echo "${EXIT_SENTINEL_PREFIX}\${ec}${EXIT_SENTINEL_SUFFIX}"; pwd; } 2>&1\n`;
12
+ }
13
+ export function parseShellFooter(raw) {
14
+ const pattern = new RegExp(`\\n${EXIT_SENTINEL_PREFIX}(\\d+)${EXIT_SENTINEL_SUFFIX}\\n([^\\n]*)\\n?$`);
15
+ const match = raw.match(pattern);
16
+ if (!match || match.index === undefined)
17
+ return null;
18
+ return {
19
+ output: raw.slice(0, match.index),
20
+ exitCode: Number.parseInt(match[1] ?? "0", 10),
21
+ cwd: match[2] ?? "",
22
+ };
23
+ }
24
+ function truncateNotice() {
25
+ return `\n[output truncated at ${MAX_OUTPUT_BYTES} bytes]\n`;
26
+ }
27
+ function emitDelta(session, delta) {
28
+ if (!delta || !session.pendingOnDelta)
29
+ return;
30
+ session.pendingSequence += 1;
31
+ session.pendingOnDelta("stdout", delta, session.pendingSequence);
32
+ }
33
+ function flushStreamableOutput(session) {
34
+ const footer = parseShellFooter(session.pendingBuffer);
35
+ if (footer) {
36
+ const newOutput = footer.output.slice(session.streamedLength);
37
+ emitDelta(session, newOutput);
38
+ session.streamedLength = footer.output.length;
39
+ return;
40
+ }
41
+ const reserveStart = Math.max(0, session.pendingBuffer.length - SENTINEL_TAIL_RESERVE);
42
+ const streamableEnd = reserveStart;
43
+ if (streamableEnd > session.streamedLength) {
44
+ emitDelta(session, session.pendingBuffer.slice(session.streamedLength, streamableEnd));
45
+ session.streamedLength = streamableEnd;
46
+ }
47
+ }
48
+ function finishPending(session, footer) {
49
+ if (session.pendingTimeout) {
50
+ clearTimeout(session.pendingTimeout);
51
+ session.pendingTimeout = null;
52
+ }
53
+ const resolve = session.pendingResolve;
54
+ session.pendingResolve = null;
55
+ session.pendingReject = null;
56
+ session.pendingOnDelta = null;
57
+ session.pendingBuffer = "";
58
+ session.streamedLength = 0;
59
+ session.pendingBytes = 0;
60
+ session.pendingTruncated = false;
61
+ session.busy = false;
62
+ if (footer.cwd)
63
+ session.cwd = footer.cwd;
64
+ resolve?.(footer);
65
+ }
66
+ function failPending(session, error) {
67
+ if (session.pendingTimeout) {
68
+ clearTimeout(session.pendingTimeout);
69
+ session.pendingTimeout = null;
70
+ }
71
+ const reject = session.pendingReject;
72
+ session.pendingResolve = null;
73
+ session.pendingReject = null;
74
+ session.pendingOnDelta = null;
75
+ session.pendingBuffer = "";
76
+ session.streamedLength = 0;
77
+ session.pendingBytes = 0;
78
+ session.pendingTruncated = false;
79
+ session.busy = false;
80
+ reject?.(error);
81
+ }
82
+ function appendPendingData(session, chunk) {
83
+ session.pendingBytes += Buffer.byteLength(chunk, "utf8");
84
+ if (session.pendingBytes > MAX_OUTPUT_BYTES) {
85
+ if (!session.pendingTruncated) {
86
+ session.pendingTruncated = true;
87
+ const allowed = MAX_OUTPUT_BYTES - Buffer.byteLength(truncateNotice(), "utf8");
88
+ const currentBytes = Buffer.byteLength(session.pendingBuffer, "utf8");
89
+ if (currentBytes > allowed) {
90
+ session.pendingBuffer = Buffer.from(session.pendingBuffer, "utf8")
91
+ .subarray(0, allowed)
92
+ .toString("utf8");
93
+ }
94
+ session.pendingBuffer += truncateNotice();
95
+ flushStreamableOutput(session);
96
+ }
97
+ return;
98
+ }
99
+ session.pendingBuffer += chunk;
100
+ flushStreamableOutput(session);
101
+ const footer = parseShellFooter(session.pendingBuffer);
102
+ if (footer) {
103
+ finishPending(session, footer);
104
+ }
105
+ }
106
+ function attachSessionHandlers(sessionId, session) {
107
+ session.process.stdout.on("data", (data) => {
108
+ appendPendingData(session, data.toString("utf8"));
109
+ });
110
+ session.process.stderr.on("data", (data) => {
111
+ if (session.pendingOnDelta && session.busy) {
112
+ session.pendingSequence += 1;
113
+ session.pendingOnDelta("stderr", data.toString("utf8"), session.pendingSequence);
114
+ }
115
+ });
116
+ session.process.on("exit", () => {
117
+ sessions.delete(sessionId);
118
+ if (session.busy) {
119
+ failPending(session, new Error("Shell session exited unexpectedly"));
120
+ }
121
+ });
122
+ }
123
+ function createSession(sessionId) {
124
+ const child = spawn("bash", ["--login"], {
125
+ cwd: homedir(),
126
+ env: process.env,
127
+ stdio: ["pipe", "pipe", "pipe"],
128
+ });
129
+ const session = {
130
+ process: child,
131
+ cwd: homedir(),
132
+ busy: false,
133
+ pendingBuffer: "",
134
+ pendingResolve: null,
135
+ pendingReject: null,
136
+ pendingTimeout: null,
137
+ pendingSequence: 0,
138
+ pendingBytes: 0,
139
+ pendingTruncated: false,
140
+ pendingOnDelta: null,
141
+ streamedLength: 0,
142
+ };
143
+ attachSessionHandlers(sessionId, session);
144
+ sessions.set(sessionId, session);
145
+ return session;
146
+ }
147
+ function getOrCreateSession(sessionId) {
148
+ const existing = sessions.get(sessionId);
149
+ if (existing && existing.process.exitCode === null && !existing.process.killed) {
150
+ return existing;
151
+ }
152
+ if (existing)
153
+ sessions.delete(sessionId);
154
+ return createSession(sessionId);
155
+ }
156
+ export async function executeShellExec(args, onDelta, timeoutMs = DEFAULT_COMMAND_TIMEOUT_MS) {
157
+ const command = args.command;
158
+ const sessionIdRaw = args.sessionId;
159
+ if (typeof command !== "string" || command.trim().length === 0) {
160
+ throw new Error('Missing "command"');
161
+ }
162
+ if (typeof sessionIdRaw !== "string" || sessionIdRaw.trim().length === 0) {
163
+ throw new Error('Missing "sessionId"');
164
+ }
165
+ const sessionId = sessionIdRaw.trim();
166
+ const session = getOrCreateSession(sessionId);
167
+ if (session.busy) {
168
+ throw new Error("Shell session is busy");
169
+ }
170
+ session.busy = true;
171
+ session.pendingBuffer = "";
172
+ session.streamedLength = 0;
173
+ session.pendingBytes = 0;
174
+ session.pendingTruncated = false;
175
+ session.pendingSequence = 0;
176
+ session.pendingOnDelta = onDelta;
177
+ const wrapped = buildWrappedCommand(command);
178
+ return await new Promise((resolve, reject) => {
179
+ session.pendingResolve = (footer) => {
180
+ resolve({ exitCode: footer.exitCode, sessionId });
181
+ };
182
+ session.pendingReject = reject;
183
+ session.pendingTimeout = setTimeout(() => {
184
+ failPending(session, new Error(`Command timed out after ${timeoutMs}ms`));
185
+ }, timeoutMs);
186
+ const wrote = session.process.stdin.write(wrapped);
187
+ if (!wrote) {
188
+ session.process.stdin.once("drain", () => undefined);
189
+ }
190
+ });
191
+ }
192
+ export function resetShellSession(sessionId) {
193
+ const existing = sessions.get(sessionId);
194
+ if (existing) {
195
+ if (existing.busy) {
196
+ throw new Error("Shell session is busy");
197
+ }
198
+ existing.process.kill("SIGTERM");
199
+ sessions.delete(sessionId);
200
+ }
201
+ return { sessionId };
202
+ }
203
+ export function clearShellSessionsForTests() {
204
+ for (const [sessionId, session] of sessions.entries()) {
205
+ if (!session.busy) {
206
+ session.process.kill("SIGTERM");
207
+ }
208
+ sessions.delete(sessionId);
209
+ }
210
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=shellSession.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shellSession.test.d.ts","sourceRoot":"","sources":["../src/shellSession.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,27 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { buildWrappedCommand, parseShellFooter, EXIT_SENTINEL_PREFIX, EXIT_SENTINEL_SUFFIX, } from "./shellSession.js";
4
+ test("parseShellFooter extracts output, exit code, and cwd", () => {
5
+ const raw = "line one\nline two\n" + `${EXIT_SENTINEL_PREFIX}1${EXIT_SENTINEL_SUFFIX}\n/tmp\n`;
6
+ const parsed = parseShellFooter(raw);
7
+ assert.ok(parsed);
8
+ assert.equal(parsed.output, "line one\nline two");
9
+ assert.equal(parsed.exitCode, 1);
10
+ assert.equal(parsed.cwd, "/tmp");
11
+ });
12
+ test("parseShellFooter returns null when sentinel is missing", () => {
13
+ assert.equal(parseShellFooter("hello\nworld\n"), null);
14
+ });
15
+ test("parseShellFooter handles exit code zero", () => {
16
+ const raw = "ok\n" + `${EXIT_SENTINEL_PREFIX}0${EXIT_SENTINEL_SUFFIX}\n/home/user\n`;
17
+ const parsed = parseShellFooter(raw);
18
+ assert.ok(parsed);
19
+ assert.equal(parsed.exitCode, 0);
20
+ assert.equal(parsed.cwd, "/home/user");
21
+ });
22
+ test("buildWrappedCommand base64-encodes user input", () => {
23
+ const wrapped = buildWrappedCommand("echo hello");
24
+ assert.match(wrapped, /base64 -d/);
25
+ assert.match(wrapped, new RegExp(`${EXIT_SENTINEL_PREFIX}\\$\\{ec\\}${EXIT_SENTINEL_SUFFIX}`));
26
+ assert.match(wrapped, /pwd/);
27
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saleso.innovations/bridge",
3
- "version": "0.1.25",
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"
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",