@cotal-ai/connector-hermes 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/install.mjs +146 -35
- package/package.json +4 -4
- package/plugin/cotal/_sidecar/standalone.cjs +30 -0
package/bin/install.mjs
CHANGED
|
@@ -2,44 +2,143 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* `npx @cotal-ai/connector-hermes install`
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* Copies this package's `plugin/cotal` (
|
|
7
|
-
* `<HERMES_HOME>/plugins/cotal
|
|
5
|
+
* Guided install of the Cotal plugin into an existing Hermes so its gateway can join a Cotal mesh.
|
|
6
|
+
* Copies this package's `plugin/cotal` (incl. the bundled `_sidecar/standalone.cjs`) into
|
|
7
|
+
* `<HERMES_HOME>/plugins/cotal`, enables it, optionally writes your mesh config to
|
|
8
|
+
* `<HERMES_HOME>/.env`, and checks the mesh is reachable.
|
|
8
9
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* Interactive when run in a TTY; non-interactive with `--yes` (or no TTY), taking config from
|
|
11
|
+
* `--link/--space/--name/--server` flags or the matching `COTAL_*` env vars.
|
|
12
|
+
*
|
|
13
|
+
* House rule: no fallbacks. If Hermes isn't installed we fail loudly and write NOTHING.
|
|
12
14
|
*/
|
|
13
15
|
import { execFileSync } from "node:child_process";
|
|
14
|
-
import { cpSync, existsSync, mkdirSync, rmSync } from "node:fs";
|
|
16
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
17
|
+
import { createConnection } from "node:net";
|
|
15
18
|
import { homedir } from "node:os";
|
|
16
19
|
import { join, sep } from "node:path";
|
|
20
|
+
import { createInterface } from "node:readline/promises";
|
|
17
21
|
import { fileURLToPath } from "node:url";
|
|
18
22
|
|
|
19
23
|
const PKG_ROOT = fileURLToPath(new URL("..", import.meta.url));
|
|
20
24
|
const PLUGIN_SRC = join(PKG_ROOT, "plugin", "cotal");
|
|
21
25
|
const SIDECAR = join(PLUGIN_SRC, "_sidecar", "standalone.cjs");
|
|
22
26
|
|
|
23
|
-
|
|
27
|
+
const DEFAULTS = { space: "demo", name: "my-hermes", server: "nats://127.0.0.1:4222" };
|
|
28
|
+
|
|
29
|
+
const die = (msg) => {
|
|
24
30
|
process.stderr.write(`✗ ${msg}\n`);
|
|
25
31
|
process.exit(1);
|
|
32
|
+
};
|
|
33
|
+
const info = (msg) => process.stdout.write(`${msg}\n`);
|
|
34
|
+
|
|
35
|
+
function usage() {
|
|
36
|
+
info("usage: npx @cotal-ai/connector-hermes install [--yes] [--link <url> | --space <s> --name <n> --server <url>]");
|
|
26
37
|
}
|
|
27
|
-
|
|
28
|
-
|
|
38
|
+
|
|
39
|
+
// ---- arg parsing ------------------------------------------------------------
|
|
40
|
+
function parseFlags(argv) {
|
|
41
|
+
const f = {};
|
|
42
|
+
for (let i = 0; i < argv.length; i++) {
|
|
43
|
+
const a = argv[i];
|
|
44
|
+
if (a === "--yes" || a === "-y") f.yes = true;
|
|
45
|
+
else if (a.startsWith("--")) {
|
|
46
|
+
const [k, inline] = a.slice(2).split("=");
|
|
47
|
+
f[k] = inline ?? (argv[i + 1] && !argv[i + 1].startsWith("--") ? argv[++i] : true);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return f;
|
|
29
51
|
}
|
|
30
52
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
53
|
+
// ---- mesh config gathering --------------------------------------------------
|
|
54
|
+
async function promptConfig() {
|
|
55
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
56
|
+
try {
|
|
57
|
+
info("\nCotal mesh — paste a join link (cotal://token@host/space), or press Enter to set it manually:");
|
|
58
|
+
const link = (await rl.question(" link: ")).trim();
|
|
59
|
+
if (link) {
|
|
60
|
+
const name = (await rl.question(` name [${DEFAULTS.name}]: `)).trim() || DEFAULTS.name;
|
|
61
|
+
return { COTAL_LINK: link, COTAL_NAME: name };
|
|
62
|
+
}
|
|
63
|
+
const space = (await rl.question(` space [${DEFAULTS.space}]: `)).trim() || DEFAULTS.space;
|
|
64
|
+
const name = (await rl.question(` name [${DEFAULTS.name}]: `)).trim() || DEFAULTS.name;
|
|
65
|
+
const server = (await rl.question(` server [${DEFAULTS.server}]: `)).trim() || DEFAULTS.server;
|
|
66
|
+
return { COTAL_SPACE: space, COTAL_NAME: name, COTAL_SERVERS: server };
|
|
67
|
+
} finally {
|
|
68
|
+
rl.close();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function configFromFlags(flags) {
|
|
73
|
+
const link = flags.link || process.env.COTAL_LINK;
|
|
74
|
+
if (link) return { COTAL_LINK: link, COTAL_NAME: flags.name || process.env.COTAL_NAME || DEFAULTS.name };
|
|
75
|
+
const space = flags.space || process.env.COTAL_SPACE;
|
|
76
|
+
const server = flags.server || process.env.COTAL_SERVERS;
|
|
77
|
+
if (!space && !server) return {}; // nothing supplied → skip .env, print manual next steps
|
|
78
|
+
return {
|
|
79
|
+
COTAL_SPACE: space || DEFAULTS.space,
|
|
80
|
+
COTAL_NAME: flags.name || process.env.COTAL_NAME || DEFAULTS.name,
|
|
81
|
+
COTAL_SERVERS: server || DEFAULTS.server,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ---- ~/.hermes/.env editing (idempotent) ------------------------------------
|
|
86
|
+
function writeEnv(file, set) {
|
|
87
|
+
// Link mode and manual mode are mutually exclusive; comment out the other mode's keys so a
|
|
88
|
+
// re-run never leaves a stale COTAL_SPACE overriding a new COTAL_LINK (individual vars win in
|
|
89
|
+
// configFromEnv).
|
|
90
|
+
const drop = set.COTAL_LINK ? ["COTAL_SPACE", "COTAL_SERVERS"] : ["COTAL_LINK"];
|
|
91
|
+
const lines = existsSync(file) ? readFileSync(file, "utf8").split("\n") : [];
|
|
92
|
+
const keyOf = (l) => l.replace(/^#\s*/, "").split("=")[0].trim();
|
|
93
|
+
for (const [k, v] of Object.entries(set)) {
|
|
94
|
+
const line = `${k}=${v}`;
|
|
95
|
+
const i = lines.findIndex((l) => keyOf(l) === k);
|
|
96
|
+
if (i >= 0) lines[i] = line;
|
|
97
|
+
else lines.push(line);
|
|
98
|
+
}
|
|
99
|
+
for (let i = 0; i < lines.length; i++) {
|
|
100
|
+
if (drop.includes(keyOf(lines[i])) && !lines[i].trimStart().startsWith("#")) lines[i] = `# ${lines[i]}`;
|
|
101
|
+
}
|
|
102
|
+
writeFileSync(file, lines.join("\n").replace(/\n*$/, "\n"));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ---- mesh reachability ------------------------------------------------------
|
|
106
|
+
function hostPort(server) {
|
|
107
|
+
try {
|
|
108
|
+
const u = new URL(server.replace(/^(cotals?|nats):\/\//, "http://"));
|
|
109
|
+
return { host: u.hostname, port: Number(u.port || 4222) };
|
|
110
|
+
} catch {
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function reachable(server) {
|
|
115
|
+
const hp = hostPort(server);
|
|
116
|
+
if (!hp) return Promise.resolve(false);
|
|
117
|
+
return new Promise((res) => {
|
|
118
|
+
const s = createConnection({ host: hp.host, port: hp.port });
|
|
119
|
+
const done = (ok) => {
|
|
120
|
+
try {
|
|
121
|
+
s.destroy();
|
|
122
|
+
} catch {
|
|
123
|
+
/* ignore */
|
|
124
|
+
}
|
|
125
|
+
res(ok);
|
|
126
|
+
};
|
|
127
|
+
s.setTimeout(2000);
|
|
128
|
+
s.once("connect", () => done(true));
|
|
129
|
+
s.once("timeout", () => done(false));
|
|
130
|
+
s.once("error", () => done(false));
|
|
131
|
+
});
|
|
34
132
|
}
|
|
35
133
|
|
|
36
|
-
|
|
37
|
-
|
|
134
|
+
// ---- main -------------------------------------------------------------------
|
|
135
|
+
const args = process.argv.slice(2);
|
|
136
|
+
if (args[0] !== "install") {
|
|
38
137
|
usage();
|
|
39
|
-
process.exit(
|
|
138
|
+
process.exit(args[0] ? 1 : 0);
|
|
40
139
|
}
|
|
140
|
+
const flags = parseFlags(args.slice(1));
|
|
41
141
|
|
|
42
|
-
// 1. Require a working Hermes — refuse (writing nothing) if absent.
|
|
43
142
|
function hermesVersion() {
|
|
44
143
|
try {
|
|
45
144
|
return execFileSync("hermes", ["--version"], { encoding: "utf8" }).trim();
|
|
@@ -47,6 +146,7 @@ function hermesVersion() {
|
|
|
47
146
|
return undefined;
|
|
48
147
|
}
|
|
49
148
|
}
|
|
149
|
+
|
|
50
150
|
const version = hermesVersion();
|
|
51
151
|
if (!version) {
|
|
52
152
|
die(
|
|
@@ -55,45 +155,56 @@ if (!version) {
|
|
|
55
155
|
" then re-run: npx @cotal-ai/connector-hermes install",
|
|
56
156
|
);
|
|
57
157
|
}
|
|
58
|
-
|
|
59
|
-
// 2. The bundled sidecar must be present (it ships in the npm package; a raw source checkout
|
|
60
|
-
// needs `pnpm --filter @cotal-ai/connector-hermes build` first). No silent half-install.
|
|
61
158
|
if (!existsSync(SIDECAR)) {
|
|
62
159
|
die(
|
|
63
160
|
`bundled sidecar missing at ${SIDECAR}\n` +
|
|
64
161
|
" (running from source? build it first: pnpm --filter @cotal-ai/connector-hermes build)",
|
|
65
162
|
);
|
|
66
163
|
}
|
|
164
|
+
info(`✓ Hermes detected — ${version.split("·")[0].trim()}`);
|
|
67
165
|
|
|
68
|
-
// 3. Resolve the Hermes home honestly: HERMES_HOME wins, else ~/.hermes.
|
|
69
166
|
const hermesHome = process.env.HERMES_HOME?.trim() || join(homedir(), ".hermes");
|
|
167
|
+
const interactive = Boolean(process.stdin.isTTY) && !flags.yes;
|
|
168
|
+
const meshEnv = interactive ? await promptConfig() : configFromFlags(flags);
|
|
169
|
+
|
|
170
|
+
// Write mesh config to <home>/.env (if any was gathered).
|
|
171
|
+
if (Object.keys(meshEnv).length) {
|
|
172
|
+
const envFile = join(hermesHome, ".env");
|
|
173
|
+
mkdirSync(hermesHome, { recursive: true });
|
|
174
|
+
writeEnv(envFile, meshEnv);
|
|
175
|
+
info(`✓ Wrote ${Object.keys(meshEnv).join(", ")} to ${envFile}`);
|
|
176
|
+
|
|
177
|
+
// Best-effort reachability check.
|
|
178
|
+
const server = meshEnv.COTAL_SERVERS || meshEnv.COTAL_LINK;
|
|
179
|
+
if (server) {
|
|
180
|
+
const ok = await reachable(server);
|
|
181
|
+
info(ok ? `✓ Mesh reachable` : `✗ Mesh not reachable at ${hostPort(server)?.host}:${hostPort(server)?.port} — start it (e.g. \`nats-server -js\`) before \`hermes gateway run\``);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Copy the plugin + enable it.
|
|
70
186
|
const pluginsDir = join(hermesHome, "plugins");
|
|
71
187
|
const dest = join(pluginsDir, "cotal");
|
|
72
|
-
|
|
73
|
-
info(`Hermes: ${version}`);
|
|
74
|
-
info(`Installing the Cotal plugin into ${dest} ...`);
|
|
75
188
|
mkdirSync(pluginsDir, { recursive: true });
|
|
76
|
-
rmSync(dest, { recursive: true, force: true });
|
|
189
|
+
rmSync(dest, { recursive: true, force: true });
|
|
77
190
|
cpSync(PLUGIN_SRC, dest, {
|
|
78
191
|
recursive: true,
|
|
79
192
|
filter: (src) => !src.includes(`${sep}__pycache__`) && !src.endsWith(".pyc"),
|
|
80
193
|
});
|
|
81
|
-
|
|
82
|
-
// 4. Enable it via Hermes' own command (it's discovered now that it's in plugins/).
|
|
83
194
|
try {
|
|
84
195
|
execFileSync("hermes", ["plugins", "enable", "cotal"], { stdio: "inherit" });
|
|
85
196
|
} catch {
|
|
86
197
|
die(
|
|
87
198
|
"copied the plugin but `hermes plugins enable cotal` failed.\n" +
|
|
88
|
-
" Enable it manually: `hermes plugins enable cotal
|
|
199
|
+
" Enable it manually: `hermes plugins enable cotal`.",
|
|
89
200
|
);
|
|
90
201
|
}
|
|
91
202
|
|
|
92
203
|
info("\n✓ Cotal plugin installed + enabled.\n");
|
|
93
|
-
|
|
94
|
-
info("
|
|
95
|
-
info("
|
|
96
|
-
|
|
97
|
-
info("
|
|
98
|
-
info("");
|
|
99
|
-
|
|
204
|
+
if (Object.keys(meshEnv).length) {
|
|
205
|
+
info("Run it on the mesh:");
|
|
206
|
+
info(" hermes gateway run");
|
|
207
|
+
} else {
|
|
208
|
+
info("Next: set your mesh in ~/.hermes/.env (COTAL_LINK, or COTAL_SPACE/COTAL_NAME/COTAL_SERVERS),");
|
|
209
|
+
info(" then: hermes gateway run");
|
|
210
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cotal-ai/connector-hermes",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -22,14 +22,14 @@
|
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"tsx": "^4.22.4",
|
|
24
24
|
"zod": "^4.4.3",
|
|
25
|
-
"@cotal-ai/connector-core": "0.
|
|
25
|
+
"@cotal-ai/connector-core": "0.3.0"
|
|
26
26
|
},
|
|
27
27
|
"peerDependencies": {
|
|
28
|
-
"@cotal-ai/core": "0.1.
|
|
28
|
+
"@cotal-ai/core": ">=0.1.0"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"esbuild": "^0.28.0",
|
|
32
|
-
"@cotal-ai/core": "0.
|
|
32
|
+
"@cotal-ai/core": "0.3.0"
|
|
33
33
|
},
|
|
34
34
|
"files": [
|
|
35
35
|
"bin",
|
|
@@ -17919,6 +17919,15 @@ var MeshAgent = class extends import_node_events2.EventEmitter {
|
|
|
17919
17919
|
args: { name, graceful: opts?.graceful ?? true }
|
|
17920
17920
|
});
|
|
17921
17921
|
}
|
|
17922
|
+
/** Ask the manager to purge the space's retained chat backlog (its `purge` op). Cleanup only —
|
|
17923
|
+
* it doesn't touch live agents or the anycast work queue. `includeDms` also clears DM history. */
|
|
17924
|
+
async purgeHistory(opts) {
|
|
17925
|
+
this.assertConnected();
|
|
17926
|
+
return this.ep.requestControl("manager", {
|
|
17927
|
+
op: "purge",
|
|
17928
|
+
args: { includeDms: opts?.includeDms ?? false }
|
|
17929
|
+
});
|
|
17930
|
+
}
|
|
17922
17931
|
/** Define a persona and persist it as config (the manager's `definePersona` op writes
|
|
17923
17932
|
* .cotal/agents/<name>.md). On success, announce it on the channel — the "send it out"
|
|
17924
17933
|
* half — so peers see the new persona; `spawn(name)` then launches an agent wearing it. */
|
|
@@ -32870,6 +32879,27 @@ ${info}${caught}`);
|
|
|
32870
32879
|
}
|
|
32871
32880
|
}
|
|
32872
32881
|
},
|
|
32882
|
+
{
|
|
32883
|
+
name: "cotal_purge",
|
|
32884
|
+
title: "Cotal: clear chat history",
|
|
32885
|
+
description: "Ask the manager to purge this space's retained chat backlog (channel history). Set includeDms to also clear direct-message history. Cleanup only \u2014 it does not affect live agents or the anycast work queue. Irreversible.",
|
|
32886
|
+
schema: {
|
|
32887
|
+
includeDms: external_exports.boolean().optional().describe("Default false: channel history only. true = also purge DM history.")
|
|
32888
|
+
},
|
|
32889
|
+
async run(agent, _config, { includeDms }) {
|
|
32890
|
+
try {
|
|
32891
|
+
const reply = await agent.purgeHistory({ includeDms });
|
|
32892
|
+
if (!reply.ok)
|
|
32893
|
+
return err(`Couldn't purge history: ${reply.error ?? "manager refused"}`);
|
|
32894
|
+
const d = reply.data;
|
|
32895
|
+
const chat = d?.chat ?? 0;
|
|
32896
|
+
const dm = d?.dm;
|
|
32897
|
+
return ok(`Cleared ${chat} channel message${chat === 1 ? "" : "s"}${dm === void 0 ? "" : ` and ${dm} DM${dm === 1 ? "" : "s"}`} from "${_config.space}".`);
|
|
32898
|
+
} catch (e) {
|
|
32899
|
+
return err(`Couldn't purge history: no manager reachable (${e.message}). Is the manager running?`);
|
|
32900
|
+
}
|
|
32901
|
+
}
|
|
32902
|
+
},
|
|
32873
32903
|
{
|
|
32874
32904
|
name: "cotal_persona",
|
|
32875
32905
|
title: "Cotal: define a persona",
|