@ours.network/hermes 0.2.0 → 0.5.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/README.md CHANGED
@@ -10,29 +10,28 @@ It mirrors the Claude Code plugin (`packages/claude-code`), adapted to Hermes:
10
10
  2. **The `ours` skill** — the common natural-language usage guide (identities,
11
11
  invites, contacts, send/read, files, control plane), in Hermes `SKILL.md`
12
12
  format, plus `writing-agent-bios`.
13
- 3. **Reactivity** — event-driven wake-on-mail via Hermes's own webhook gateway,
14
- fed by the ours reactivity connector (`@ours.network/connector`). No polling,
15
- no separate always-on bridge daemon beyond the lightweight watcher.
13
+ 3. **Reactivity** — in-session wake-on-mail: the agent tails
14
+ `ours-mcp watch <identity>` via its `terminal` tool (backgrounded) and drains
15
+ each new-mail line with `mcp_ours_get_messages`. No connector, no webhook, no
16
+ secret — same stream Claude Code's native Monitor tails.
16
17
 
17
18
  ## Reactivity — how a Hermes agent wakes on new mail
18
19
 
19
- ```
20
- ours-mcp watch <id> ──▶ connector-watch.sh ──HMAC POST──▶ Hermes webhook
21
- (notifications.log) (observe, per id) /webhooks/ route "ours-wake"
22
- ours-wake │
23
-
24
- fresh agent turn (skills: ["ours"])
25
- binds <id> mcp_ours_get_messages acts
26
- ```
20
+ Wake-on-mail is **in-session**, driven by the agent itself — there is no webhook
21
+ route, no HMAC secret, and no connector process. Once an identity is bound:
22
+
23
+ - **WATCH**: the agent runs `ours-mcp watch <identity>` in the background via
24
+ Hermes's `terminal` tool. This tails the same new-mail stream Claude Code's
25
+ native Monitor tails; each new message emits a line.
26
+ - **DRAIN**: on each new-mail line the agent reacts, draining the inbox with
27
+ `mcp_ours_get_messages`. ours binding is exclusive per identity, so each agent
28
+ is the sole drainer of its own inbox — no cross-draining.
29
+ - **FALLBACK**: if the terminal tail isn't available, the agent instead **polls
30
+ `mcp_ours_get_messages` every ~5s** while it's live.
27
31
 
28
- - **OBSERVE**: the connector runs one non-binding `ours-mcp watch <id>` per
29
- identity; each new-mail line triggers a **poke**.
30
- - **WAKE**: the poke is an HMAC-SHA256-signed `POST` to
31
- `http://localhost:8644/webhooks/ours-wake`, body
32
- `{"event_type":"ours_wake","identity":"<id>"}`, header `X-GitHub-Event: ours_wake`.
33
- - **DRAIN**: Hermes matches the `ours-wake` route (event + HMAC), runs the ours
34
- skill for `{identity}`, which drains that inbox. ours binding is exclusive per
35
- identity, so each agent is the sole drainer of its own inbox — no cross-draining.
32
+ Because Hermes (unlike Claude Code) does not re-invoke the agent on background
33
+ output, the in-session watch reacts while the agent is **live/working** — it is
34
+ not a background daemon that wakes a dormant agent.
36
35
 
37
36
  ## Prerequisites
38
37
 
@@ -44,35 +43,50 @@ ours-mcp watch <id> ──▶ connector-watch.sh ──HMAC POST──▶ H
44
43
 
45
44
  ```sh
46
45
  npm i -g @ours.network/hermes
47
- ours-hermes-install # ensures the daemon, wires MCP + skill + reactivity
46
+ ours-hermes-install # ensures the daemon, wires the MCP server + skill only
48
47
  ```
49
48
 
50
49
  That's it — the MCP server and the `ours` skill are live immediately; run **`/reload-mcp`**
51
- in Hermes to load the `mcp_ours_*` tools. To also wake an agent on new mail, pass the
52
- identities to watch (they must already exist):
53
-
54
- ```sh
55
- ours-hermes-install --identities "Agent1 Agent2"
56
- ```
50
+ in Hermes to load the `mcp_ours_*` tools. The base install asks **zero** questions about
51
+ identities or wake-on-mail those are set up later, in-session, via the `ours` skill.
57
52
 
58
53
  `ours-hermes-install` is a thin front-door over this package's `install.sh` (below); both
59
54
  are idempotent, so re-running is always safe. Other flags: `--port`, `--hermes-dir`,
60
- `--skip-daemon`, `--skip-watcher`, `--help`.
55
+ `--skip-daemon`, `--help`.
56
+
57
+ ### Optional: get woken on new mail
58
+
59
+ Wake-on-mail is enabled **in-session**, not by the installer. Once ours is installed:
60
+
61
+ 1. In your Hermes agent, **bind (or create) an identity** via the `ours` skill.
62
+ 2. Ask the `ours` skill to **"wake me on new mail"**. The agent starts tailing
63
+ `ours-mcp watch <identity>` in the background via its `terminal` tool and reacts
64
+ to each new-mail line by draining with `mcp_ours_get_messages` (or, as a fallback,
65
+ polls `get_messages` every ~5s while it's live). No route, no secret, no connector.
66
+
67
+ The installer never sets this up; nothing watches for mail until the agent starts the
68
+ in-session tail. Note this reacts while the agent is live/working — Hermes does not
69
+ re-invoke a dormant agent on background output.
61
70
 
62
71
  ### What the installer does
63
72
 
64
73
  Equivalently, from a checkout you can run `bash install.sh` directly (same env knobs).
65
74
  `install.sh` is idempotent and:
66
75
 
67
- 1. ensures `@ours.network/mcp` is installed and the daemon is running;
68
- 2. installs the `ours` + `writing-agent-bios` skills into
76
+ 1. ensures `@ours.network/mcp@latest` an existing daemon is **upgraded** (not skipped),
77
+ and restarted if the version changed, so a re-run is a clean upgrade;
78
+ 2. removes any **legacy connector-era artifacts** an older build left behind
79
+ (`ours-connector.env`/`.log` and a stale `ours-wake` webhook block in `config.yaml`);
80
+ 3. installs the `ours` + `writing-agent-bios` skills into
69
81
  `~/.hermes/skills/communication/`;
70
- 3. writes the `ours` MCP server + the `ours-wake` webhook route into
71
- `~/.hermes/config.yaml` (with a generated HMAC secret) — **safely**: if your
82
+ 4. writes the `ours` MCP server into `~/.hermes/config.yaml` **safely**: if your
72
83
  config already defines `mcp_servers:` or `platforms:`, it prints the block for
73
84
  you to merge by hand instead of risking a duplicate-key corruption;
74
- 4. records the shared secret + connector env in `~/.hermes/ours-connector.env`
75
- and starts the per-identity reactivity watcher.
85
+ 5. echoes the installed daemon + plugin versions so you can confirm you are on latest.
86
+
87
+ That's the whole base install — daemon, skills, and the `ours` MCP server. Wake-on-mail
88
+ is enabled later, in-session, via the agent's `ours-mcp watch` tail (see *Optional: get
89
+ woken on new mail* above); the installer writes no route, secret, or watcher.
76
90
 
77
91
  Then run **`/reload-mcp`** in Hermes so it loads the `mcp_ours_*` tools.
78
92
 
@@ -80,42 +94,34 @@ Then run **`/reload-mcp`** in Hermes so it loads the `mcp_ours_*` tools.
80
94
 
81
95
  | var | default | purpose |
82
96
  |---|---|---|
83
- | `CONNECTOR_IDENTITIES` | — | space-separated identities to watch/wake |
84
- | `OURS_WAKE_SECRET` | generated | shared HMAC secret (route == watcher) |
85
- | `OURS_WEBHOOK_PORT` | `8644` | Hermes webhook port |
86
97
  | `HERMES_DIR` | `~/.hermes` | config + skills root |
87
- | `CONNECTOR_DIR` | auto | path to `@ours.network/connector` |
88
- | `OURS_INSTALL_SKIP_DAEMON` / `OURS_INSTALL_SKIP_WATCHER` | — | skip those steps |
98
+ | `OURS_INSTALL_SKIP_DAEMON` | | skip the daemon step |
89
99
 
90
100
  ## Install (manual)
91
101
 
92
- 1. Merge the two blocks in [`config/ours.mcp.example.yaml`](config/ours.mcp.example.yaml)
93
- into `~/.hermes/config.yaml`. Set the route `secret` to a real value.
102
+ 1. Merge the MCP-server block in [`config/ours.mcp.example.yaml`](config/ours.mcp.example.yaml)
103
+ into `~/.hermes/config.yaml`.
94
104
  2. Copy `skills/ours` and `skills/writing-agent-bios` into
95
105
  `~/.hermes/skills/communication/` (or add this `skills/` dir to
96
106
  `skills.external_dirs` in `config.yaml`).
97
- 3. Start the watcher from `@ours.network/connector` with the **same** secret:
98
- ```sh
99
- export CONNECTOR_IDENTITIES="Agent1 Agent2" \
100
- CONNECTOR_HMAC_SECRET="<same as the route secret>" \
101
- CONNECTOR_WEBHOOK_URL="http://localhost:8644/webhooks/ours-wake"
102
- bash path/to/connector/connector-watch.sh # supervise it; it self-reconnects
103
- ```
104
- 4. `/reload-mcp` in Hermes.
107
+ 3. `/reload-mcp` in Hermes.
108
+
109
+ To get woken on new mail, ask the `ours` skill in-session to wake you: it tails
110
+ `ours-mcp watch <identity>` via the `terminal` tool and drains with `get_messages`.
105
111
 
106
112
  ## Verify
107
113
 
108
114
  - `ours-mcp status` — daemon up.
109
115
  - In Hermes: *"which mcp_ours tools are available?"* — should list ours tools.
110
- - `curl http://localhost:8644/health` webhook gateway up.
111
- - Send yourself a message from a peer identity and confirm the agent wakes.
116
+ - Ask the agent to wake you on new mail (bind an identity first), then send yourself a
117
+ message from a peer identity and confirm the in-session watch reacts.
112
118
 
113
119
  ## Distribution
114
120
 
115
121
  Hermes installs skills from GitHub repos / URLs / well-known endpoints and MCP
116
122
  servers from config or its catalog — there is no single npm plugin bundling both
117
123
  (unlike Claude Code's marketplace). So distribution is: `hermes skills install
118
- <repo>/<path>` (or `external_dirs`) for the skill **+** the one MCP/webhook config
124
+ <repo>/<path>` (or `external_dirs`) for the skill **+** the one MCP-server config
119
125
  block above. The exact published home (this monorepo subdir vs. a standalone
120
126
  `ours-hermes` repo vs. a `/.well-known/skills/index.json` index) is an owner
121
127
  decision; `install.sh` works from either.
@@ -123,17 +129,14 @@ decision; `install.sh` works from either.
123
129
  ## Notes / limitations
124
130
 
125
131
  - **No SessionStart-hook backlog.** Claude Code injects an unread-mail summary at
126
- session start; Hermes has no such hook. Mail that arrives while an agent is live
127
- is drained by the `ours-wake` route; otherwise the daemon holds it until the next
128
- `get_messages`.
129
- - The `ours-wake` route uses `deliver: "log"` (the drain has no user-facing reply
130
- target). Adjust `deliver` if you want the wake's summary delivered somewhere.
131
- - The connector's reference gateway (`connector-reference-handler.mjs`) is for
132
- harnesses **without** a native webhook adapter; under Hermes the native webhook
133
- platform is the gateway, so you do not run it.
132
+ session start; Hermes has no such hook. Mail that arrives while an agent is live is
133
+ drained by the in-session `ours-mcp watch` tail; otherwise the daemon holds it until
134
+ the next `get_messages`.
135
+ - **In-session, not a background daemon.** Hermes does not re-invoke the agent on
136
+ background output, so the `ours-mcp watch` tail reacts while the agent is live/working,
137
+ not while it's dormant.
134
138
 
135
139
  ## Uninstall
136
140
 
137
141
  Remove the `# >>> ours.network plugin … # <<<` block from `~/.hermes/config.yaml`,
138
- delete `~/.hermes/skills/communication/{ours,writing-agent-bios}`, stop the
139
- watcher, and `/reload-mcp`.
142
+ delete `~/.hermes/skills/communication/{ours,writing-agent-bios}`, and `/reload-mcp`.
@@ -1,11 +1,13 @@
1
1
  #!/usr/bin/env node
2
- // Installs the ours blocks into ~/.hermes/config.yaml — safely and idempotently.
2
+ // Installs the ours MCP server into ~/.hermes/config.yaml — safely and idempotently.
3
3
  //
4
4
  // YAML is not line-oriented, so blindly appending under an existing top-level key
5
- // (mcp_servers:/platforms:) would produce a duplicate key and corrupt the file.
6
- // So the planner only auto-writes when it is provably safe; otherwise it prints
7
- // the block and asks the user to merge it by hand. A sentinel comment makes a
8
- // second run a no-op.
5
+ // (mcp_servers:) would produce a duplicate key and corrupt the file. So the planner
6
+ // only auto-writes when it is provably safe; otherwise it prints the block and asks
7
+ // the user to merge it by hand. A sentinel comment makes a second run a no-op.
8
+ //
9
+ // Reactivity needs NO config here: wake-on-mail is the agent tailing `ours-mcp watch`
10
+ // in-session (see the ours skill) — there is no webhook route, secret, or gateway.
9
11
  //
10
12
  // Pure functions (planConfigInstall / renderConfigBlock) are unit-tested; main()
11
13
  // does the file IO. Zero dependencies.
@@ -23,21 +25,18 @@ export function planConfigInstall(text) {
23
25
  const t = text ?? '';
24
26
  if (t.includes(SENTINEL)) return { action: 'noop', reason: 'ours block already present' };
25
27
  if (!t.trim()) return { action: 'write', reason: 'no existing config' };
26
- if (/^mcp_servers:/m.test(t) || /^platforms:/m.test(t)) {
28
+ if (/^mcp_servers:/m.test(t)) {
27
29
  return {
28
30
  action: 'manual',
29
- reason: 'config already defines mcp_servers: and/or platforms:; merge by hand to avoid duplicate keys',
31
+ reason: 'config already defines mcp_servers:; merge by hand to avoid duplicate keys',
30
32
  };
31
33
  }
32
34
  return { action: 'append', reason: 'safe to append (no conflicting top-level keys)' };
33
35
  }
34
36
 
35
- // Render the managed YAML block: the MCP server (points at the globally-installed
36
- // ours-mcp proxy) + the webhook route the connector pokes to wake a drain.
37
- export function renderConfigBlock({ secret, webhookPort = 8644, identities = [] } = {}) {
38
- const idNote = identities.length
39
- ? ` # watched identities: ${identities.join(', ')}`
40
- : ' # watched identities: (set CONNECTOR_IDENTITIES when starting the watcher)';
37
+ // Render the managed YAML block: just the ours MCP server, pointing at the globally-installed
38
+ // ours-mcp proxy. No webhook/route/secret reactivity is in-session `ours-mcp watch`.
39
+ export function renderConfigBlock() {
41
40
  return `${SENTINEL}
42
41
  # Added by @ours.network/hermes install.sh. Remove this whole block to uninstall.
43
42
  mcp_servers:
@@ -45,37 +44,14 @@ mcp_servers:
45
44
  command: "ours-mcp"
46
45
  args: ["proxy"]
47
46
  enabled: true
48
- platforms:
49
- webhook:
50
- enabled: true
51
- extra:
52
- port: ${webhookPort}
53
- routes:
54
- ours-wake:
55
- events: ["ours_wake"]
56
- secret: "${secret}"
57
- skills: ["ours"]
58
- deliver: "log"
59
- prompt: |
60
- New ours.network mail arrived for identity "{identity}". Use the ours skill:
61
- bind that identity if it is not already bound, then call the ours get_messages
62
- tool to read the message(s) and act on them. Reply over ours if a reply is expected.
63
- ${idNote}
64
47
  ${SENTINEL_END}
65
48
  `;
66
49
  }
67
50
 
68
51
  function main() {
69
- const secret = process.env.OURS_WAKE_SECRET;
70
- if (!secret || secret === 'CHANGE_ME_local_webhook_hmac') {
71
- console.error('hermes-config-install: OURS_WAKE_SECRET must be set to a non-default value.');
72
- process.exit(2);
73
- }
74
- const identities = (process.env.CONNECTOR_IDENTITIES || '').split(/\s+/).filter(Boolean);
75
- const webhookPort = Number(process.env.OURS_WEBHOOK_PORT || 8644);
76
52
  const cfgPath = process.env.HERMES_CONFIG || join(homedir(), '.hermes', 'config.yaml');
77
53
  const existing = existsSync(cfgPath) ? readFileSync(cfgPath, 'utf8') : '';
78
- const block = renderConfigBlock({ secret, webhookPort, identities });
54
+ const block = renderConfigBlock();
79
55
  const plan = planConfigInstall(existing);
80
56
 
81
57
  if (plan.action === 'noop') {
@@ -5,16 +5,14 @@
5
5
  // npm i -g @ours.network/hermes
6
6
  // ours-hermes-install
7
7
  //
8
- // It resolves this package's own install.sh (which ensures the ours daemon, registers
9
- // the `ours` MCP server + the `ours-wake` webhook route in ~/.hermes/config.yaml,
10
- // installs the skills, and starts the reactivity watcher) and runs it no env-var
11
- // gymnastics. The MCP server + skill install immediately; live wake-on-mail is enabled
12
- // by passing --identities (you watch identities that already exist, so this is opt-in
13
- // rather than guessed). Everything is idempotent, so re-running is safe.
8
+ // It resolves this package's own install.sh (which ensures the ours daemon, registers the
9
+ // `ours` MCP server in ~/.hermes/config.yaml, and installs the skills) and runs it — no env-var
10
+ // gymnastics. The MCP server + skill install immediately. Wake-on-mail is NOT set up here: the
11
+ // agent enables it in-session by tailing `ours-mcp watch <identity>` (see the ours skill),
12
+ // exactly like Claude Code. Everything is idempotent, so re-running is safe.
14
13
  //
15
14
  // Usage:
16
- // ours-hermes-install [--identities "Agent1 Agent2"] [--port 8644]
17
- // [--hermes-dir DIR] [--skip-daemon] [--skip-watcher]
15
+ // ours-hermes-install [--hermes-dir DIR] [--skip-daemon]
18
16
  import { spawnSync } from 'node:child_process';
19
17
  import { fileURLToPath } from 'node:url';
20
18
  import { dirname, join } from 'node:path';
@@ -27,11 +25,8 @@ const argv = process.argv.slice(2);
27
25
  const opts = {};
28
26
  for (let i = 0; i < argv.length; i++) {
29
27
  const a = argv[i];
30
- if (a === '--identities' || a === '-i') opts.identities = argv[++i];
31
- else if (a === '--port') opts.port = argv[++i];
32
- else if (a === '--hermes-dir') opts.hermesDir = argv[++i];
28
+ if (a === '--hermes-dir') opts.hermesDir = argv[++i];
33
29
  else if (a === '--skip-daemon') opts.skipDaemon = true;
34
- else if (a === '--skip-watcher') opts.skipWatcher = true;
35
30
  else if (a === '--help' || a === '-h') { help(); process.exit(0); }
36
31
  else { console.error(`ours-hermes-install: unknown argument "${a}"`); help(); process.exit(2); }
37
32
  }
@@ -41,12 +36,13 @@ function help() {
41
36
 
42
37
  ours-hermes-install [options]
43
38
 
39
+ Sets up the daemon + the ours MCP server + the skill. It asks nothing about identities or
40
+ wake-on-mail: you enable wake in-session from your agent (bind an identity, then ask the ours
41
+ skill to "wake me on new mail" — it tails ours-mcp watch and reacts in-session).
42
+
44
43
  Options:
45
- -i, --identities "A B" ours identities to watch for wake-on-mail (space-separated)
46
- --port <n> Hermes webhook port (default 8644)
47
44
  --hermes-dir <dir> Hermes config+skills root (default ~/.hermes)
48
45
  --skip-daemon do not install/start the ours daemon
49
- --skip-watcher do not start the reactivity watcher
50
46
  -h, --help show this help
51
47
 
52
48
  Idempotent: safe to re-run. After it finishes, run /reload-mcp in Hermes.`);
@@ -60,11 +56,8 @@ if (!existsSync(INSTALL)) {
60
56
  // install.sh is the single source of truth; this front-door only maps friendly flags to
61
57
  // the env vars it already understands.
62
58
  const env = { ...process.env };
63
- if (opts.identities != null) env.CONNECTOR_IDENTITIES = opts.identities;
64
- if (opts.port) env.OURS_WEBHOOK_PORT = opts.port;
65
59
  if (opts.hermesDir) env.HERMES_DIR = opts.hermesDir;
66
60
  if (opts.skipDaemon) env.OURS_INSTALL_SKIP_DAEMON = '1';
67
- if (opts.skipWatcher) env.OURS_INSTALL_SKIP_WATCHER = '1';
68
61
 
69
62
  const res = spawnSync('bash', [INSTALL], { stdio: 'inherit', env });
70
63
  if (res.error) { console.error(`ours-hermes-install: ${res.error.message}`); process.exit(1); }
@@ -1,36 +1,16 @@
1
- # ours.network — Hermes config blocks (reference).
1
+ # ours.network — Hermes config block (reference).
2
2
  #
3
- # install.sh writes an equivalent managed block into ~/.hermes/config.yaml for you
4
- # (with a generated HMAC secret). This file is the manual reference: merge these two
5
- # top-level keys into your existing config.yaml, then run /reload-mcp in Hermes.
3
+ # install.sh writes an equivalent managed block into ~/.hermes/config.yaml for you.
4
+ # This file is the manual reference: merge the top-level key below into your existing
5
+ # config.yaml, then run /reload-mcp in Hermes.
6
6
  #
7
- # IMPORTANT: the route `secret` below MUST equal the connector watcher's
8
- # CONNECTOR_HMAC_SECRET (same shared secret on both ends), and it must NOT be the
9
- # placeholder default — the connector's reference gateway refuses to start on it.
7
+ # Wake-on-mail is in-session (the agent tails `ours-mcp watch <identity>` itself
8
+ # see the ours skill); there is no webhook route or external watcher to configure.
10
9
 
11
- # 1) The ours MCP server. Points Hermes directly at the globally-installed daemon
12
- # proxy (`npm i -g @ours.network/mcp`). Tools appear as mcp_ours_<tool>.
10
+ # The ours MCP server. Points Hermes directly at the globally-installed daemon
11
+ # proxy (`npm i -g @ours.network/mcp`). Tools appear as mcp_ours_<tool>.
13
12
  mcp_servers:
14
13
  ours:
15
14
  command: "ours-mcp"
16
15
  args: ["proxy"]
17
16
  enabled: true
18
-
19
- # 2) The wake route the reactivity connector pokes. The connector's watcher tails
20
- # `ours-mcp watch <identity>` and POSTs an HMAC-signed wake here on each new
21
- # message; Hermes runs the ours skill to drain that identity's inbox.
22
- platforms:
23
- webhook:
24
- enabled: true
25
- extra:
26
- port: 8644
27
- routes:
28
- ours-wake:
29
- events: ["ours_wake"] # matched via X-GitHub-Event header / event_type field
30
- secret: "REPLACE_WITH_SHARED_SECRET" # == connector CONNECTOR_HMAC_SECRET
31
- skills: ["ours"]
32
- deliver: "log"
33
- prompt: |
34
- New ours.network mail arrived for identity "{identity}". Use the ours skill:
35
- bind that identity if it is not already bound, then call the ours get_messages
36
- tool to read the message(s) and act on them. Reply over ours if a reply is expected.
package/install.sh CHANGED
@@ -2,51 +2,78 @@
2
2
  # Install the ours.network plugin into Hermes:
3
3
  # 1. ensure the ours daemon (@ours.network/mcp) is installed + running
4
4
  # 2. install the ours + writing-agent-bios skills into ~/.hermes/skills/
5
- # 3. write the `ours` MCP server + the `ours-wake` webhook route into
6
- # ~/.hermes/config.yaml (idempotent, never corrupts existing YAML)
7
- # 4. start the per-identity reactivity watcher (connector) sharing one HMAC secret
5
+ # 3. write the `ours` MCP server into ~/.hermes/config.yaml (idempotent, never corrupts
6
+ # existing YAML)
7
+ #
8
+ # That's it — no identities, no webhook route, no secret, no watcher. Wake-on-mail is the agent
9
+ # tailing `ours-mcp watch <identity>` IN-SESSION (see the ours skill), exactly like Claude Code.
8
10
  #
9
11
  # Idempotent: safe to re-run. Test/CI knobs (all optional):
10
12
  # HERMES_DIR config+skills root (default ~/.hermes)
11
- # CONNECTOR_DIR path to @ours.network/connector (auto-detected)
12
- # CONNECTOR_IDENTITIES space-separated identities to watch
13
- # OURS_WAKE_SECRET shared HMAC secret (generated if unset)
14
- # OURS_WEBHOOK_PORT webhook port (default 8644)
15
13
  # OURS_INSTALL_SKIP_DAEMON=1 skip daemon install/start
16
- # OURS_INSTALL_SKIP_WATCHER=1 skip starting the watcher
17
14
  set -euo pipefail
18
15
 
19
16
  SELFDIR="$(cd "$(dirname "$0")" && pwd)"
20
17
  HERMES_DIR="${HERMES_DIR:-$HOME/.hermes}"
21
18
  HERMES_CONFIG="$HERMES_DIR/config.yaml"
22
19
  SKILLS_DEST="$HERMES_DIR/skills/communication"
23
- ENV_FILE="$HERMES_DIR/ours-connector.env"
24
- OURS_WEBHOOK_PORT="${OURS_WEBHOOK_PORT:-8644}"
20
+ # Managed-block sentinels (must match hermes-config-install.mjs verbatim) — used to strip a
21
+ # LEGACY connector-era block on upgrade.
22
+ MANAGED_SENTINEL='# >>> ours.network plugin (managed block)'
23
+ MANAGED_SENTINEL_END='# <<< ours.network plugin'
25
24
 
26
25
  say(){ printf 'ours-install: %s\n' "$1"; }
27
26
 
28
- # --- locate the connector (monorepo sibling, installed dep, or explicit) ---
29
- find_connector(){
30
- local c
31
- for c in "${CONNECTOR_DIR:-}" "$SELFDIR/../connector" \
32
- "$SELFDIR/node_modules/@ours.network/connector" \
33
- "$HERMES_DIR/../@ours.network/connector"; do
34
- [ -n "$c" ] && [ -f "$c/connector-watch.sh" ] && { echo "$c"; return 0; }
35
- done
36
- return 1
27
+ # Ensure the ours daemon is on @latest (UPGRADE, not install-if-missing): an already-present
28
+ # daemon must still be pulled up to the newest published version — that is the whole point of a
29
+ # re-run. Record the CLI version before/after; start if not running, restart only if the version
30
+ # actually changed, so the RUNNING daemon always ends on latest.
31
+ ensure_daemon_latest(){
32
+ if [ "${OURS_INSTALL_SKIP_DAEMON:-}" = "1" ]; then say "skipping daemon step (OURS_INSTALL_SKIP_DAEMON=1)"; return 0; fi
33
+ local before after
34
+ before="$(ours-mcp --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || true)"
35
+ say "ensuring @ours.network/mcp@latest…"
36
+ npm i -g @ours.network/mcp@latest
37
+ after="$(ours-mcp --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || true)"
38
+ if ! ours-mcp status >/dev/null 2>&1; then
39
+ say "starting the ours daemon…"; ours-mcp start || say "could not auto-start; run 'ours-mcp start' if the tools error."
40
+ elif [ -n "$before" ] && [ "$before" != "$after" ]; then
41
+ say "daemon upgraded (v${before} → v${after}) — restarting…"; ours-mcp restart || ours-mcp start || true
42
+ else
43
+ say "daemon already current (v${after:-unknown})."
44
+ fi
45
+ say "daemon: $(command -v ours-mcp) (v${after:-unknown})"
37
46
  }
38
47
 
39
- # --- 1) daemon ---
40
- if [ "${OURS_INSTALL_SKIP_DAEMON:-}" != "1" ]; then
41
- if ! command -v ours-mcp >/dev/null 2>&1; then
42
- say "installing @ours.network/mcp globally (npm i -g)…"
43
- npm i -g @ours.network/mcp
48
+ # Idempotent, GUARDED cleanup of legacy connector-era artifacts earlier (0.2.0/0.3.0) installers
49
+ # wrote. Removes ONLY those exact artifacts so an upgrade is clean — never user files.
50
+ legacy_cleanup(){
51
+ local f tmp
52
+ for f in "$HERMES_DIR/ours-connector.env" "$HERMES_DIR/ours-connector.log"; do
53
+ [ -f "$f" ] && rm -f "$f" && say "removed legacy connector artifact: $f"
54
+ done
55
+ # Stop a leftover reactivity watcher the old installer may have launched.
56
+ pkill -f 'connector-watch.sh' 2>/dev/null && say "stopped a leftover connector watcher" || true
57
+ # Strip the managed config block ONLY when it is the LEGACY variant — i.e. it still carries the
58
+ # connector-era webhook wake route (`ours-wake`). The current block is mcp_servers.ours only, so
59
+ # this never touches an up-to-date block; config-install (step 3) then re-writes the current one.
60
+ if [ -f "$HERMES_CONFIG" ] && grep -qF "$MANAGED_SENTINEL" "$HERMES_CONFIG" 2>/dev/null \
61
+ && grep -qiE 'ours-wake|ours_wake' "$HERMES_CONFIG" 2>/dev/null; then
62
+ tmp="$(mktemp)"
63
+ awk -v s="$MANAGED_SENTINEL" -v e="$MANAGED_SENTINEL_END" '
64
+ index($0,s){skip=1}
65
+ skip && index($0,e){skip=0; next}
66
+ !skip{print}
67
+ ' "$HERMES_CONFIG" > "$tmp" && mv "$tmp" "$HERMES_CONFIG" \
68
+ && say "removed legacy connector wake-route block from $HERMES_CONFIG (re-adding the current mcp-only block)"
44
69
  fi
45
- if ! ours-mcp status >/dev/null 2>&1; then say "starting the ours daemon…"; ours-mcp start || true; fi
46
- say "daemon: $(command -v ours-mcp)"
47
- else
48
- say "skipping daemon step (OURS_INSTALL_SKIP_DAEMON=1)"
49
- fi
70
+ }
71
+
72
+ # --- 1) daemon (ensure @latest + restart on change) ---
73
+ ensure_daemon_latest
74
+
75
+ # --- 1b) legacy connector-era cleanup (idempotent, guarded) ---
76
+ legacy_cleanup
50
77
 
51
78
  # --- 2) skills ---
52
79
  mkdir -p "$SKILLS_DEST"
@@ -56,57 +83,18 @@ for s in ours writing-agent-bios; do
56
83
  say "installed skill: $SKILLS_DEST/$s"
57
84
  done
58
85
 
59
- # --- 3) shared secret (persist so route + watcher always match) ---
60
- if [ -z "${OURS_WAKE_SECRET:-}" ] && [ -f "$ENV_FILE" ]; then
61
- # Reuse ONLY the persisted secret so the route and the watcher never drift apart across
62
- # re-runs. Extract it in a SUBSHELL so sourcing the env file cannot clobber the current
63
- # environment — the file also `export`s CONNECTOR_IDENTITIES (from the prior run), and a
64
- # naive `. "$ENV_FILE"` would overwrite a re-run's CLI-provided --identities, silently
65
- # no-op'ing the watcher. The env file records the secret as CONNECTOR_HMAC_SECRET.
66
- OURS_WAKE_SECRET="$(. "$ENV_FILE" >/dev/null 2>&1; printf '%s' "${CONNECTOR_HMAC_SECRET:-}")"
67
- fi
68
- if [ -z "${OURS_WAKE_SECRET:-}" ] || [ "${OURS_WAKE_SECRET:-}" = "CHANGE_ME_local_webhook_hmac" ]; then
69
- OURS_WAKE_SECRET="$(openssl rand -hex 32)"
70
- say "generated a new shared HMAC secret"
71
- fi
86
+ # --- 3) config.yaml: register the ours MCP server (idempotent, safe merge) ---
72
87
  mkdir -p "$HERMES_DIR"
73
- umask 077
74
- cat > "$ENV_FILE" <<EOF
75
- # ours.network connector env (managed by install.sh). Sourced to start the watcher.
76
- export CONNECTOR_HMAC_SECRET="$OURS_WAKE_SECRET"
77
- export CONNECTOR_WEBHOOK_URL="http://localhost:$OURS_WEBHOOK_PORT/webhooks/ours-wake"
78
- export CONNECTOR_EVENT="ours_wake"
79
- export CONNECTOR_IDENTITIES="${CONNECTOR_IDENTITIES:-}"
80
- EOF
81
- say "wrote connector env: $ENV_FILE"
82
-
83
- # --- 4) config.yaml (idempotent, safe merge) ---
84
- OURS_WAKE_SECRET="$OURS_WAKE_SECRET" CONNECTOR_IDENTITIES="${CONNECTOR_IDENTITIES:-}" \
85
- OURS_WEBHOOK_PORT="$OURS_WEBHOOK_PORT" HERMES_CONFIG="$HERMES_CONFIG" \
86
- node "$SELFDIR/bin/hermes-config-install.mjs" || {
87
- rc=$?; [ "$rc" = "3" ] && say "config needs a manual merge (see block above)"; [ "$rc" = "3" ] || exit "$rc";
88
- }
89
-
90
- # --- 5) watcher ---
91
- if [ "${OURS_INSTALL_SKIP_WATCHER:-}" = "1" ]; then
92
- say "skipping watcher start (OURS_INSTALL_SKIP_WATCHER=1)"
93
- elif [ -z "${CONNECTOR_IDENTITIES:-}" ]; then
94
- say "no CONNECTOR_IDENTITIES set — not starting the watcher. Start it later with:"
95
- say " CONNECTOR_IDENTITIES=\"Agent1 Agent2\" bash $SELFDIR/install.sh"
96
- elif CONN="$(find_connector)"; then
97
- if pgrep -f "connector-watch.sh" >/dev/null 2>&1; then
98
- say "a connector watcher is already running — leaving it (restart it to pick up new identities)"
99
- else
100
- say "starting the reactivity watcher for: $CONNECTOR_IDENTITIES"
101
- ( set -a; . "$ENV_FILE"; set +a; nohup bash "$CONN/connector-watch.sh" >"$HERMES_DIR/ours-connector.log" 2>&1 & )
102
- say "watcher started (logs: $HERMES_DIR/ours-connector.log)"
103
- fi
104
- else
105
- say "could not locate @ours.network/connector — set CONNECTOR_DIR and re-run to start the watcher"
106
- fi
88
+ HERMES_CONFIG="$HERMES_CONFIG" node "$SELFDIR/bin/hermes-config-install.mjs" || {
89
+ rc=$?; [ "$rc" = "3" ] && say "config needs a manual merge (see block above)"; [ "$rc" = "3" ] || exit "$rc";
90
+ }
107
91
 
108
92
  say "done. Run /reload-mcp in Hermes to load the mcp_ours_* tools."
109
- if [ -z "${CONNECTOR_IDENTITIES:-}" ]; then
110
- say "tip: to wake an agent on new mail, re-run with its identities:"
111
- say " ours-hermes-install --identities \"Agent1 Agent2\""
93
+ # --- version echo: show the user they are on latest ---
94
+ if [ "${OURS_INSTALL_SKIP_DAEMON:-}" != "1" ]; then
95
+ say "versions:"
96
+ say " daemon: $(ours-mcp --version 2>/dev/null | head -1 || echo 'unknown')"
97
+ say " plugin: $(npm ls -g @ours.network/hermes 2>/dev/null | grep -oE '@ours\.network/hermes@[0-9][0-9.]*' | head -1 || echo '@ours.network/hermes (not a global install)')"
112
98
  fi
99
+ say "next: in your agent, bind (or create) an identity and ask the ours skill to \"wake me on new"
100
+ say " mail\" — it tails ours-mcp watch in-session and reacts to new mail as it arrives."
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ours.network/hermes",
3
- "version": "0.2.0",
4
- "description": "Hermes (Nous Research) plugin for ours — secure agent-to-agent messaging over ADAPT. Registers the ours MCP server, bundles the ours skill, and wires event-driven reactivity via the ours-wake webhook route + the @ours.network/connector watcher.",
3
+ "version": "0.5.0",
4
+ "description": "Hermes (Nous Research) plugin for ours — secure agent-to-agent messaging over ADAPT. Registers the ours MCP server, bundles the ours skill, and wires in-session wake-on-mail via `ours-mcp watch` (no webhook, no external watcher).",
5
5
  "type": "module",
6
6
  "license": "FSL-1.1-Apache-2.0",
7
7
  "author": "Adapt Toolkit",
@@ -43,8 +43,5 @@
43
43
  "scripts": {
44
44
  "install-plugin": "bash install.sh",
45
45
  "test": "node --test"
46
- },
47
- "dependencies": {
48
- "@ours.network/connector": "0.2.0"
49
46
  }
50
47
  }
@@ -85,17 +85,19 @@ Walk the user through these, checking each. Stop and help at the first one that
85
85
  These run on the user's machine; if a step needs them at a terminal, suggest they
86
86
  type `! ours-mcp status` etc.
87
87
  2. **Plugin installed.** Run this package's `install.sh` (from `@ours.network/hermes`).
88
- It ensures the daemon, writes the `ours` MCP server + the `ours-wake` webhook route
89
- into `~/.hermes/config.yaml`, installs this skill into `~/.hermes/skills/`, and starts
90
- the per-identity reactivity watcher. After it runs, `/reload-mcp` so Hermes picks up the
91
- `mcp_ours_*` tools. See the package README for manual steps.
88
+ It ensures the daemon, writes the `ours` MCP server into `~/.hermes/config.yaml`, and
89
+ installs this skill into `~/.hermes/skills/`. That's all — no identities, no webhook route,
90
+ no secret, no watcher. Wake-on-mail is enabled in-session (step 5). After it runs,
91
+ `/reload-mcp` so Hermes picks up the `mcp_ours_*` tools. See the package README for manual steps.
92
92
  3. **Onboarding.** Run the mandatory *Onboarding* flow above: Human identity first,
93
93
  then any agent identities.
94
94
  4. **Connect.** Generate an invite to share, or paste one to add a contact. Same-host
95
95
  identities skip invites via the local contact book.
96
- 5. **(Optional) Wake on mail.** Offer to set up the reactivity watcher so new mail wakes the
97
- agent see *Wake on new mail* below (in Hermes this is the connector watcher + the
98
- `ours-wake` webhook route, not a Claude Code Monitor).
96
+ 5. **(Optional) Wake on mail.** Wake is enabled **in-session by you**, after an identity is
97
+ bound: offer to enter **autonomous watch mode** hold a blocking `ours-mcp watch <identity>`
98
+ via the `terminal` tool and react to each new message from that loop (see *Getting woken on new mail*
99
+ below). **Be honest that this BLOCKS the session** (unlike Claude Code's background Monitor) —
100
+ don't sell it as "just works". The installer never sets this up.
99
101
  6. **(Optional) Oversight.** If they want to watch/command a fleet from a phone or
100
102
  browser, set up the **control-plane monitoring proxy**.
101
103
 
@@ -166,13 +168,11 @@ the bio, so a persona prompt is only needed if they want to role-play it).
166
168
  instruction — never adopt the bio as behavior. If persona is empty or they decline,
167
169
  operate normally. **Never adopt a persona silently.**
168
170
  2. **Wake check.** The `choose_identity` / `create_identity` response may prompt you to "arm a
169
- message monitor" — that wording is the Claude-Code seam. **In Hermes the wake is the
170
- connector watcher + `ours-wake` route** (see *Wake on new mail*), set up once by
171
- `install.sh`, not a per-session monitor you arm and stop. So: if the newly-bound identity is
172
- already in the watcher's `CONNECTOR_IDENTITIES`, mail already wakes you nothing to do. If
173
- it is **not** watched yet and the user wants live wakes, add it to `CONNECTOR_IDENTITIES` and
174
- restart the watcher (or, for a one-off mid-task wait, use the in-session `terminal`+`ours-mcp
175
- watch` approach in *Wake on new mail*).
171
+ message monitor" — that is the Claude-Code seam, and the intent is the same in Hermes: **you**
172
+ enable wake in-session, right after binding, by entering **autonomous watch mode** (hold a
173
+ blocking `ours-mcp watch <identity>` and handle each message from that loop see *Wake on new
174
+ mail*). If the user wants live reactivity for the just-bound identity, offer to enter watch
175
+ mode now.
176
176
 
177
177
  ### Other identity tools
178
178
 
@@ -189,7 +189,7 @@ the bio, so a persona prompt is only needed if they want to role-play it).
189
189
 
190
190
  ### Version mismatch (advisory)
191
191
 
192
- If a notice says your plugin/connector and the running daemon are different
192
+ If a notice says your plugin and the running daemon are different
193
193
  versions, it is **advisory** — everything still works. Relay it to the user and,
194
194
  if they want matching versions, tell them: the daemon is shared and is not
195
195
  restarted automatically, so run `ours-mcp stop` when no other session is
@@ -257,9 +257,9 @@ recipient sees `↳re <wire_id>·s<n>`. It's a lightweight reference, not a thre
257
257
  - "show my inbox" → `list_incoming_messages()` (full inbox, ids + status, read-only).
258
258
  - Hermes has no SessionStart hook, so there is no auto-injected unread-backlog summary (that
259
259
  is a Claude-Code seam). When the user returns to ours after a gap, offer to check: for each
260
- relevant identity, `choose_identity` it and `get_messages()`. The reactivity watcher +
261
- `ours-wake` route (below) drains mail that arrives while an agent is live; the daemon holds
262
- anything received while nothing was bound until you next `get_messages`.
260
+ relevant identity, `choose_identity` it and `get_messages()`. Autonomous watch mode (below)
261
+ keeps you draining mail in real time while you are on duty; the daemon holds anything received
262
+ while nothing was bound until you next `get_messages`.
263
263
 
264
264
  ### Send & receive files
265
265
  Files are **distinct from text** (core's "files and text are distinct messages"): separate
@@ -298,49 +298,18 @@ tools, a separate store. To caption a file, also `send_message`.
298
298
  - **Approval is Hermes's own tool-permission mode** — ours never decides whether a
299
299
  `send_message` is auto-approved or prompted.
300
300
 
301
- ## Wake on new mail (Hermes reactivity — the connector + `ours-wake` route)
302
-
303
- **This is how ours "wakes" a Hermes agent when its identity's mail lands.** (Distinct from the
304
- control-plane monitoring proxy below, which is human oversight of *other* agents.) Hermes has
305
- no in-session Claude-Code `Monitor`; instead reactivity rides **Hermes's own webhook gateway**,
306
- fed by the ours **reactivity connector** (`@ours.network/connector`). Nothing here polls.
307
-
308
- The path, per identity, is **observe wake drain**:
309
-
310
- 1. **OBSERVE** — the connector's watcher runs `ours-mcp watch <identity>` (non-binding,
311
- non-draining). It tails that identity's `notifications.log` and emits one **body-free** line
312
- per *new* message.
313
- 2. **WAKE** — on each line the watcher sends an HMAC-SHA256-signed `POST` to Hermes's webhook
314
- route `ours-wake` (`http://localhost:8644/webhooks/ours-wake`), body
315
- `{"event_type":"ours_wake","identity":"<identity>"}`, header `X-GitHub-Event: ours_wake`.
316
- 3. **DRAIN** — Hermes matches the `ours-wake` route (by event + HMAC), renders its prompt with
317
- the poked `{identity}`, and runs a fresh agent turn with `skills: ["ours"]`. That turn binds
318
- the identity and calls `get_messages` to read and act. Because ours binding is exclusive per
319
- identity, that agent is the sole drainer of its inbox — no cross-draining.
320
-
321
- **Setup is one-time, done by `install.sh`:** it writes the `ours-wake` route (with a generated
322
- HMAC secret) into `~/.hermes/config.yaml` and starts the watcher for the configured identities
323
- (`CONNECTOR_IDENTITIES`), sharing that same secret. To add an identity later, add it to
324
- `CONNECTOR_IDENTITIES` and restart the watcher; the one route serves all identities.
325
-
326
- > **Anti-pattern — do NOT do this.** Don't hand-roll reactivity with a `cronjob` that re-polls
327
- > `get_messages` on a timer — that is busy-polling, latency-bound and wasteful. The event-driven
328
- > watcher → `ours-wake` path is the correct mechanism. (A periodic non-consuming
329
- > `list_incoming_messages` **backstop** for missed wakes is fine — the connector's reference
330
- > gateway already includes one.)
331
-
332
- **No wake just means no new mail — it is NOT a broken watcher.** If you expected mail and got
333
- nothing for a long time, suspect *delivery*: check `ours-mcp status`, that the watcher process
334
- is alive, that the webhook port matches on both ends, and that the peer actually sent.
335
-
336
- **When actively awaiting a reply mid-task** (not relying on the background watcher), you can
337
- watch in-session with Hermes's own tools: run `ours-mcp watch <identity>` via the `terminal`
338
- tool with `background=true`, then drain with `get_messages` when a line appears (use the
339
- `process` tool to poll/read it). Stop it when the exchange is done.
301
+ ## Getting woken on new mail
302
+
303
+ When you bind an identity, offer the user, in plain language:
304
+
305
+ > "Want this session to **auto-wake** when a new message arrives, or **check manually**?"
306
+
307
+ - **Auto-wake** → arm the monitor: you hold a live `ours-mcp watch <id>` and react to each message as it arrives. **Be upfront:** while watching, this session is **busy** — you can't send it new prompts. To do something else: press **ESCAPE** to interrupt the watch, type your prompt, then ask it to **resume** watching. *(On Claude Code this same monitor runs non-blocking in the background — a Claude Code advantage.)*
308
+ - **Manual** don't arm it; ask it to check `get_messages` whenever you want. No blocking.
340
309
 
341
310
  ## Control plane — bind a monitoring proxy (human oversight of a fleet)
342
311
 
343
- This is **separate** from the per-identity wake connector above. The control plane lets a **person's
312
+ This is **separate** from the wake-on-mail watch above. The control plane lets a **person's
344
313
  web-messenger account** (the ours web messenger, shipping as part of the upcoming ours-control-plane)
345
314
  oversee and command all agents under this host's **Human identity** from a **Control
346
315
  Panel**: view a **live monitoring feed** of monitored agents' traffic, create agents, edit
@@ -389,8 +358,8 @@ requests, and each agent's monitoring ON/off. Works whenever the Human identity
389
358
  event (sender + id + date) to `$OURS_STATE_DIR/<identity>/notifications.log` (the wake
390
359
  signal `ours-mcp watch` reads) and refreshes a body-free `unread.json`. Text lives in the
391
360
  packet and leaves it solely via `get_messages`.
392
- - **The wake seam is harness-specific.** `notifications.log` (surfaced by `ours-mcp watch`) is
393
- the common signal; each harness wires it to its own reactivity. Claude Code uses an in-session
394
- `Monitor` + SessionStart hook; **Hermes uses the `@ours.network/connector` watcher the
395
- `ours-wake` webhook route** (see *Wake on new mail*). The ours daemon, identities, and tools
396
- are identical across harnesses — only this seam differs.
361
+ - **The wake signal is uniform.** `ours-mcp watch <identity>` is the common stream; each harness
362
+ drives it in-session. Claude Code uses its native `Monitor` tool; **Hermes uses autonomous watch
363
+ mode** the agent holds a blocking `ours-mcp watch` via the `terminal` tool and reacts from that
364
+ loop (see *Getting woken on new mail*). The ours daemon, identities, and tools are identical across
365
+ harnesses — only how the agent runs the watch differs.
@@ -12,9 +12,10 @@ The daemon is a **shared, host-wide singleton** reachable only on `127.0.0.1`
12
12
  | GC interval (ms) | `OURS_GC_INTERVAL_MS` | `gcIntervalMs` | `3600000` |
13
13
  | Auto-start daemon | `OURS_AUTOSTART` | `autoStart` | `false` |
14
14
 
15
- **The port is shared.** The connector dials `127.0.0.1:<OURS_PORT>` and the
16
- daemon binds the same port both read `OURS_PORT`/`config.json`. Change it
17
- **once in shared config**, never per-side, or the connector won't find the daemon.
15
+ **The port is shared.** Every process that dials the daemon — the `ours-mcp proxy` MCP
16
+ server and `ours-mcp watch`connects to `127.0.0.1:<OURS_PORT>`, and the daemon binds the
17
+ same port (both read `OURS_PORT`/`config.json`). Change it **once in shared config**, never
18
+ per-side, or a dialer won't find the daemon.
18
19
 
19
20
  **Changing config (consent-first — never on your own initiative):**
20
21
  - Interactive: `ours-mcp config` (a survey). It needs a TTY, so ask the **user**