@saleso.innovations/bridge 0.1.38 → 0.1.40

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/INTEGRATION.md CHANGED
@@ -34,10 +34,21 @@ export async function onCleosPairingCodeSubmitted(code: string) {
34
34
  for await (const chunk of stream) {
35
35
  reply.delta(chunk, sequence++);
36
36
  }
37
- // If you stream from Hermes /v1/chat/completions, also forward tool progress via
38
- // reply.activity() when you receive `event: hermes.tool.progress` SSE events.
37
+ // If you stream from Hermes /v1/chat/completions, also forward activity via
38
+ // reply.activity() when you receive progress SSE events:
39
+ // - `event: hermes.tool.progress` { tool, label?, emoji?, source?, server? }
40
+ // - `event: hermes.skill.progress` { skill, label?, emoji? }
41
+ // - `event: hermes.memory.progress` { memory, label?, emoji? }
42
+ // - `event: hermes.mcp.progress` { server, tool?, label?, emoji? }
39
43
  // Do not inject tool markers into assistant text (see Hermes #6972).
40
- reply.complete(stream.finalText, sequence);
44
+ // Accumulate the capabilities used during the turn and pass a `turnActivity`
45
+ // summary (plus the model that produced the reply) to reply.complete() so Cleos
46
+ // can show it on the completed message. The `forwardToHermes` helper does this
47
+ // automatically when you use createHermesMessageHandler().
48
+ reply.complete(stream.finalText, sequence, undefined, undefined, {
49
+ tools: [{ name: "web_search", count: 2 }],
50
+ model: "anthropic/claude-sonnet-4",
51
+ });
41
52
  },
42
53
  });
43
54
  return session.agentId;
package/dist/client.d.ts CHANGED
@@ -22,13 +22,44 @@ export type UserMessageMeta = {
22
22
  replyMessageId: string;
23
23
  attachments?: UserMessageAttachment[];
24
24
  };
25
+ export type AgentActivityKind = "tool" | "skill" | "memory" | "mcp";
25
26
  export type AgentActivityPayload = {
26
27
  activityType: "tool.started" | "tool.completed";
28
+ kind?: AgentActivityKind;
27
29
  tool?: string;
30
+ skill?: string;
31
+ memory?: string;
32
+ server?: string;
33
+ source?: string;
28
34
  label?: string;
29
35
  emoji?: string;
30
36
  sequence: number;
31
37
  };
38
+ /** Per-message summary of capabilities the agent used. Keep in sync with @repo/backend-types. */
39
+ export type AgentTurnActivity = {
40
+ tools?: Array<{
41
+ name: string;
42
+ label?: string;
43
+ emoji?: string;
44
+ count?: number;
45
+ source?: string;
46
+ }>;
47
+ skills?: Array<{
48
+ name: string;
49
+ label?: string;
50
+ }>;
51
+ memories?: Array<{
52
+ name: string;
53
+ label?: string;
54
+ }>;
55
+ mcps?: Array<{
56
+ server: string;
57
+ tool?: string;
58
+ label?: string;
59
+ count?: number;
60
+ }>;
61
+ model?: string;
62
+ };
32
63
  export type AgentFailedPayload = {
33
64
  error: string;
34
65
  code?: string;
@@ -37,7 +68,7 @@ export type AgentFailedPayload = {
37
68
  export type AgentReply = {
38
69
  delta: (text: string, sequence: number) => void;
39
70
  activity: (activity: AgentActivityPayload) => void;
40
- complete: (text: string, sequence: number, attachments?: UserMessageAttachment[], hermesAssistantMessageId?: string) => void;
71
+ complete: (text: string, sequence: number, attachments?: UserMessageAttachment[], hermesAssistantMessageId?: string, turnActivity?: AgentTurnActivity) => void;
41
72
  failed: (failure: AgentFailedPayload) => void;
42
73
  };
43
74
  export type CronDeliveryMeta = {
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAEA,OAAO,EAAoC,KAAK,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AAYhG,MAAM,MAAM,cAAc,GAAG;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CACrG,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC;IACvB,eAAe,EAAE,MAAM,CAAC;IACxB,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,CAAC,EAAE,qBAAqB,EAAE,CAAC;CACvC,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,YAAY,EAAE,cAAc,GAAG,gBAAgB,CAAC;IAChD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAChD,QAAQ,EAAE,CAAC,QAAQ,EAAE,oBAAoB,KAAK,IAAI,CAAC;IACnD,QAAQ,EAAE,CACR,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,WAAW,CAAC,EAAE,qBAAqB,EAAE,EACrC,wBAAwB,CAAC,EAAE,MAAM,KAC9B,IAAI,CAAC;IACV,MAAM,EAAE,CAAC,OAAO,EAAE,kBAAkB,KAAK,IAAI,CAAC;CAC/C,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,cAAc,EAAE,MAAM,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAE/D;AAED,MAAM,MAAM,aAAa,GAAG;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,MAAM,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB,iBAAiB,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,gBAAgB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/E,CAAC;AAkXF,wBAAsB,cAAc,CAAC,OAAO,EAAE,IAAI,CAAC,cAAc,EAAE,eAAe,CAAC,GAAG,OAAO,CAAC;IAC5F,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC,CA2BD;AAED,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC,CAcxF;AAED,wBAAsB,oBAAoB,CAAC,OAAO,GAAE;IAClD,WAAW,CAAC,EAAE,qBAAqB,CAAC;IACpC,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,aAAa,CAAC,EAAE,cAAc,CAAC,eAAe,CAAC,CAAC;CAC5C,GAAG,OAAO,CAAC,aAAa,CAAC,CAc9B"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
package/dist/client.js CHANGED
@@ -9,6 +9,13 @@ import { DEFAULT_BRIDGE_CAPABILITIES } from "./constants.js";
9
9
  export function cronDeliveryMessageId(sourceKey) {
10
10
  return `cron:${sourceKey}`;
11
11
  }
12
+ function hasTurnActivity(activity) {
13
+ return ((activity.tools?.length ?? 0) > 0 ||
14
+ (activity.skills?.length ?? 0) > 0 ||
15
+ (activity.memories?.length ?? 0) > 0 ||
16
+ (activity.mcps?.length ?? 0) > 0 ||
17
+ typeof activity.model === "string");
18
+ }
12
19
  function parseUserMessageAttachments(raw) {
13
20
  if (!Array.isArray(raw))
14
21
  return undefined;
@@ -71,13 +78,25 @@ function createReplySender(ws, agentId, conversationId, messageId) {
71
78
  conversationId,
72
79
  messageId,
73
80
  activityType: activity.activityType,
81
+ kind: activity.kind,
74
82
  tool: activity.tool,
83
+ skill: activity.skill,
84
+ memory: activity.memory,
85
+ server: activity.server,
86
+ source: activity.source,
75
87
  label: activity.label,
76
88
  emoji: activity.emoji,
77
89
  sequence: activity.sequence,
78
90
  }));
79
91
  },
80
- complete(text, sequence, attachments, hermesAssistantMessageId) {
92
+ complete(text, sequence, attachments, hermesAssistantMessageId, turnActivity) {
93
+ const metadata = {};
94
+ if (hermesAssistantMessageId) {
95
+ metadata.hermes = { assistantMessageId: hermesAssistantMessageId };
96
+ }
97
+ if (turnActivity && hasTurnActivity(turnActivity)) {
98
+ metadata.turnActivity = turnActivity;
99
+ }
81
100
  ws.send(JSON.stringify({
82
101
  type: "agent.message",
83
102
  agentId,
@@ -87,9 +106,7 @@ function createReplySender(ws, agentId, conversationId, messageId) {
87
106
  sequence,
88
107
  final: true,
89
108
  ...(attachments && attachments.length > 0 ? { attachments } : {}),
90
- ...(hermesAssistantMessageId
91
- ? { metadata: { hermes: { assistantMessageId: hermesAssistantMessageId } } }
92
- : {}),
109
+ ...(Object.keys(metadata).length > 0 ? { metadata } : {}),
93
110
  }));
94
111
  },
95
112
  failed(failure) {
@@ -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", "bridge.version", "bridge.update", "sessions.messages.list", "sessions.messages.countSent", "sessions.titles.resolve", "sessions.usage.get", "sessions.list", "skills.list", "tools.list", "tools.set", "mcp.list", "mcp.add", "mcp.remove", "files.list", "files.read", "files.write", "files.delete", "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", "bridge.version", "bridge.update", "sessions.messages.list", "sessions.messages.countSent", "sessions.titles.resolve", "sessions.rename", "sessions.usage.get", "sessions.list", "skills.list", "tools.list", "tools.set", "mcp.list", "mcp.add", "mcp.remove", "files.list", "files.read", "files.write", "files.delete", "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":"AAqBA,OAAO,EAAuC,KAAK,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAMhG,eAAO,MAAM,oBAAoB,svBA6CvB,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;AAkID,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,CAkQlB;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":"AAsBA,OAAO,EAAuC,KAAK,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAMhG,eAAO,MAAM,oBAAoB,ywBA8CvB,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;AAkID,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,CAuQlB;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG;IAAE,IAAI,EAAE,sBAAsB,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAetG"}
@@ -7,6 +7,7 @@ import { listHermesTools, setHermesToolEnabled } from "./toolsList.js";
7
7
  import { addHermesMcpServer, listHermesMcpServers, removeHermesMcpServer } from "./mcpList.js";
8
8
  import { runHermesUpdate } from "./hermesUpdate.js";
9
9
  import { executeFilesList, executeFilesRead, executeFilesWrite, executeFilesDelete, executeMemoriesList, } from "./hermesFileCommands.js";
10
+ import { renameHermesSession } from "./renameHermesSession.js";
10
11
  import { executeShellExec, resetShellSession } from "./shellSession.js";
11
12
  import { fetchHermesRuntimeVersion } from "./runtimeVersion.js";
12
13
  import { fetchBridgeRuntimeVersion, fetchInstalledBridgeVersion } from "./bridgeVersion.js";
@@ -42,6 +43,7 @@ export const HERMES_COMMAND_NAMES = [
42
43
  "sessions.messages.list",
43
44
  "sessions.messages.countSent",
44
45
  "sessions.titles.resolve",
46
+ "sessions.rename",
45
47
  "sessions.usage.get",
46
48
  "sessions.list",
47
49
  "skills.list",
@@ -355,6 +357,11 @@ export async function executeHermesCommand(command, args, options = {}) {
355
357
  const sessionIds = optionalStringArray(args, "sessionIds") ?? [];
356
358
  return { titles: resolveSessionTitles(sessionIds) };
357
359
  }
360
+ case "sessions.rename": {
361
+ const sessionId = requireString(args, "sessionId");
362
+ const title = requireString(args, "title");
363
+ return await renameHermesSession(sessionId, title);
364
+ }
358
365
  case "sessions.usage.get": {
359
366
  const sessionId = requireString(args, "sessionId");
360
367
  return getSessionUsage(sessionId);
@@ -1 +1 @@
1
- {"version":3,"file":"hermesForwarder.d.ts","sourceRoot":"","sources":["../src/hermesForwarder.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAyB,eAAe,EAAE,MAAM,aAAa,CAAC;AAWtF,MAAM,MAAM,sBAAsB,GAAG;IACnC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAkBF,wBAAgB,sBAAsB,CAAC,OAAO,GAAE,sBAA2B,GAAG;IAC5E,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,KAAK,EAAE,MAAM,CAAC;CACf,CAUA;AA+MD,wBAAsB,eAAe,CACnC,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,eAAe,EACrB,KAAK,EAAE,UAAU,EACjB,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,mBAAmB,CAAC,CA6E9B;AAED,wBAAgB,0BAA0B,CAAC,OAAO,GAAE,sBAA2B,IAC/D,SAAS,MAAM,EAAE,MAAM,eAAe,EAAE,OAAO,UAAU,KAAG,OAAO,CAAC,IAAI,CAAC,CAMxF"}
1
+ {"version":3,"file":"hermesForwarder.d.ts","sourceRoot":"","sources":["../src/hermesForwarder.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAyB,eAAe,EAAE,MAAM,aAAa,CAAC;AAkBtF,MAAM,MAAM,sBAAsB,GAAG;IACnC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAkBF,wBAAgB,sBAAsB,CAAC,OAAO,GAAE,sBAA2B,GAAG;IAC5E,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,KAAK,EAAE,MAAM,CAAC;CACf,CAUA;AAgID,wBAAsB,eAAe,CACnC,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,eAAe,EACrB,KAAK,EAAE,UAAU,EACjB,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,mBAAmB,CAAC,CAkF9B;AA2BD,wBAAgB,0BAA0B,CAAC,OAAO,GAAE,sBAA2B,IAC/D,SAAS,MAAM,EAAE,MAAM,eAAe,EAAE,OAAO,UAAU,KAAG,OAAO,CAAC,IAAI,CAAC,CAMxF"}
@@ -3,7 +3,8 @@ import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import { linkHermesMessageIds, uploadFileToConvex } from "./convexRelay.js";
5
5
  import { DEFAULT_HERMES_API_URL } from "./constants.js";
6
- import { getLatestTurnMessageIds, hermesStateDbExists } from "./hermesSessionDb.js";
6
+ import { getLatestTurnMessageIds, getSessionUsage, hermesStateDbExists } from "./hermesSessionDb.js";
7
+ import { buildTurnActivity, createTurnActivityAccumulator, parseSseDataLines, processSseEventBlock, } from "./turnActivity.js";
7
8
  import { parseMediaReferences, resolveMediaReferenceBytes, stripMediaReferences, } from "./hermesFiles.js";
8
9
  function readHermesApiKeyFromEnvFile() {
9
10
  const envPath = join(homedir(), ".hermes", ".env");
@@ -49,70 +50,6 @@ async function ensureHermesReachable(apiUrl, apiKey) {
49
50
  throw new Error(`Hermes is not reachable at ${healthUrl}. Ensure Hermes is installed and run \`hermes gateway\` on this machine. ${message}`);
50
51
  }
51
52
  }
52
- function parseSseDataLines(buffer) {
53
- const parts = buffer.split("\n\n");
54
- const rest = parts.pop() ?? "";
55
- return { events: parts, rest };
56
- }
57
- function extractDeltaFromChunk(payload) {
58
- const choices = payload.choices;
59
- if (!Array.isArray(choices) || choices.length === 0)
60
- return null;
61
- const choice = choices[0];
62
- if (!choice || typeof choice !== "object")
63
- return null;
64
- const delta = choice.delta;
65
- if (!delta || typeof delta !== "object")
66
- return null;
67
- const content = delta.content;
68
- return typeof content === "string" ? content : null;
69
- }
70
- function processSseEventBlock(eventBlock, reply, counters, onTextDelta) {
71
- let eventName = "message";
72
- for (const line of eventBlock.split("\n")) {
73
- if (line.startsWith("event:")) {
74
- eventName = line.slice(6).trim();
75
- continue;
76
- }
77
- if (!line.startsWith("data:"))
78
- continue;
79
- const data = line.slice(5).trim();
80
- if (!data || data === "[DONE]")
81
- continue;
82
- if (eventName === "hermes.tool.progress") {
83
- try {
84
- const payload = JSON.parse(data);
85
- const tool = typeof payload.tool === "string" ? payload.tool : undefined;
86
- const label = typeof payload.label === "string" ? payload.label : undefined;
87
- const emoji = typeof payload.emoji === "string" ? payload.emoji : undefined;
88
- reply.activity({
89
- activityType: "tool.started",
90
- tool,
91
- label,
92
- emoji,
93
- sequence: counters.activitySequence,
94
- });
95
- counters.activitySequence += 1;
96
- }
97
- catch {
98
- // Ignore malformed tool progress payloads.
99
- }
100
- continue;
101
- }
102
- try {
103
- const payload = JSON.parse(data);
104
- const delta = extractDeltaFromChunk(payload);
105
- if (delta && delta.trim()) {
106
- onTextDelta(delta);
107
- reply.delta(delta, counters.textSequence);
108
- counters.textSequence += 1;
109
- }
110
- }
111
- catch {
112
- // Ignore malformed SSE chunks.
113
- }
114
- }
115
- }
116
53
  function buildHermesUserContent(text, attachments) {
117
54
  const trimmed = text.trim();
118
55
  const imageParts = (attachments ?? [])
@@ -233,6 +170,7 @@ export async function forwardToHermes(content, meta, reply, options = {}) {
233
170
  throw new Error(`Hermes request failed (${response.status}): ${text}`);
234
171
  }
235
172
  const resolvedSessionId = response.headers.get("X-Hermes-Session-Id")?.trim() || sessionId;
173
+ const modelFromHeader = response.headers.get("X-Hermes-Model")?.trim() || undefined;
236
174
  if (!response.body) {
237
175
  throw new Error("Hermes returned an empty streaming body");
238
176
  }
@@ -240,6 +178,7 @@ export async function forwardToHermes(content, meta, reply, options = {}) {
240
178
  const decoder = new TextDecoder();
241
179
  let buffer = "";
242
180
  const counters = { textSequence: 0, activitySequence: 0 };
181
+ const accumulator = createTurnActivityAccumulator();
243
182
  let fullText = "";
244
183
  while (true) {
245
184
  const { done, value } = await reader.read();
@@ -249,7 +188,7 @@ export async function forwardToHermes(content, meta, reply, options = {}) {
249
188
  const { events, rest } = parseSseDataLines(buffer);
250
189
  buffer = rest;
251
190
  for (const eventBlock of events) {
252
- processSseEventBlock(eventBlock, reply, counters, (delta) => {
191
+ processSseEventBlock(eventBlock, reply, counters, accumulator, (delta) => {
253
192
  fullText += delta;
254
193
  });
255
194
  }
@@ -263,10 +202,33 @@ export async function forwardToHermes(content, meta, reply, options = {}) {
263
202
  const attachments = mediaRefs.length > 0 ? await uploadMediaAttachments(meta.agentId, mediaRefs) : [];
264
203
  const displayText = attachments.length > 0 ? stripMediaReferences(fullText, mediaRefs) : fullText.trim() || fullText;
265
204
  const { assistantMessageId } = getLatestTurnMessageIds(resolvedSessionId);
266
- reply.complete(displayText, counters.textSequence, attachments.length > 0 ? attachments : undefined, assistantMessageId !== undefined ? String(assistantMessageId) : undefined);
205
+ const replyModel = resolveReplyModel(modelFromHeader, resolvedSessionId, model);
206
+ const turnActivity = buildTurnActivity(accumulator, replyModel);
207
+ reply.complete(displayText, counters.textSequence, attachments.length > 0 ? attachments : undefined, assistantMessageId !== undefined ? String(assistantMessageId) : undefined, turnActivity);
267
208
  await linkTurnToConvex(meta, resolvedSessionId);
268
209
  return { sessionId: resolvedSessionId };
269
210
  }
211
+ /**
212
+ * Resolves which model produced the reply, in priority order:
213
+ * 1. The `X-Hermes-Model` response header.
214
+ * 2. The session's recorded model in Hermes `state.db`.
215
+ * 3. The model from the outbound request (skipping the generic placeholder alias).
216
+ */
217
+ function resolveReplyModel(modelFromHeader, resolvedSessionId, requestModel) {
218
+ if (modelFromHeader)
219
+ return modelFromHeader;
220
+ if (hermesStateDbExists()) {
221
+ try {
222
+ const usage = getSessionUsage(resolvedSessionId);
223
+ if (usage.model)
224
+ return usage.model;
225
+ }
226
+ catch {
227
+ // Fall through to request model.
228
+ }
229
+ }
230
+ return requestModel && requestModel !== "hermes-agent" ? requestModel : undefined;
231
+ }
270
232
  export function createHermesMessageHandler(options = {}) {
271
233
  return async (content, meta, reply) => {
272
234
  await forwardToHermes(content, meta, reply, {
@@ -0,0 +1,7 @@
1
+ export type RenameHermesSessionResult = {
2
+ ok: true;
3
+ sessionId: string;
4
+ title: string;
5
+ };
6
+ export declare function renameHermesSession(sessionId: string, title: string): Promise<RenameHermesSessionResult>;
7
+ //# sourceMappingURL=renameHermesSession.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"renameHermesSession.d.ts","sourceRoot":"","sources":["../src/renameHermesSession.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,yBAAyB,GAAG;IACtC,EAAE,EAAE,IAAI,CAAC;IACT,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAgBF,wBAAsB,mBAAmB,CACvC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,yBAAyB,CAAC,CAmBpC"}
@@ -0,0 +1,39 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ const execFileAsync = promisify(execFile);
4
+ const RENAME_TIMEOUT_MS = 30_000;
5
+ function execFailureMessage(error) {
6
+ if (error && typeof error === "object") {
7
+ const record = error;
8
+ const stderr = record.stderr?.trim();
9
+ if (stderr)
10
+ return stderr;
11
+ const stdout = record.stdout?.trim();
12
+ if (stdout)
13
+ return stdout;
14
+ if (typeof record.message === "string" && record.message.trim()) {
15
+ return record.message.trim();
16
+ }
17
+ }
18
+ return "Could not rename Hermes session.";
19
+ }
20
+ export async function renameHermesSession(sessionId, title) {
21
+ const trimmedId = sessionId.trim();
22
+ const trimmedTitle = title.trim().slice(0, 120);
23
+ if (!trimmedId) {
24
+ throw new Error('Missing or invalid "sessionId"');
25
+ }
26
+ if (!trimmedTitle) {
27
+ throw new Error('Missing or invalid "title"');
28
+ }
29
+ try {
30
+ await execFileAsync("hermes", ["sessions", "rename", trimmedId, trimmedTitle], {
31
+ timeout: RENAME_TIMEOUT_MS,
32
+ env: process.env,
33
+ });
34
+ return { ok: true, sessionId: trimmedId, title: trimmedTitle };
35
+ }
36
+ catch (error) {
37
+ throw new Error(execFailureMessage(error));
38
+ }
39
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=renameHermesSession.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"renameHermesSession.test.d.ts","sourceRoot":"","sources":["../src/renameHermesSession.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,15 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { renameHermesSession } from "./renameHermesSession.js";
4
+ test("renameHermesSession rejects empty session id", async () => {
5
+ await assert.rejects(() => renameHermesSession(" ", "My session"), (error) => {
6
+ assert.match(String(error), /sessionId/i);
7
+ return true;
8
+ });
9
+ });
10
+ test("renameHermesSession rejects empty title", async () => {
11
+ await assert.rejects(() => renameHermesSession("20250305_091523_a1b2c3", " "), (error) => {
12
+ assert.match(String(error), /title/i);
13
+ return true;
14
+ });
15
+ });
@@ -0,0 +1,36 @@
1
+ import type { AgentReply, AgentTurnActivity } from "./client.js";
2
+ export type StreamCounters = {
3
+ textSequence: number;
4
+ activitySequence: number;
5
+ };
6
+ export type TurnActivityAccumulator = {
7
+ tools: Map<string, {
8
+ name: string;
9
+ label?: string;
10
+ emoji?: string;
11
+ count: number;
12
+ source?: string;
13
+ }>;
14
+ skills: Map<string, {
15
+ name: string;
16
+ label?: string;
17
+ }>;
18
+ memories: Map<string, {
19
+ name: string;
20
+ label?: string;
21
+ }>;
22
+ mcps: Map<string, {
23
+ server: string;
24
+ tool?: string;
25
+ label?: string;
26
+ count: number;
27
+ }>;
28
+ };
29
+ export declare function createTurnActivityAccumulator(): TurnActivityAccumulator;
30
+ export declare function buildTurnActivity(acc: TurnActivityAccumulator, model?: string): AgentTurnActivity;
31
+ export declare function parseSseDataLines(buffer: string): {
32
+ events: string[];
33
+ rest: string;
34
+ };
35
+ export declare function processSseEventBlock(eventBlock: string, reply: AgentReply, counters: StreamCounters, accumulator: TurnActivityAccumulator, onTextDelta: (delta: string) => void): void;
36
+ //# sourceMappingURL=turnActivity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"turnActivity.d.ts","sourceRoot":"","sources":["../src/turnActivity.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAEjE,MAAM,MAAM,cAAc,GAAG;IAC3B,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,MAAM,CAAC;CAC1B,CAAC;AAgCF,MAAM,MAAM,uBAAuB,GAAG;IACpC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACrG,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACtD,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACxD,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACrF,CAAC;AAEF,wBAAgB,6BAA6B,IAAI,uBAAuB,CAEvE;AAmCD,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,uBAAuB,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,iBAAiB,CA6CjG;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG;IAAE,MAAM,EAAE,MAAM,EAAE,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAIpF;AAiBD,wBAAgB,oBAAoB,CAClC,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE,UAAU,EACjB,QAAQ,EAAE,cAAc,EACxB,WAAW,EAAE,uBAAuB,EACpC,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,GACnC,IAAI,CA4IN"}
@@ -0,0 +1,233 @@
1
+ /** Caps how many distinct items per category are persisted on a message summary. */
2
+ const MAX_TURN_ACTIVITY_ITEMS = 20;
3
+ export function createTurnActivityAccumulator() {
4
+ return { tools: new Map(), skills: new Map(), memories: new Map(), mcps: new Map() };
5
+ }
6
+ function recordToolUse(acc, name, label, emoji, source) {
7
+ const existing = acc.tools.get(name);
8
+ if (existing) {
9
+ existing.count += 1;
10
+ if (!existing.label && label)
11
+ existing.label = label;
12
+ if (!existing.emoji && emoji)
13
+ existing.emoji = emoji;
14
+ return;
15
+ }
16
+ acc.tools.set(name, { name, label, emoji, count: 1, source });
17
+ }
18
+ function recordMcpUse(acc, server, tool, label) {
19
+ const key = `${server}\u0000${tool ?? ""}`;
20
+ const existing = acc.mcps.get(key);
21
+ if (existing) {
22
+ existing.count += 1;
23
+ if (!existing.label && label)
24
+ existing.label = label;
25
+ return;
26
+ }
27
+ acc.mcps.set(key, { server, tool, label, count: 1 });
28
+ }
29
+ export function buildTurnActivity(acc, model) {
30
+ const activity = {};
31
+ const tools = [...acc.tools.values()].slice(0, MAX_TURN_ACTIVITY_ITEMS);
32
+ if (tools.length > 0) {
33
+ activity.tools = tools.map((tool) => ({
34
+ name: tool.name,
35
+ ...(tool.label ? { label: tool.label } : {}),
36
+ ...(tool.emoji ? { emoji: tool.emoji } : {}),
37
+ ...(tool.count > 1 ? { count: tool.count } : {}),
38
+ ...(tool.source ? { source: tool.source } : {}),
39
+ }));
40
+ }
41
+ const skills = [...acc.skills.values()].slice(0, MAX_TURN_ACTIVITY_ITEMS);
42
+ if (skills.length > 0) {
43
+ activity.skills = skills.map((skill) => ({
44
+ name: skill.name,
45
+ ...(skill.label ? { label: skill.label } : {}),
46
+ }));
47
+ }
48
+ const memories = [...acc.memories.values()].slice(0, MAX_TURN_ACTIVITY_ITEMS);
49
+ if (memories.length > 0) {
50
+ activity.memories = memories.map((memory) => ({
51
+ name: memory.name,
52
+ ...(memory.label ? { label: memory.label } : {}),
53
+ }));
54
+ }
55
+ const mcps = [...acc.mcps.values()].slice(0, MAX_TURN_ACTIVITY_ITEMS);
56
+ if (mcps.length > 0) {
57
+ activity.mcps = mcps.map((mcp) => ({
58
+ server: mcp.server,
59
+ ...(mcp.tool ? { tool: mcp.tool } : {}),
60
+ ...(mcp.label ? { label: mcp.label } : {}),
61
+ ...(mcp.count > 1 ? { count: mcp.count } : {}),
62
+ }));
63
+ }
64
+ if (model) {
65
+ activity.model = model;
66
+ }
67
+ return activity;
68
+ }
69
+ export function parseSseDataLines(buffer) {
70
+ const parts = buffer.split("\n\n");
71
+ const rest = parts.pop() ?? "";
72
+ return { events: parts, rest };
73
+ }
74
+ function extractDeltaFromChunk(payload) {
75
+ const choices = payload.choices;
76
+ if (!Array.isArray(choices) || choices.length === 0)
77
+ return null;
78
+ const choice = choices[0];
79
+ if (!choice || typeof choice !== "object")
80
+ return null;
81
+ const delta = choice.delta;
82
+ if (!delta || typeof delta !== "object")
83
+ return null;
84
+ const content = delta.content;
85
+ return typeof content === "string" ? content : null;
86
+ }
87
+ function asString(value) {
88
+ return typeof value === "string" && value.trim().length > 0 ? value : undefined;
89
+ }
90
+ export function processSseEventBlock(eventBlock, reply, counters, accumulator, onTextDelta) {
91
+ let eventName = "message";
92
+ for (const line of eventBlock.split("\n")) {
93
+ if (line.startsWith("event:")) {
94
+ eventName = line.slice(6).trim();
95
+ continue;
96
+ }
97
+ if (!line.startsWith("data:"))
98
+ continue;
99
+ const data = line.slice(5).trim();
100
+ if (!data || data === "[DONE]")
101
+ continue;
102
+ if (eventName === "hermes.tool.progress") {
103
+ try {
104
+ const payload = JSON.parse(data);
105
+ const tool = asString(payload.tool);
106
+ const label = asString(payload.label);
107
+ const emoji = asString(payload.emoji);
108
+ const source = asString(payload.source);
109
+ const server = asString(payload.server);
110
+ // MCP tool calls may arrive as tool progress; bucket them as MCP usage.
111
+ if (source === "mcp" || server) {
112
+ const mcpServer = server ?? "mcp";
113
+ recordMcpUse(accumulator, mcpServer, tool, label);
114
+ reply.activity({
115
+ activityType: "tool.started",
116
+ kind: "mcp",
117
+ tool,
118
+ server: mcpServer,
119
+ source: "mcp",
120
+ label,
121
+ emoji,
122
+ sequence: counters.activitySequence,
123
+ });
124
+ }
125
+ else {
126
+ if (tool)
127
+ recordToolUse(accumulator, tool, label, emoji, source);
128
+ reply.activity({
129
+ activityType: "tool.started",
130
+ kind: "tool",
131
+ tool,
132
+ source,
133
+ label,
134
+ emoji,
135
+ sequence: counters.activitySequence,
136
+ });
137
+ }
138
+ counters.activitySequence += 1;
139
+ }
140
+ catch {
141
+ // Ignore malformed tool progress payloads.
142
+ }
143
+ continue;
144
+ }
145
+ if (eventName === "hermes.skill.progress") {
146
+ try {
147
+ const payload = JSON.parse(data);
148
+ const skill = asString(payload.skill);
149
+ const label = asString(payload.label);
150
+ const emoji = asString(payload.emoji);
151
+ if (skill && !accumulator.skills.has(skill)) {
152
+ accumulator.skills.set(skill, { name: skill, label });
153
+ }
154
+ reply.activity({
155
+ activityType: "tool.started",
156
+ kind: "skill",
157
+ skill,
158
+ label,
159
+ emoji,
160
+ sequence: counters.activitySequence,
161
+ });
162
+ counters.activitySequence += 1;
163
+ }
164
+ catch {
165
+ // Ignore malformed skill progress payloads.
166
+ }
167
+ continue;
168
+ }
169
+ if (eventName === "hermes.memory.progress") {
170
+ try {
171
+ const payload = JSON.parse(data);
172
+ const memory = asString(payload.memory);
173
+ const label = asString(payload.label);
174
+ const emoji = asString(payload.emoji);
175
+ if (memory && !accumulator.memories.has(memory)) {
176
+ accumulator.memories.set(memory, { name: memory, label });
177
+ }
178
+ reply.activity({
179
+ activityType: "tool.started",
180
+ kind: "memory",
181
+ memory,
182
+ label,
183
+ emoji,
184
+ sequence: counters.activitySequence,
185
+ });
186
+ counters.activitySequence += 1;
187
+ }
188
+ catch {
189
+ // Ignore malformed memory progress payloads.
190
+ }
191
+ continue;
192
+ }
193
+ if (eventName === "hermes.mcp.progress") {
194
+ try {
195
+ const payload = JSON.parse(data);
196
+ const server = asString(payload.server);
197
+ const tool = asString(payload.tool);
198
+ const label = asString(payload.label);
199
+ const emoji = asString(payload.emoji);
200
+ if (server) {
201
+ recordMcpUse(accumulator, server, tool, label);
202
+ reply.activity({
203
+ activityType: "tool.started",
204
+ kind: "mcp",
205
+ server,
206
+ tool,
207
+ source: "mcp",
208
+ label,
209
+ emoji,
210
+ sequence: counters.activitySequence,
211
+ });
212
+ counters.activitySequence += 1;
213
+ }
214
+ }
215
+ catch {
216
+ // Ignore malformed MCP progress payloads.
217
+ }
218
+ continue;
219
+ }
220
+ try {
221
+ const payload = JSON.parse(data);
222
+ const delta = extractDeltaFromChunk(payload);
223
+ if (delta && delta.trim()) {
224
+ onTextDelta(delta);
225
+ reply.delta(delta, counters.textSequence);
226
+ counters.textSequence += 1;
227
+ }
228
+ }
229
+ catch {
230
+ // Ignore malformed SSE chunks.
231
+ }
232
+ }
233
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=turnActivity.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"turnActivity.test.d.ts","sourceRoot":"","sources":["../src/turnActivity.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,69 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { buildTurnActivity, createTurnActivityAccumulator, processSseEventBlock, } from "./turnActivity.js";
4
+ function collectingReply() {
5
+ const activities = [];
6
+ const deltas = [];
7
+ const reply = {
8
+ delta: (text) => {
9
+ deltas.push(text);
10
+ },
11
+ activity: (activity) => {
12
+ activities.push(activity);
13
+ },
14
+ complete: () => { },
15
+ failed: () => { },
16
+ };
17
+ return { reply, activities, deltas };
18
+ }
19
+ function sseEvent(event, data) {
20
+ return `event: ${event}\ndata: ${JSON.stringify(data)}`;
21
+ }
22
+ test("accumulates and dedupes tool usage with counts", () => {
23
+ const { reply, activities } = collectingReply();
24
+ const accumulator = createTurnActivityAccumulator();
25
+ const counters = { textSequence: 0, activitySequence: 0 };
26
+ processSseEventBlock(sseEvent("hermes.tool.progress", { tool: "web_search", label: "Searching", emoji: "🔍" }), reply, counters, accumulator, () => { });
27
+ processSseEventBlock(sseEvent("hermes.tool.progress", { tool: "web_search" }), reply, counters, accumulator, () => { });
28
+ const activity = buildTurnActivity(accumulator);
29
+ assert.deepEqual(activity.tools, [
30
+ { name: "web_search", label: "Searching", emoji: "🔍", count: 2 },
31
+ ]);
32
+ assert.equal(activities.length, 2);
33
+ assert.equal(activities[0]?.kind, "tool");
34
+ });
35
+ test("buckets MCP tool progress into mcps, not tools", () => {
36
+ const { reply } = collectingReply();
37
+ const accumulator = createTurnActivityAccumulator();
38
+ const counters = { textSequence: 0, activitySequence: 0 };
39
+ processSseEventBlock(sseEvent("hermes.tool.progress", { tool: "search", source: "mcp", server: "context7" }), reply, counters, accumulator, () => { });
40
+ processSseEventBlock(sseEvent("hermes.mcp.progress", { server: "context7", tool: "search" }), reply, counters, accumulator, () => { });
41
+ const activity = buildTurnActivity(accumulator);
42
+ assert.equal(activity.tools, undefined);
43
+ assert.deepEqual(activity.mcps, [{ server: "context7", tool: "search", count: 2 }]);
44
+ });
45
+ test("collects skills and memories uniquely", () => {
46
+ const { reply } = collectingReply();
47
+ const accumulator = createTurnActivityAccumulator();
48
+ const counters = { textSequence: 0, activitySequence: 0 };
49
+ processSseEventBlock(sseEvent("hermes.skill.progress", { skill: "frontend-design", label: "Frontend design" }), reply, counters, accumulator, () => { });
50
+ processSseEventBlock(sseEvent("hermes.skill.progress", { skill: "frontend-design" }), reply, counters, accumulator, () => { });
51
+ processSseEventBlock(sseEvent("hermes.memory.progress", { memory: "USER.md" }), reply, counters, accumulator, () => { });
52
+ const activity = buildTurnActivity(accumulator, "anthropic/claude-sonnet-4");
53
+ assert.deepEqual(activity.skills, [{ name: "frontend-design", label: "Frontend design" }]);
54
+ assert.deepEqual(activity.memories, [{ name: "USER.md" }]);
55
+ assert.equal(activity.model, "anthropic/claude-sonnet-4");
56
+ });
57
+ test("text deltas are forwarded and not treated as activity", () => {
58
+ const { reply, deltas, activities } = collectingReply();
59
+ const accumulator = createTurnActivityAccumulator();
60
+ const counters = { textSequence: 0, activitySequence: 0 };
61
+ let captured = "";
62
+ processSseEventBlock(`data: ${JSON.stringify({ choices: [{ delta: { content: "Hello" } }] })}`, reply, counters, accumulator, (delta) => {
63
+ captured += delta;
64
+ });
65
+ assert.equal(captured, "Hello");
66
+ assert.deepEqual(deltas, ["Hello"]);
67
+ assert.equal(activities.length, 0);
68
+ assert.equal(buildTurnActivity(accumulator).model, undefined);
69
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saleso.innovations/bridge",
3
- "version": "0.1.38",
3
+ "version": "0.1.40",
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/hermesSessionDb.test.ts src/shellSession.test.ts src/bridgeVersion.test.ts"
39
+ "test": "node --import tsx --test src/hermesFiles.test.ts src/hermesSessionDb.test.ts src/renameHermesSession.test.ts src/shellSession.test.ts src/bridgeVersion.test.ts src/turnActivity.test.ts"
40
40
  },
41
41
  "dependencies": {
42
42
  "better-sqlite3": "^11.10.0",