Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,34 @@ jobs:
- name: Packed CLI conformance
run: npm run test:packed-cli

minimum-node-packed-hermes:
name: Packed Hermes conformance (minimum Node 22.5.0)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4.2.2
with:
persist-credentials: false
- name: Setup minimum supported runtime Node
uses: actions/setup-node@v6.4.0
with:
node-version: '22.5.0'
- name: Preserve minimum runtime path
run: echo "MINIMUM_NODE=$(command -v node)" >> "$GITHUB_ENV"
- name: Setup build Node
uses: actions/setup-node@v6.4.0
with:
node-version: '24.x'
cache: npm
- name: Install build dependencies
run: npm ci
- name: Build and pack once
run: npm run build && npm run pack:prepare
- name: Install package with build Node and execute it with minimum Node
env:
HONEYCOMB_CONFORMANCE_NODE: ${{ env.MINIMUM_NODE }}
run: npm run test:packed-hermes

# ── Windows smoke. The dev host is Windows and the build scripts
# (esbuild.config.mjs chmod, pack-check.mjs) have win32 branches that Linux CI
# would never exercise. One pinned Node major is enough here — the Node matrix
Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,12 @@ jobs:
- name: Pack-check (tarball secret/forbidden-file + required-file scan)
run: npm run pack:check

- name: Packed Hermes connector conformance
run: npm run test:packed-hermes
env:
HONEYCOMB_POSTHOG_KEY: ${{ secrets.HONEYCOMB_POSTHOG_KEY }}
HONEYCOMB_REF_DEFAULT: ${{ vars.HONEYCOMB_REF_DEFAULT }}

# ── Tag-vs-package.json version guard. ───────────────────────────────────
# On a tag push the pushed tag `vX.Y.Z` MUST equal package.json's version
# (which sync-versions has already propagated into every manifest). A
Expand Down
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

<p align="center">
<a href="https://www.npmjs.com/package/@legioncodeinc/honeycomb"><img src="https://img.shields.io/npm/v/@legioncodeinc/honeycomb?style=flat-square&color=F7A823&label=version" alt="npm version"></a>
<img src="https://img.shields.io/badge/harnesses-3%20supported%20%7C%203%20in%20progress-F7A823?style=flat-square" alt="3 supported, 3 in progress">
<img src="https://img.shields.io/badge/harnesses-4%20supported%20%7C%202%20in%20progress-F7A823?style=flat-square" alt="4 supported, 2 in progress">
<img src="https://img.shields.io/badge/OS-windows%20%7C%20macos%20%7C%20linux-6E6A62?style=flat-square" alt="Windows, macOS, Linux">
</p>

Expand Down Expand Up @@ -257,14 +257,16 @@ Most agent-memory tools bolt onto a vector-only store, which forces *every* acce

## 🔌 Supported harnesses

Honeycomb supports 3 harnesses in production (Claude Code, Codex, Cursor). Hermes, pi, and OpenClaw are in progress.
Honeycomb supports 4 harnesses in production (Claude Code, Codex, Cursor, and Hermes). pi and OpenClaw are in progress.

| Supported today | In progress |
|---|---|
| **Claude Code**, **Cursor**, **Codex** | **Hermes**, **pi**, **OpenClaw** |
| **Claude Code**, **Cursor**, **Codex**, **Hermes** | **pi**, **OpenClaw** |

`honeycomb setup` detects the ones you have installed and wires each idempotently; `honeycomb uninstall` reverses only Honeycomb's changes. A skill mined while you were in Cursor is auto-pulled and ready in Claude Code on your next session.

Hermes is wired through its native `$HERMES_HOME/config.yaml` shell hooks and `mcp_servers` entry. For a non-default Hermes profile, run the connector with that profile's explicit home (for example, `HERMES_HOME="$HOME/.hermes/profiles/work" honeycomb connect hermes`); a sticky `active_profile` file alone is not inherited by a separately launched Honeycomb process. Honeycomb does not bypass Hermes' hook-consent boundary: after connecting Hermes, review and approve the exact Honeycomb hook commands at the next interactive Hermes start. Unapproved hooks are skipped in non-interactive/gateway sessions.

<img src="assets/brand/divider-minor.svg" width="100%" height="3">

## 🎛️ Other interfaces
Expand Down
19 changes: 15 additions & 4 deletions esbuild.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,12 @@
// `platform: "node"` and targets the repo Node engine (>=22). There is no need
// to down-level harness bundles; all hosts run on the same Node 22 floor.

import { chmodSync, copyFileSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { build } from "esbuild";
import { chmodSync, copyFileSync, writeFileSync, readFileSync } from "node:fs";

const ESM_PACKAGE_JSON = '{"type":"module"}\n';
const CJS_REQUIRE_BANNER =
"import { createRequire as __cr } from 'node:module'; const require = __cr(import.meta.url);";

// Single source of truth for the injected version: the root package.json
// version (PRD-001c keeps every manifest in sync with this same value, so the
Expand Down Expand Up @@ -136,7 +138,7 @@ await build({
// daemon at startup. (In-process itests assemble the daemon directly and never hit
// the bundle, so only the real `daemon start` path exercised this — caught by dogfood.)
banner: {
js: "import { createRequire as __cr } from 'node:module'; const require = __cr(import.meta.url);",
js: CJS_REQUIRE_BANNER,
},
});
stampEsm("daemon");
Expand Down Expand Up @@ -183,7 +185,13 @@ const HOOK_HARNESSES = [
outdir: "harnesses/cursor/bundle",
aliases: ["session-start.js", "capture.js", "pre-tool-use.js", "session-end.js"],
},
{ name: "hermes", entry: "dist/harnesses/hermes/src/index.js", outdir: "harnesses/hermes/bundle" },
{
name: "hermes",
entry: "dist/harnesses/hermes/src/index.js",
outdir: "harnesses/hermes/bundle",
aliases: ["session-start.mjs", "capture.mjs", "session-end.mjs"],
staleAliases: ["session-start.js", "capture.js", "session-end.js"],
},
{ name: "pi", entry: "dist/harnesses/pi/src/index.js", outdir: "harnesses/pi/bundle" },
];

Expand All @@ -195,6 +203,7 @@ const HARNESS_EXTRA_ENTRIES = [
];

for (const h of HOOK_HARNESSES) {
for (const staleAlias of h.staleAliases ?? []) rmSync(`${h.outdir}/${staleAlias}`, { force: true });
await build({
entryPoints: { index: h.entry },
bundle: true,
Expand Down Expand Up @@ -361,7 +370,9 @@ await build({
format: "esm",
outdir: "bundle",
external: ["node:*", ...NATIVE_COMPRESSION_EXTERNAL, ...TREE_SITTER_EXTERNAL],
banner: { js: "#!/usr/bin/env node" },
// The Hermes connector brings `yaml` into the CLI bundle. Its CommonJS build performs a
// dynamic `require("process")`, so the executable needs the same ESM require bridge as the daemon.
banner: { js: `#!/usr/bin/env node\n${CJS_REQUIRE_BANNER}` },
define: VERSION_DEFINE,
});
stampExecutable("bundle/cli.js");
Expand Down
10 changes: 0 additions & 10 deletions harnesses/hermes/.mcp.json

This file was deleted.

27 changes: 22 additions & 5 deletions harnesses/hermes/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,27 @@
/*
* Honeycomb - a cross-harness AI memory system.
* Copyright (C) 2026 Legion Code Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version. See the LICENSE file for details.
*/

/**
* Hermes harness adapter entry root. Thin client only (no DeepLake).
* Independently addressable by the bundler (PRD-001b).
* Hermes shell-hook binary entrypoint.
*
* Every configured Hermes lifecycle hook invokes one of the aliases built from
* this module. The adapter reads Hermes' JSON envelope from stdin, normalizes it
* through the shared hook runtime, and emits only Hermes-native JSON on stdout.
*/

import { bootHarness, type HarnessContext } from "../../../src/daemon-client/harness.js";
import { maybeRunHookBinaryMain, runHookBinary } from "../../../src/hooks/binary.js";
import { createHermesShim } from "../../../src/hooks/hermes/shim.js";
import type { HookEventOutcome } from "../../../src/hooks/runtime.js";

export function activate(): HarnessContext {
return bootHarness("hermes");
export function runHermesHook(): Promise<HookEventOutcome> {
return runHookBinary({ shim: createHermesShim() });
}

maybeRunHookBinaryMain(createHermesShim(), import.meta.url);
23 changes: 9 additions & 14 deletions library/knowledge/private/integrations/harness-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

> Category: Integrations | Version: 1.1 | Date: July 2026 | Status: Active

How Honeycomb plugs underneath coding harnesses: the install-time connector base, the per-harness shims, MCP-server-via-install, and the capability detection plus idempotent install/uninstall contract that wires 3 supported harnesses today (Claude Code, Codex, Cursor) while tracking Hermes, pi, and OpenClaw as in progress.
How Honeycomb plugs underneath coding harnesses: the install-time connector base, the per-harness shims, MCP-server-via-install, and the capability detection plus idempotent install/uninstall contract that wires 4 supported harnesses today (Claude Code, Codex, Cursor, Hermes) while tracking pi and OpenClaw as in progress.

**Related:**
- [`hook-lifecycle.md`](hook-lifecycle.md)
Expand All @@ -17,7 +17,7 @@ How Honeycomb plugs underneath coding harnesses: the install-time connector base

Honeycomb does not try to be another agent shell. It runs underneath the harnesses people already use and gives them one shared memory layer. The challenge is that every harness exposes a different extension surface, and they share almost nothing at the integration layer. The answer is to write the memory logic once in the daemon and wrap it per harness with a thin shim. Adding a harness means writing a shim and a connector subclass, not a memory engine.

Honeycomb currently supports 3 harnesses in production: **Claude Code, Codex, and Cursor**. Hermes, pi, and OpenClaw remain in progress. The daemon remains the only process that touches DeepLake, and supported harnesses reach it through the same three surfaces.
Honeycomb currently supports 4 harnesses in production: **Claude Code, Codex, Cursor, and Hermes**. pi and OpenClaw remain in progress. The daemon remains the only process that touches DeepLake, and supported harnesses reach it through the same three surfaces.

## Three surfaces, one daemon

Expand Down Expand Up @@ -45,16 +45,17 @@ The connector touches the local filesystem only, it opens no DeepLake, holds no

## The connector base

Every per-harness connector is a subclass of the abstract `HarnessConnector` (`src/connectors/contracts.ts`). The base owns `install()` and `uninstall()` and all the shared mechanics; a subclass overrides only four seams:
Every per-harness connector is a subclass of the abstract `HarnessConnector` (`src/connectors/contracts.ts`). The base owns `install()` and `uninstall()` and all the shared mechanics; a subclass overrides four required seams and may add owned install-time artifacts:

1. **`configPath()`**, where the harness keeps its hook config.
2. **`hookHandlers()`**, which compiled handlers to write and register.
3. **`skillLinkTargets()`**, where org/team skills are symlinked.
4. **`eventNameMap()`**, the native event names the handlers register under.
5. **`additionalFiles()`** (optional), non-hook artifacts such as Hermes' stable MCP bundle copy.

The base supplies everything else: the foreign-preserving config patch, the idempotent `writeJsonIfChanged`, the Honeycomb-entry predicate, skill symlinking, platform detection, and the reversible uninstall. All filesystem access goes through an injectable `ConnectorFs` seam, so the whole connector is testable against an in-memory `FakeFs`, a real `~/.cursor` or `~/.codex` is never touched in a test.

The `ClaudeCodeConnector` is the reference; `CodexConnector` and `CursorConnector` prove the base is subclass-only, each is a small class that fills the four seams and inherits install/uninstall verbatim. Codex uses the same nested matcher-block config shape as Claude Code (`~/.codex/hooks.json`). Cursor overrides the config-shape seams because real Cursor stores each event's handlers as a *flat* array directly under the event key in `~/.cursor/hooks.json`, not the nested `{ matcher, hooks: [...] }` block, but it still inherits the base's foreign-preserve and idempotency guarantees on that flat shape.
The `ClaudeCodeConnector` is the reference. Codex and Cursor adapt their JSON shapes; Hermes overrides the config-text seams for comment-preserving YAML while retaining the base's handler, owned-artifact, idempotency, detection, and skill-link mechanics. Hermes resolves the active profile from `$HERMES_HOME` (default `~/.hermes`).

### Idempotent install, foreign-safe uninstall

Expand All @@ -71,7 +72,7 @@ Each connector reports whether its harness is installed via `detectPlatforms()`,
- **`honeycomb connect <harness>`**, wire exactly one named harness.
- **`honeycomb uninstall [<harness>]`**, reverse only Honeycomb's footprint for one harness, or for every detected harness when no target is given.

The connector registry (`src/cli/connector-runner.ts`, `createConnectorRegistry`) builds each connector over the real `node:fs`-backed `ConnectorFs` and the user's home. Claude Code is wired by registering its marketplace plugin via the real `claude plugin` CLI (rather than writing top-level `settings.json` hooks); Codex and Cursor are wired by the config-patch path. A new harness is a subclass added to the registry, never a fork of install logic.
The connector registry (`src/cli/connector-runner.ts`, `createConnectorRegistry`) builds each connector over the real `node:fs`-backed `ConnectorFs` and the user's home. Claude Code is wired by registering its marketplace plugin via the real `claude plugin` CLI (rather than writing top-level `settings.json` hooks); Codex, Cursor, and Hermes are wired by their native config-patch paths. A new harness is a subclass added to the registry, never a fork of install logic.

## The support matrix

Expand All @@ -82,21 +83,15 @@ Each harness wires the same logical lifecycle events through its own mechanism;
| Claude Code | Supported | Marketplace plugin + hooks + MCP | Reference connector and reference hook set; model-only context, `legacy` runtime path |
| Codex | Supported | `~/.codex/hooks.json` + hooks + MCP | Nested matcher-block config shape; user-visible context; Bash-only VFS intercept |
| Cursor | Supported | `~/.cursor/hooks.json` + extension + MCP | Flat per-event config shape; first-party editor extension; `Shell`-tool VFS intercept; see [`../frontend/cursor-extension-architecture.md`](../frontend/cursor-extension-architecture.md) |
| Hermes | In progress | Planned hook + MCP path | Not wired as a production connector path yet |
| Hermes | Supported | `$HERMES_HOME/config.yaml` shell hooks + MCP | Native 0.19 lifecycle; model-only `pre_llm_call` recall; explicit first-use hook consent |
| pi | In progress | Planned extension + `AGENTS.md` path | Not wired as a production connector path yet |
| OpenClaw | In progress | Planned native-extension path | Not wired as a production connector path yet |

The differences are real but shallow: native event names and payload fields vary, and the context channel is model-only on some harnesses (Claude Code, Cursor, OpenClaw) and user-visible on others (Codex, Hermes, pi), so each shim normalizes before handing off and renders the context block through its harness's channel.
The differences are real but shallow: native event names and payload fields vary, and the context channel is model-only on some harnesses (Claude Code, Cursor, Hermes, OpenClaw) and user-visible on others (Codex, pi), so each shim normalizes before handing off and renders the context block through its harness's channel.

## MCP-server-via-install

For harnesses that speak the Model Context Protocol, the Honeycomb MCP server is registered during install so its `honeycomb_*` tools appear in the harness's native tool list. The server bundle is built by esbuild to `mcp/bundle/server.js` and ships with the package. Hermes, for example, registers it through its `.mcp.json`:

```json
{ "mcpServers": { "honeycomb": { "command": "node", "args": ["mcp/bundle/server.js"] } } }
```

and the Hermes shim appends a user-visible mention so the agent knows the tools exist: `(Honeycomb MCP tools available: honeycomb_search, honeycomb_read, honeycomb_index.)`. The same `node mcp/bundle/server.js` stdio entry registers into the other MCP-speaking harnesses during their connect step. The tool surface, the read/resolve and search/mine clusters, and the registration mechanics are documented in [`mcp-and-sdk.md`](mcp-and-sdk.md).
For harnesses that speak the Model Context Protocol, the Honeycomb MCP server is registered during install so its `honeycomb_*` tools appear in the harness's native tool list. The server bundle is built by esbuild to `mcp/bundle/server.js` and ships with the package. The Hermes connector copies it to `$HERMES_HOME/honeycomb/mcp/server.mjs` and writes a foreign-safe `mcp_servers.honeycomb` stdio entry in `config.yaml`; no repository-local `.mcp.json` is involved. The tool surface, the read/resolve and search/mine clusters, and the registration mechanics are documented in [`mcp-and-sdk.md`](mcp-and-sdk.md).

## The Claude Code plugin: packaging and delivery

Expand Down
Loading
Loading