@cotal-ai/connector-hermes 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/LICENSE +202 -0
  2. package/bin/install.mjs +99 -0
  3. package/dist/bridge.d.ts +7 -0
  4. package/dist/bridge.d.ts.map +1 -0
  5. package/dist/bridge.js +199 -0
  6. package/dist/bridge.js.map +1 -0
  7. package/dist/extension.d.ts +13 -0
  8. package/dist/extension.d.ts.map +1 -0
  9. package/dist/extension.js +49 -0
  10. package/dist/extension.js.map +1 -0
  11. package/dist/hermes-hooks.d.ts +15 -0
  12. package/dist/hermes-hooks.d.ts.map +1 -0
  13. package/dist/hermes-hooks.js +51 -0
  14. package/dist/hermes-hooks.js.map +1 -0
  15. package/dist/index.d.ts +2 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +2 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/launch.d.ts +2 -0
  20. package/dist/launch.d.ts.map +1 -0
  21. package/dist/launch.js +149 -0
  22. package/dist/launch.js.map +1 -0
  23. package/dist/sidecar.d.ts +10 -0
  24. package/dist/sidecar.d.ts.map +1 -0
  25. package/dist/sidecar.js +60 -0
  26. package/dist/sidecar.js.map +1 -0
  27. package/dist/standalone.d.ts +2 -0
  28. package/dist/standalone.d.ts.map +1 -0
  29. package/dist/standalone.js +31 -0
  30. package/dist/standalone.js.map +1 -0
  31. package/dist/tool-schema.d.ts +10 -0
  32. package/dist/tool-schema.d.ts.map +1 -0
  33. package/dist/tool-schema.js +34 -0
  34. package/dist/tool-schema.js.map +1 -0
  35. package/package.json +51 -0
  36. package/plugin/cotal/__init__.py +148 -0
  37. package/plugin/cotal/_sidecar/standalone.cjs +33188 -0
  38. package/plugin/cotal/adapter.py +114 -0
  39. package/plugin/cotal/bridge_client.py +153 -0
  40. package/plugin/cotal/hooks.py +74 -0
  41. package/plugin/cotal/plugin.yaml +10 -0
  42. package/plugin/cotal/tools.py +62 -0
  43. package/pyproject.toml +19 -0
@@ -0,0 +1,51 @@
1
+ /** Last tool the turn started — so `waiting` (approval) and `working` can name what it's doing. */
2
+ let pendingTool;
3
+ /** A short, human-readable preview of a tool call: "name: most-salient-input". */
4
+ function toolDetail(name, input) {
5
+ if (typeof name !== "string" || !name)
6
+ return undefined;
7
+ const i = (input ?? {});
8
+ const salient = i.command ?? i.file_path ?? i.path ?? i.url ?? i.query ?? i.description;
9
+ let detail = typeof salient === "string" && salient ? `${name}: ${salient}` : name;
10
+ if (detail.length > 200)
11
+ detail = `${detail.slice(0, 199)}…`;
12
+ return detail;
13
+ }
14
+ export const hermesHookHandle = async (agent, ev) => {
15
+ const event = String(ev.hook_event_name ?? "");
16
+ try {
17
+ switch (event) {
18
+ case "gateway_startup": // gateway up / adapter connected — present and free
19
+ case "on_session_start":
20
+ await agent.setStatus("idle");
21
+ return {};
22
+ case "pre_llm_call": // a turn is running the model
23
+ await agent.setStatus("working", pendingTool);
24
+ return {};
25
+ case "pre_tool_call": // record what it's about to run
26
+ pendingTool = toolDetail(ev.tool_name, ev.tool_input);
27
+ await agent.setStatus("working", pendingTool);
28
+ return {};
29
+ case "approval_wait": // Hermes asked for command approval — blocked on a human/peer
30
+ await agent.setStatus("waiting", typeof ev.detail === "string" ? ev.detail : pendingTool);
31
+ return {};
32
+ case "post_llm_call": // turn done
33
+ case "on_session_end":
34
+ pendingTool = undefined;
35
+ await agent.setStatus("idle");
36
+ // Now idle: if ambient messages were held while busy, ask the bridge to flush them.
37
+ if (agent.inboxCount() > 0)
38
+ agent.requestWake();
39
+ return {};
40
+ case "gateway_shutdown":
41
+ await agent.setStatus("offline");
42
+ return {};
43
+ default:
44
+ return {};
45
+ }
46
+ }
47
+ catch {
48
+ return {}; // never block the gateway
49
+ }
50
+ };
51
+ //# sourceMappingURL=hermes-hooks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hermes-hooks.js","sourceRoot":"","sources":["../src/hermes-hooks.ts"],"names":[],"mappings":"AAcA,mGAAmG;AACnG,IAAI,WAA+B,CAAC;AAEpC,kFAAkF;AAClF,SAAS,UAAU,CAAC,IAAa,EAAE,KAAc;IAC/C,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,CAAC,IAAI;QAAE,OAAO,SAAS,CAAC;IACxD,MAAM,CAAC,GAAG,CAAC,KAAK,IAAI,EAAE,CAA4B,CAAC;IACnD,MAAM,OAAO,GAAG,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,SAAS,IAAI,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,WAAW,CAAC;IACxF,IAAI,MAAM,GAAG,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,CAAC,CAAC,CAAC,GAAG,IAAI,KAAK,OAAO,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;IACnF,IAAI,MAAM,CAAC,MAAM,GAAG,GAAG;QAAE,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG,CAAC;IAC7D,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,CAAC,MAAM,gBAAgB,GAAe,KAAK,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE;IAC9D,MAAM,KAAK,GAAG,MAAM,CAAC,EAAE,CAAC,eAAe,IAAI,EAAE,CAAC,CAAC;IAC/C,IAAI,CAAC;QACH,QAAQ,KAAK,EAAE,CAAC;YACd,KAAK,iBAAiB,CAAC,CAAC,oDAAoD;YAC5E,KAAK,kBAAkB;gBACrB,MAAM,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;gBAC9B,OAAO,EAAE,CAAC;YACZ,KAAK,cAAc,EAAE,8BAA8B;gBACjD,MAAM,KAAK,CAAC,SAAS,CAAC,SAAS,EAAE,WAAW,CAAC,CAAC;gBAC9C,OAAO,EAAE,CAAC;YACZ,KAAK,eAAe,EAAE,gCAAgC;gBACpD,WAAW,GAAG,UAAU,CAAC,EAAE,CAAC,SAAS,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC;gBACtD,MAAM,KAAK,CAAC,SAAS,CAAC,SAAS,EAAE,WAAW,CAAC,CAAC;gBAC9C,OAAO,EAAE,CAAC;YACZ,KAAK,eAAe,EAAE,8DAA8D;gBAClF,MAAM,KAAK,CAAC,SAAS,CAAC,SAAS,EAAE,OAAO,EAAE,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC;gBAC1F,OAAO,EAAE,CAAC;YACZ,KAAK,eAAe,CAAC,CAAC,YAAY;YAClC,KAAK,gBAAgB;gBACnB,WAAW,GAAG,SAAS,CAAC;gBACxB,MAAM,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;gBAC9B,oFAAoF;gBACpF,IAAI,KAAK,CAAC,UAAU,EAAE,GAAG,CAAC;oBAAE,KAAK,CAAC,WAAW,EAAE,CAAC;gBAChD,OAAO,EAAE,CAAC;YACZ,KAAK,kBAAkB;gBACrB,MAAM,KAAK,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;gBACjC,OAAO,EAAE,CAAC;YACZ;gBACE,OAAO,EAAE,CAAC;QACd,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC,CAAC,0BAA0B;IACvC,CAAC;AACH,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export * from "./extension.js";
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export * from "./extension.js"; // self-registers the `hermes` connector on import
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC,CAAC,kDAAkD"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=launch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"launch.d.ts","sourceRoot":"","sources":["../src/launch.ts"],"names":[],"mappings":""}
package/dist/launch.js ADDED
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Hermes launcher / supervisor — the `hermes` connector's command (run from source via tsx).
3
+ *
4
+ * Hermes is a long-lived gateway daemon that creates a fresh `AIAgent` per inbound message, so the
5
+ * mesh endpoint must outlive every turn — it can't ride inside a per-turn MCP server the way it
6
+ * does for Claude Code / Codex. This process OWNS the single {@link MeshAgent} (via the shared
7
+ * {@link startSidecar}) and supervises `hermes gateway run` as its child:
8
+ *
9
+ * - connector-core **control socket** ← Python presence hooks (relay.ts pattern) → presence
10
+ * - **bridge socket** ⇄ Python gateway adapter + cotal_* tools (inbound wake/drive, outbound)
11
+ * - **tools file** → the cotal_* descriptors the plugin registers at load
12
+ * - an isolated **HERMES_HOME** profile so the operator's own ~/.hermes is never touched
13
+ *
14
+ * The manager runs this in a PTY; stdio is inherited so the gateway's output is what you attach to.
15
+ */
16
+ import { spawn, execFileSync } from "node:child_process";
17
+ import { mkdirSync, writeFileSync, cpSync, rmSync } from "node:fs";
18
+ import { tmpdir } from "node:os";
19
+ import { join } from "node:path";
20
+ import { fileURLToPath } from "node:url";
21
+ import { loadAgentFile } from "@cotal-ai/core";
22
+ import { hasIdentity, configFromEnv, controlSocketPath } from "@cotal-ai/connector-core";
23
+ import { startSidecar } from "./sidecar.js";
24
+ /** Hermes API line this connector is written + pinned against (see pyproject.toml). A different
25
+ * major.minor may move the plugin/platform/hook signatures, so we assert and fail loudly. */
26
+ const HERMES_PIN = "0.16";
27
+ const ILLEGAL = /[^A-Za-z0-9_-]/g;
28
+ const tok = (s) => s.trim().replace(ILLEGAL, "_").slice(0, 40) || "_";
29
+ /** This package's root (where pyproject.toml + plugin/ live), resolved from this source file. */
30
+ const PKG_DIR = fileURLToPath(new URL("..", import.meta.url));
31
+ const PLUGIN_SRC = join(PKG_DIR, "plugin", "cotal");
32
+ function bridgeSocketPath(space, name) {
33
+ return join(tmpdir(), `cotal-hermes-bridge-${tok(space)}-${tok(name)}.sock`);
34
+ }
35
+ function log(msg) {
36
+ process.stderr.write(`[cotal-hermes] ${msg}\n`);
37
+ }
38
+ /** A double-quoted YAML basic-string literal (escaped). */
39
+ const yamlStr = (s) => `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
40
+ /**
41
+ * Build the isolated Hermes profile (HERMES_HOME) so the operator's ~/.hermes is never touched.
42
+ * Drops the cotal plugin into the profile's plugins dir, enables it + the cotal platform, and
43
+ * turns approvals off (an autonomous spawned gateway has no human at the TUI to approve commands).
44
+ */
45
+ function setupProfile(home, opts) {
46
+ mkdirSync(home, { recursive: true });
47
+ const pluginDst = join(home, "plugins", "cotal");
48
+ rmSync(pluginDst, { recursive: true, force: true });
49
+ mkdirSync(join(home, "plugins"), { recursive: true });
50
+ cpSync(PLUGIN_SRC, pluginDst, { recursive: true });
51
+ const lines = [
52
+ "# Cotal-managed Hermes profile — regenerated each launch; do not edit.",
53
+ "plugins:",
54
+ " enabled: [cotal]",
55
+ "gateway:",
56
+ " platforms:",
57
+ " cotal:",
58
+ " enabled: true",
59
+ "approvals:",
60
+ " mode: off",
61
+ ];
62
+ if (opts.model)
63
+ lines.push(`model: ${yamlStr(opts.model)}`);
64
+ writeFileSync(join(home, "config.yaml"), lines.join("\n") + "\n");
65
+ // Persona → SOUL.md (Hermes' identity file) — the one place a system prompt can be set.
66
+ if (opts.persona)
67
+ writeFileSync(join(home, "SOUL.md"), opts.persona.trim() + "\n");
68
+ }
69
+ /** Assert the installed hermes-agent is on the pinned API line, or throw. No silent degrade: a
70
+ * different major.minor can shift the plugin/platform/hook contract this connector depends on. */
71
+ function assertHermesVersion() {
72
+ let raw;
73
+ try {
74
+ raw = execFileSync("uv", ["run", "--project", PKG_DIR, "--quiet", "python", "-c", "from importlib.metadata import version; print(version('hermes-agent'))"], { encoding: "utf8" }).trim();
75
+ }
76
+ catch (e) {
77
+ throw new Error(`could not resolve the hermes-agent version via uv — is uv installed and hermes-agent available? (${e.message})`);
78
+ }
79
+ const line = raw.split(".").slice(0, 2).join(".");
80
+ if (line !== HERMES_PIN)
81
+ throw new Error(`hermes-agent ${raw} is not on the pinned ${HERMES_PIN} line this connector targets — pin ${HERMES_PIN}.x or update src/launch.ts + pyproject.toml together`);
82
+ log(`hermes-agent ${raw} (pinned line ${HERMES_PIN}) ✓`);
83
+ }
84
+ async function main() {
85
+ // No identity → a plain run, not a launcher-spawned agent. Stay off the mesh.
86
+ if (!hasIdentity()) {
87
+ log("no COTAL_NAME — not a managed session; staying off the mesh");
88
+ process.exit(0);
89
+ }
90
+ const config = configFromEnv();
91
+ const home = join(tmpdir(), `cotal-hermes-${tok(config.space)}-${tok(config.name)}`);
92
+ const persona = process.env.COTAL_AGENT_FILE
93
+ ? loadAgentFile(process.env.COTAL_AGENT_FILE).persona
94
+ : undefined;
95
+ setupProfile(home, { model: process.env.HERMES_MODEL, persona });
96
+ // Paths shared by the sidecar and the gateway child — set in our env so startSidecar reads
97
+ // them, and forwarded verbatim to the child so the plugin connects to the same sockets/file.
98
+ const controlSock = controlSocketPath(config.space, config.name);
99
+ const bridgeSock = bridgeSocketPath(config.space, config.name);
100
+ const toolsFile = join(home, "cotal-tools.json");
101
+ process.env.COTAL_CONTROL_SOCKET = controlSock;
102
+ process.env.COTAL_BRIDGE_SOCKET = bridgeSock;
103
+ process.env.COTAL_TOOLS_FILE = toolsFile;
104
+ const sidecar = startSidecar();
105
+ // Fail loudly before we hand control to the gateway if the Hermes API line is wrong.
106
+ assertHermesVersion();
107
+ const childEnv = {
108
+ ...process.env,
109
+ HERMES_HOME: home,
110
+ COTAL_CONTROL_SOCKET: controlSock,
111
+ COTAL_BRIDGE_SOCKET: bridgeSock,
112
+ COTAL_TOOLS_FILE: toolsFile,
113
+ };
114
+ log(`launching hermes gateway as ${config.name}${config.role ? `/${config.role}` : ""} (HERMES_HOME=${home})`);
115
+ const child = spawn("uv", ["run", "--project", PKG_DIR, "hermes", "gateway", "run"], {
116
+ env: childEnv,
117
+ stdio: "inherit",
118
+ });
119
+ let shuttingDown = false;
120
+ const shutdown = async (code) => {
121
+ if (shuttingDown)
122
+ return;
123
+ shuttingDown = true;
124
+ try {
125
+ child.kill("SIGTERM");
126
+ }
127
+ catch {
128
+ /* ignore */
129
+ }
130
+ try {
131
+ await sidecar.stop();
132
+ }
133
+ finally {
134
+ process.exit(code);
135
+ }
136
+ };
137
+ child.on("exit", (code) => void shutdown(code ?? 0));
138
+ child.on("error", (e) => {
139
+ log(`failed to launch hermes gateway: ${e.message} — is uv (and hermes-agent) available?`);
140
+ void shutdown(1);
141
+ });
142
+ process.on("SIGINT", () => void shutdown(0));
143
+ process.on("SIGTERM", () => void shutdown(0));
144
+ }
145
+ main().catch((e) => {
146
+ log(`fatal: ${e.stack ?? String(e)}`);
147
+ process.exit(1);
148
+ });
149
+ //# sourceMappingURL=launch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"launch.js","sourceRoot":"","sources":["../src/launch.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ import { MeshAgent, type AgentConfig } from "@cotal-ai/connector-core";
2
+ export interface Sidecar {
3
+ agent: MeshAgent;
4
+ config: AgentConfig;
5
+ stop(): Promise<void>;
6
+ }
7
+ /** Start the mesh agent, the control + bridge servers, and emit the tool descriptors file.
8
+ * Reads `COTAL_CONTROL_SOCKET`, `COTAL_BRIDGE_SOCKET`, and `COTAL_TOOLS_FILE` from the env. */
9
+ export declare function startSidecar(): Sidecar;
10
+ //# sourceMappingURL=sidecar.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sidecar.d.ts","sourceRoot":"","sources":["../src/sidecar.ts"],"names":[],"mappings":"AAgBA,OAAO,EAEL,SAAS,EAET,KAAK,WAAW,EACjB,MAAM,0BAA0B,CAAC;AAKlC,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,SAAS,CAAC;IACjB,MAAM,EAAE,WAAW,CAAC;IACpB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAQD;gGACgG;AAChG,wBAAgB,YAAY,IAAI,OAAO,CAgCtC"}
@@ -0,0 +1,60 @@
1
+ /**
2
+ * The Cotal mesh sidecar — the long-lived TypeScript half of the Hermes connector.
3
+ *
4
+ * It owns the single {@link MeshAgent} for the gateway's whole life (NATS, presence, the
5
+ * stream-backed inbox, attention modes, manager control) and exposes two local sockets the
6
+ * in-gateway Python plugin connects to:
7
+ * - the connector-core **control socket** ← presence hooks (relay.ts pattern),
8
+ * - the **bridge socket** ⇄ inbound push / outbound replies / cotal_* tool calls (bridge.ts).
9
+ * It also writes the generated tool descriptors to **`COTAL_TOOLS_FILE`** so the plugin can
10
+ * register the `cotal_*` tools synchronously at load, without blocking on the bridge.
11
+ *
12
+ * Both entrypoints reuse this: `launch.ts` (managed — also spawns `hermes gateway run` as a child)
13
+ * and `standalone.ts` (a user's own gateway spawns the bundled sidecar itself). Whoever spawns the
14
+ * sidecar sets the three socket/file env vars; we throw if any is missing (no silent fallback).
15
+ */
16
+ import { writeFileSync } from "node:fs";
17
+ import { configFromEnv, MeshAgent, startControlServer, } from "@cotal-ai/connector-core";
18
+ import { hermesHookHandle } from "./hermes-hooks.js";
19
+ import { startBridgeServer } from "./bridge.js";
20
+ import { hermesToolDescriptors } from "./tool-schema.js";
21
+ function need(name) {
22
+ const v = process.env[name];
23
+ if (!v)
24
+ throw new Error(`${name} not set — the sidecar must be spawned with bridge/control/tools paths`);
25
+ return v;
26
+ }
27
+ /** Start the mesh agent, the control + bridge servers, and emit the tool descriptors file.
28
+ * Reads `COTAL_CONTROL_SOCKET`, `COTAL_BRIDGE_SOCKET`, and `COTAL_TOOLS_FILE` from the env. */
29
+ export function startSidecar() {
30
+ const config = configFromEnv();
31
+ const agent = new MeshAgent(config);
32
+ agent.start(); // background connect with retry
33
+ const controlSock = need("COTAL_CONTROL_SOCKET");
34
+ const bridgeSock = need("COTAL_BRIDGE_SOCKET");
35
+ const toolsFile = need("COTAL_TOOLS_FILE");
36
+ const controlServer = startControlServer(agent, controlSock, hermesHookHandle);
37
+ const bridge = startBridgeServer(agent, config, bridgeSock);
38
+ // The plugin reads this at register(ctx) time to declare the cotal_* tools (full shared parity).
39
+ writeFileSync(toolsFile, JSON.stringify(hermesToolDescriptors(config)));
40
+ return {
41
+ agent,
42
+ config,
43
+ async stop() {
44
+ try {
45
+ bridge.close();
46
+ }
47
+ catch {
48
+ /* ignore */
49
+ }
50
+ try {
51
+ controlServer.close();
52
+ }
53
+ catch {
54
+ /* ignore */
55
+ }
56
+ await agent.stop();
57
+ },
58
+ };
59
+ }
60
+ //# sourceMappingURL=sidecar.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sidecar.js","sourceRoot":"","sources":["../src/sidecar.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACxC,OAAO,EACL,aAAa,EACb,SAAS,EACT,kBAAkB,GAEnB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AAQzD,SAAS,IAAI,CAAC,IAAY;IACxB,MAAM,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC5B,IAAI,CAAC,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,GAAG,IAAI,wEAAwE,CAAC,CAAC;IACzG,OAAO,CAAC,CAAC;AACX,CAAC;AAED;gGACgG;AAChG,MAAM,UAAU,YAAY;IAC1B,MAAM,MAAM,GAAG,aAAa,EAAE,CAAC;IAC/B,MAAM,KAAK,GAAG,IAAI,SAAS,CAAC,MAAM,CAAC,CAAC;IACpC,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,gCAAgC;IAE/C,MAAM,WAAW,GAAG,IAAI,CAAC,sBAAsB,CAAC,CAAC;IACjD,MAAM,UAAU,GAAG,IAAI,CAAC,qBAAqB,CAAC,CAAC;IAC/C,MAAM,SAAS,GAAG,IAAI,CAAC,kBAAkB,CAAC,CAAC;IAE3C,MAAM,aAAa,GAAG,kBAAkB,CAAC,KAAK,EAAE,WAAW,EAAE,gBAAgB,CAAC,CAAC;IAC/E,MAAM,MAAM,GAAG,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,CAAC,CAAC;IAE5D,iGAAiG;IACjG,aAAa,CAAC,SAAS,EAAE,IAAI,CAAC,SAAS,CAAC,qBAAqB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IAExE,OAAO;QACL,KAAK;QACL,MAAM;QACN,KAAK,CAAC,IAAI;YACR,IAAI,CAAC;gBACH,MAAM,CAAC,KAAK,EAAE,CAAC;YACjB,CAAC;YAAC,MAAM,CAAC;gBACP,YAAY;YACd,CAAC;YACD,IAAI,CAAC;gBACH,aAAa,CAAC,KAAK,EAAE,CAAC;YACxB,CAAC;YAAC,MAAM,CAAC;gBACP,YAAY;YACd,CAAC;YACD,MAAM,KAAK,CAAC,IAAI,EAAE,CAAC;QACrB,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=standalone.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"standalone.d.ts","sourceRoot":"","sources":["../src/standalone.ts"],"names":[],"mappings":""}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Standalone sidecar entry — bundled (esbuild → dist/standalone.cjs) and spawned by the Python
3
+ * plugin when a user runs their OWN Hermes (`hermes plugins install cotal-ai/<repo> --enable`).
4
+ *
5
+ * In that mode there is no Cotal launcher: the enabled plugin sees COTAL_SPACE / COTAL_NAME /
6
+ * COTAL_SERVERS but no bridge socket, so it derives the socket/file paths, sets them in the
7
+ * sidecar's env, and spawns this. We just bring up the mesh + bridge + control + tools file and
8
+ * stay alive for the gateway's lifetime — the gateway itself is already running around us, so
9
+ * (unlike launch.ts) we never spawn a `hermes` child.
10
+ */
11
+ import { startSidecar } from "./sidecar.js";
12
+ function log(msg) {
13
+ process.stderr.write(`[cotal-hermes/standalone] ${msg}\n`);
14
+ }
15
+ const sidecar = startSidecar();
16
+ log(`mesh sidecar up for ${sidecar.config.name} in space ${sidecar.config.space}`);
17
+ let stopping = false;
18
+ const stop = async (code) => {
19
+ if (stopping)
20
+ return;
21
+ stopping = true;
22
+ try {
23
+ await sidecar.stop();
24
+ }
25
+ finally {
26
+ process.exit(code);
27
+ }
28
+ };
29
+ process.on("SIGINT", () => void stop(0));
30
+ process.on("SIGTERM", () => void stop(0));
31
+ //# sourceMappingURL=standalone.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"standalone.js","sourceRoot":"","sources":["../src/standalone.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAE5C,SAAS,GAAG,CAAC,GAAW;IACtB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,6BAA6B,GAAG,IAAI,CAAC,CAAC;AAC7D,CAAC;AAED,MAAM,OAAO,GAAG,YAAY,EAAE,CAAC;AAC/B,GAAG,CAAC,uBAAuB,OAAO,CAAC,MAAM,CAAC,IAAI,aAAa,OAAO,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC;AAEnF,IAAI,QAAQ,GAAG,KAAK,CAAC;AACrB,MAAM,IAAI,GAAG,KAAK,EAAE,IAAY,EAAiB,EAAE;IACjD,IAAI,QAAQ;QAAE,OAAO;IACrB,QAAQ,GAAG,IAAI,CAAC;IAChB,IAAI,CAAC;QACH,MAAM,OAAO,CAAC,IAAI,EAAE,CAAC;IACvB,CAAC;YAAS,CAAC;QACT,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACrB,CAAC;AACH,CAAC,CAAC;AAEF,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;AACzC,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC"}
@@ -0,0 +1,10 @@
1
+ import { type AgentConfig } from "@cotal-ai/connector-core";
2
+ /** A Hermes plugin tool: name + description + a JSON-Schema object for its parameters. */
3
+ export interface HermesToolDescriptor {
4
+ name: string;
5
+ description: string;
6
+ parameters: Record<string, unknown>;
7
+ }
8
+ /** Build the Hermes tool descriptors for a given agent config (rendered from the shared specs). */
9
+ export declare function hermesToolDescriptors(config: AgentConfig): HermesToolDescriptor[];
10
+ //# sourceMappingURL=tool-schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool-schema.d.ts","sourceRoot":"","sources":["../src/tool-schema.ts"],"names":[],"mappings":"AAaA,OAAO,EAAkB,KAAK,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAE5E,0FAA0F;AAC1F,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACrC;AAYD,mGAAmG;AACnG,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,WAAW,GAAG,oBAAoB,EAAE,CAUjF"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Render the shared Cotal tool surface as Hermes plugin-tool descriptors.
3
+ *
4
+ * One source of truth: {@link cotalToolSpecs} (connector-core). We do NOT hand-write the Hermes
5
+ * tool list — we generate `{name, description, parameters}` from each spec (Zod raw shape →
6
+ * JSON Schema via Zod 4's `toJSONSchema`) so a Hermes peer gets exactly the same `cotal_*`
7
+ * surface as Claude Code / OpenCode, and `parity.smoke.ts` fails if the two ever drift.
8
+ *
9
+ * The descriptors are written to a file the launcher hands the gateway (`COTAL_TOOLS_FILE`); the
10
+ * Python plugin reads it at `register(ctx)` time so tool registration stays synchronous and never
11
+ * has to block on the bridge. Tool *calls* still ride the bridge at runtime.
12
+ */
13
+ import { z } from "zod";
14
+ import { cotalToolSpecs } from "@cotal-ai/connector-core";
15
+ const EMPTY_PARAMS = { type: "object", properties: {}, required: [] };
16
+ /** cotal_inbox is read-only under a driven connector: this connector surfaces each batch into a
17
+ * turn and acks on completion, so the agent's inbox tool must PEEK, never drain (a drain would
18
+ * race the connector's ack). Mirror the OpenCode connector's read-only framing. */
19
+ const READONLY_INBOX_DESCRIPTION = "Show the peer messages currently waiting for you (incl. focus-mode recall). You don't normally " +
20
+ "need this — peer messages are delivered into your turns automatically; use it to re-check " +
21
+ "what's pending mid-task. Read-only: it never consumes them.";
22
+ /** Build the Hermes tool descriptors for a given agent config (rendered from the shared specs). */
23
+ export function hermesToolDescriptors(config) {
24
+ return cotalToolSpecs(config, "hermes").map((spec) => {
25
+ if (spec.name === "cotal_inbox") {
26
+ return { name: spec.name, description: READONLY_INBOX_DESCRIPTION, parameters: EMPTY_PARAMS };
27
+ }
28
+ const parameters = spec.schema
29
+ ? z.toJSONSchema(z.object(spec.schema))
30
+ : EMPTY_PARAMS;
31
+ return { name: spec.name, description: spec.description, parameters };
32
+ });
33
+ }
34
+ //# sourceMappingURL=tool-schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool-schema.js","sourceRoot":"","sources":["../src/tool-schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,cAAc,EAAoB,MAAM,0BAA0B,CAAC;AAS5E,MAAM,YAAY,GAA4B,EAAE,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC;AAE/F;;oFAEoF;AACpF,MAAM,0BAA0B,GAC9B,iGAAiG;IACjG,4FAA4F;IAC5F,6DAA6D,CAAC;AAEhE,mGAAmG;AACnG,MAAM,UAAU,qBAAqB,CAAC,MAAmB;IACvD,OAAO,cAAc,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;QACnD,IAAI,IAAI,CAAC,IAAI,KAAK,aAAa,EAAE,CAAC;YAChC,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,WAAW,EAAE,0BAA0B,EAAE,UAAU,EAAE,YAAY,EAAE,CAAC;QAChG,CAAC;QACD,MAAM,UAAU,GAAG,IAAI,CAAC,MAAM;YAC5B,CAAC,CAAE,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAA6B;YACpE,CAAC,CAAC,YAAY,CAAC;QACjB,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,UAAU,EAAE,CAAC;IACxE,CAAC,CAAC,CAAC;AACL,CAAC"}
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@cotal-ai/connector-hermes",
3
+ "version": "0.1.0",
4
+ "license": "Apache-2.0",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/Cotal-AI/Cotal.git",
8
+ "directory": "extensions/connector-hermes"
9
+ },
10
+ "type": "module",
11
+ "main": "./dist/index.js",
12
+ "types": "./dist/index.d.ts",
13
+ "bin": {
14
+ "connector-hermes": "./bin/install.mjs"
15
+ },
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "import": "./dist/index.js"
20
+ }
21
+ },
22
+ "dependencies": {
23
+ "tsx": "^4.22.4",
24
+ "zod": "^4.4.3",
25
+ "@cotal-ai/connector-core": "0.2.0"
26
+ },
27
+ "peerDependencies": {
28
+ "@cotal-ai/core": "0.1.3"
29
+ },
30
+ "devDependencies": {
31
+ "esbuild": "^0.28.0",
32
+ "@cotal-ai/core": "0.1.3"
33
+ },
34
+ "files": [
35
+ "bin",
36
+ "dist",
37
+ "plugin/cotal/*.py",
38
+ "plugin/cotal/plugin.yaml",
39
+ "plugin/cotal/_sidecar",
40
+ "pyproject.toml"
41
+ ],
42
+ "publishConfig": {
43
+ "access": "public"
44
+ },
45
+ "scripts": {
46
+ "typecheck": "tsc -p tsconfig.json --noEmit",
47
+ "test": "tsx tool-parity.smoke.ts",
48
+ "build": "tsc -p tsconfig.json && pnpm run bundle",
49
+ "bundle": "esbuild src/standalone.ts --bundle --platform=node --format=cjs --target=node20 --outfile=plugin/cotal/_sidecar/standalone.cjs"
50
+ }
51
+ }
@@ -0,0 +1,148 @@
1
+ """Cotal plugin for Hermes — joins the gateway to the Cotal mesh.
2
+
3
+ Registers three things on the Hermes plugin context:
4
+ - a **gateway platform adapter** (``cotal``) — inbound wake/drive + outbound reply routing,
5
+ - **lifecycle hooks** → Cotal presence (over connector-core's control socket),
6
+ - the **cotal_* tools** — proactive mesh actions for the agent (full shared parity).
7
+
8
+ All three talk to the TS sidecar (which owns the mesh endpoint) over the sockets the launcher set
9
+ in the environment. Two run modes:
10
+ - **Managed** — the Cotal launcher spawns this gateway with COTAL_BRIDGE_SOCKET (and the control
11
+ socket + tools file) already set; we just register against them.
12
+ - **Standalone** — a user's own ``hermes`` with the plugin installed: COTAL_SPACE / COTAL_NAME /
13
+ COTAL_SERVERS are set but no bridge socket, so we spawn the bundled sidecar ourselves, derive
14
+ the socket/file paths, and register against those.
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import os
19
+ import subprocess
20
+ import tempfile
21
+ import time
22
+ from pathlib import Path
23
+ from typing import Any
24
+
25
+ from . import hooks
26
+ from .tools import register_tools
27
+
28
+
29
+ def _is_managed() -> bool:
30
+ """Managed launches preset the bridge socket; its presence means the sidecar already exists."""
31
+ return bool(os.environ.get("COTAL_BRIDGE_SOCKET"))
32
+
33
+
34
+ def _have_identity() -> bool:
35
+ """Mirror core's hasIdentity: a name, a join link, or an agent file is the explicit opt-in.
36
+ A COTAL_LINK (cotal://token@host/space) carries space + server + auth on its own, so a peer
37
+ can join with just the link (+ an optional COTAL_NAME)."""
38
+ return bool(
39
+ os.environ.get("COTAL_NAME")
40
+ or os.environ.get("COTAL_LINK")
41
+ or os.environ.get("COTAL_AGENT_FILE")
42
+ )
43
+
44
+
45
+ def _check_requirements() -> bool:
46
+ """Enable the cotal platform when we can reach (or bootstrap) a sidecar: a preset bridge socket
47
+ (managed) or enough identity to spawn one (standalone)."""
48
+ return _is_managed() or _have_identity()
49
+
50
+
51
+ def _resolve_sidecar_js() -> Path:
52
+ """Locate the bundled standalone sidecar (esbuild → cotal/_sidecar/standalone.cjs). Honors an
53
+ explicit COTAL_SIDECAR_JS override; otherwise resolves it relative to THIS plugin dir. The
54
+ bundle ships inside the plugin dir on purpose, so it travels with the plugin in every layout
55
+ (monorepo, managed copy into HERMES_HOME, `hermes plugins install`, npm). Throws if absent —
56
+ no silent fallback."""
57
+ override = os.environ.get("COTAL_SIDECAR_JS")
58
+ if override:
59
+ p = Path(override)
60
+ if not p.is_file():
61
+ raise RuntimeError(f"COTAL_SIDECAR_JS={override} does not exist")
62
+ return p
63
+ candidate = Path(__file__).resolve().parent / "_sidecar" / "standalone.cjs"
64
+ if not candidate.is_file():
65
+ raise RuntimeError(
66
+ f"bundled sidecar not found at {candidate} — build the connector (pnpm build) or set "
67
+ "COTAL_SIDECAR_JS"
68
+ )
69
+ return candidate
70
+
71
+
72
+ def _bootstrap_standalone_sidecar() -> None:
73
+ """Standalone mode: spawn the bundled Node sidecar and wait until it has published the bridge
74
+ socket + tools file. Sets the three path env vars first so the sidecar and the rest of this
75
+ registration agree on them."""
76
+ run_dir = Path(tempfile.mkdtemp(prefix="cotal-hermes-"))
77
+ # Identity may come entirely from COTAL_LINK (which carries the space), so neither of these is
78
+ # guaranteed — they're only used for the readiness error below.
79
+ name = os.environ.get("COTAL_NAME") or "hermes"
80
+ space = os.environ.get("COTAL_SPACE") or "(from COTAL_LINK)"
81
+ os.environ.setdefault("COTAL_BRIDGE_SOCKET", str(run_dir / "bridge.sock"))
82
+ os.environ.setdefault("COTAL_CONTROL_SOCKET", str(run_dir / "control.sock"))
83
+ os.environ.setdefault("COTAL_TOOLS_FILE", str(run_dir / "cotal-tools.json"))
84
+
85
+ sidecar = _resolve_sidecar_js()
86
+ subprocess.Popen( # noqa: S603 — trusted bundled asset
87
+ ["node", str(sidecar)],
88
+ env=os.environ.copy(),
89
+ stdout=subprocess.DEVNULL,
90
+ stderr=subprocess.DEVNULL,
91
+ )
92
+
93
+ # Wait for the sidecar to be ready (tools file written + bridge socket bound). Fail loudly.
94
+ tools_file = Path(os.environ["COTAL_TOOLS_FILE"])
95
+ bridge_sock = Path(os.environ["COTAL_BRIDGE_SOCKET"])
96
+ deadline = time.monotonic() + 20.0
97
+ while time.monotonic() < deadline:
98
+ if tools_file.is_file() and bridge_sock.exists():
99
+ return
100
+ time.sleep(0.1)
101
+ raise RuntimeError(
102
+ f"cotal sidecar did not come up within 20s for {name}@{space} "
103
+ f"(tools={tools_file.is_file()}, bridge={bridge_sock.exists()})"
104
+ )
105
+
106
+
107
+ def register(ctx: Any) -> None:
108
+ if not _is_managed():
109
+ if not _have_identity():
110
+ return # not a cotal gateway — nothing to wire up
111
+ _bootstrap_standalone_sidecar()
112
+
113
+ # Gateway platform adapter (imported lazily so a non-gateway context still loads tools/hooks).
114
+ from .adapter import CotalAdapter
115
+
116
+ # Cotal mesh peers are already authorized by the NATS JWT, and an autonomous gateway has no
117
+ # operator to approve a pairing code or pick a home channel — both are first-contact gateway
118
+ # prompts that would otherwise intercept a peer's first message. Suppress them for the cotal
119
+ # platform ONLY (per-platform allow-all + a sentinel home channel), so a standalone user's
120
+ # other platforms (Telegram, …) keep their own access control.
121
+ os.environ.setdefault("COTAL_ALLOW_ALL_USERS", "true")
122
+ os.environ.setdefault("COTAL_HOME_CHANNEL", "mesh")
123
+
124
+ ctx.register_platform(
125
+ name="cotal",
126
+ label="Cotal",
127
+ adapter_factory=lambda cfg: CotalAdapter(cfg),
128
+ check_fn=_check_requirements,
129
+ allowed_users_env="COTAL_ALLOWED_USERS",
130
+ allow_all_env="COTAL_ALLOW_ALL_USERS",
131
+ platform_hint=(
132
+ "You are coordinating with peer agents on the Cotal mesh. Your reply is delivered "
133
+ "automatically back to whoever messaged you; use cotal_* tools only to reach OTHER "
134
+ "peers/channels or report status."
135
+ ),
136
+ emoji="🔗",
137
+ max_message_length=8000,
138
+ )
139
+
140
+ # Lifecycle hooks → presence.
141
+ ctx.register_hook("on_session_start", hooks.on_session_start)
142
+ ctx.register_hook("pre_llm_call", hooks.pre_llm_call)
143
+ ctx.register_hook("pre_tool_call", hooks.pre_tool_call)
144
+ ctx.register_hook("post_llm_call", hooks.post_llm_call)
145
+ ctx.register_hook("on_session_end", hooks.on_session_end)
146
+
147
+ # Proactive mesh tools (declared from the descriptors the sidecar wrote to COTAL_TOOLS_FILE).
148
+ register_tools(ctx)