main
9 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 00799b45c6 |
fix: v0.4.0-gitea.3 — claude/channel capability + notify echo filter + README accuracy
All checks were successful
Test / bun test (pull_request) Successful in 23s
Reno-Stars (airenostars) verified install end-to-end against v0.4.0-gitea.2
ONLY after applying a local patch. This ships the same patch upstream so
fresh self-hosters get a working install out-of-the-box, no per-host
workaround required.
P0 (CRITICAL) — declare experimental.claude/channel capability
The Server constructor previously declared `{ capabilities: { tools: {} } }`.
Without `experimental['claude/channel']` (and the companion
`claude/channel/permission` flag), Claude Code's MCP host treats the
server as tool-only and silently drops every `notifications/claude/channel`
event we emit. Symptom: poll loop runs cleanly, cursor advances, stderr
says "delivered", message never reaches the conversation. Mirrors the
shape used by the official telegram channel plugin's MCP server.
P1 — filter outbound `method=notify` rows in pollWorkspace
The activity feed under `?type=a2a_receive` ALSO returns the agent's own
outbound /notify calls (recorded with method='notify' and source_id=null
on the same workspace_id). emitNotification would classify them as
canvas_user inbound and the reply would echo back as a fake user turn one
poll later — the model would see its own answer as a new user prompt and
try to "respond" to it. Filtered at the per-row layer via a new pure
helper `shouldEmitActivity` so the cursor still advances past the
skipped rows.
P2 — README accuracy
- Drop the `claude --channels plugin:…` one-liner instruction (silently
no-ops on Claude Code 2.1.129; only the marketplace flow works).
- Document `allowedChannelPlugins` schema: it's an array of OBJECTS
`{ plugin, marketplace }`, not strings — the host's Zod validator
silently ignores string entries, which is the most common cause of
"plugin installed but no notifications" reports.
- Document `allowedChannelPlugins` LOCATION: only takes effect from the
managed-settings file (/Library/Application Support/ClaudeCode/
managed-settings.json on macOS, /etc/claude-code/managed-settings.json
on Linux), NOT from `~/.claude/settings.json`. Most self-hosters try
user settings first.
Tests
Added channel-capabilities-and-filter.test.ts (9 cases) that pin both
regressions via two small exported surfaces (`SERVER_CAPABILITIES`,
`shouldEmitActivity`). Verified the new tests fail when each fix is
reverted: removing the experimental block makes 2 tests fail; removing
the notify-method filter makes 2 tests fail. 27 pass / 0 fail (was 18).
Version bump (all four manifests + the Server() literal):
0.4.0-gitea.2 → 0.4.0-gitea.3.
Closes Reno-Stars feedback P0+P1+P2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| b3b79a5efc |
fix(presence): POST /registry/heartbeat ticker so canvas badge stays online (closes #6, closes molecule-core#24)
All checks were successful
Test / bun test (pull_request) Successful in 20s
v0.4.0-gitea.1 polled /workspaces/:id/activity but never sent /registry/heartbeat. The platform's healthsweep (workspace-server/internal/registry/healthsweep.go) flipped any runtime='external' workspace whose last_heartbeat_at was older than 90s back to status='awaiting_agent', so the canvas presence badge stuck on awaiting_agent within 90s of plugin start even while A2A traffic flowed fine over the long-poll loop. Fix: per-workspace heartbeat ticker (default 30s, three ticks inside the 90s stale window) POSTs the minimal HeartbeatPayload shape (workspace_id only) — same path the Python runtime in workspace/heartbeat.py uses when it has nothing else to report. heartbeat.ts pure module + Bun.serve fixture test pin the wire shape (POST + bearer + Origin + workspace_id body) so a future refactor that drops any of those silently re-breaks the badge. Bump 0.4.0-gitea.1 → 0.4.0-gitea.2 and document MOLECULE_HEARTBEAT_INTERVAL_MS in README. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
|||
|
|
ebeb7d9f55 |
fix(install): make Gitea repo install-ready + canonical marketplace flow (#37)
Some checks failed
Test / bun test (pull_request) Failing after 20s
Adds the bits needed for `claude plugin marketplace add https://git.moleculesai.app/molecule-ai/molecule-mcp-claude-channel.git` to actually work, and rewrites the README install section to use the canonical Claude Code marketplace flow (replacing the suspended-GitHub install command). Changes: - New `.claude-plugin/marketplace.json` describing the single-plugin marketplace `molecule-channel` with one plugin entry `molecule`. Required for `claude plugin marketplace add` to recognize this repo. - Bump `.claude-plugin/plugin.json` version `0.1.0` → `0.4.0-gitea.1`. - Bump `package.json` version `0.3.0` → `0.4.0-gitea.1`. (Both versions were already drifted; this reconciles them.) - README install section rewritten: - Old: `claude --channels plugin:molecule@Molecule-AI/molecule-mcp-claude-channel` (suspended-GitHub URL, no longer resolves) - New: `claude plugin marketplace add https://git.moleculesai.app/...` + `claude plugin install molecule@molecule-channel` (canonical marketplace flow per Claude Code docs) - README "Contributing" issues link migrated github.com → Gitea. - Migration note for users on the old install path: GitHub Molecule-AI org permanently gone 2026-05-06; new path is direct replacement, behavior unchanged. Version `0.4.0-gitea.1` admits the lineage gap with the never-recovered-from-GitHub `v0.4.x` work; preserves user expectation of a 0.4.x release. Annotated tag to follow this PR (re-tag is cheap if Hongming prefers a different version). Refs: molecule-ai/internal#37, molecule-ai/internal#38 |
||
|
|
68d8ae981f |
test(extractText): bootstrap bun:test + pin v0/v1 part discriminator
Extracts the extractText helper + ActivityEntry type into their own module so unit tests can import them without dragging server.ts's top-level boot side-effects (cursor load, MCP transport connect, poll loop) into the test runner. server.ts re-imports both — the wire behavior is unchanged. Tests cover the v0/v1 part-discriminator regression that landed on 2026-04-30 (every canvas peer message returning act.summary because parts had `kind` instead of `type`): - v1 `kind: text` parts (current production shape) - v0 `type: text` back-compat - multi-part text join, ignore non-text parts - body-shape priority: params.message.parts > params.parts > body.parts - empty-text-part recovery: skips empty candidate, tries next - summary fallback when no shape matches - `(empty A2A message)` fallback when summary is null Adds: - extract-text.ts (helper + type, no side-effects) - extract-text.test.ts (9 tests) - .github/workflows/test.yml (bun test on push/PR) - "test": "bun test" script in package.json Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
bcc7db5c86 |
feat(v0.3): mirror universal-MCP surface for external workspaces
Brings the channel's tool surface up to parity with the in-container
universal MCP (workspace/platform_tools/registry.py). External agents
driven through this channel now get the same 9 tools that an in-container
agent gets — same names, same input shapes, same semantics — so a hermes
or codex session bridged via this channel can do anything a containerized
claude-code can.
Tools:
- reply_to_workspace — smart-routed (canvas_user → /notify, peer_agent → /a2a).
Was peer-only; now handles canvas chat replies too. Works for the most
common case (user types in My Chat, agent replies in My Chat) which the
v0.2 channel silently dropped.
- delegate_task / delegate_task_async / check_task_status — proactive A2A.
Previously only inbound peer messages could trigger an outbound a2a;
the agent could not initiate.
- list_peers — peer discovery. Backed by GET /registry/:id/peers.
- get_workspace_info — self-introspection. Needed for memory scope checks
(only tier-0 roots can write GLOBAL).
- send_message_to_user — canvas push with attachments. Multipart upload via
/chat/uploads then /notify, mirrors the universal tool's two-phase shape.
- commit_memory / recall_memory — persistent memory across sessions, with
LOCAL/TEAM/GLOBAL scopes. Platform enforces RBAC + scope.
Inbound delivery:
- Drop seed-then-skip on first run. The previous policy assumed events
before session start were "out of context"; in practice operators
restart Claude Code mid-conversation and EXPECT the queued messages.
Cold start now delivers everything in POLL_WINDOW_SECS and advances
the cursor past it.
- emitNotification adds meta.kind ('canvas_user' | 'peer_agent') so
Claude can pick the right reply form without re-parsing the row.
Channel-specific:
- New tools take an optional _as_workspace param to disambiguate when
watching multiple workspaces. Underscore-prefixed so it can't collide
with the universal contract (which is bound to a single workspace).
|
||
|
|
fef6a6210e |
fix: probe ordering + PID file cleanup on exit(2) (v0.2.2)
Independent five-axis review of v0.2.1 surfaced three issues. All
three only manifest after the upgrade-or-die path actually fires,
which is rare in normal operation but exactly the failure mode the
probe was added for — so quiet correctness matters.
1. **CRITICAL: process.exit(2) orphans PID file → cross-process-kill
hazard.** v0.2.1 added a fatal exit on too_old probe response, but
left the PID file pointing at this (about-to-die) process. Next
launch reads PID_FILE, calls `process.kill(stale, 'SIGTERM')` —
the dead PID is now likely owned by an unrelated process, so the
plugin SIGTERMs whatever happens to be there. This is exactly the
bug the singleton lock dance was designed to prevent. Fix: install
a `process.on('exit')` listener that unlinks PID_FILE iff this
process still owns it (handles all exit paths: clean shutdown,
probe failure, unhandledException, etc.). Listener uses sync I/O
(only path 'exit' permits) and is no-op-safe if another process
has already taken ownership of the file.
2. **Probe ran AFTER mcp.connect(transport).** Claude Code had
already finished the MCP initialize handshake when the probe
detected too_old and exited; from Claude's perspective this looked
like "MCP server crashed mid-session" — the carefully-written
stderr explanation isn't surfaced. Probe must run BEFORE
mcp.connect so the failure mode is "server failed to start" with
stderr visible to the user.
3. **Probe ran AFTER registerAsPoll().** If the platform predates
delivery_mode=poll AND since_id (consistent — both shipped under
#2339), registerAsPoll() may have already mutated the workspace's
delivery_mode on the platform. Then the probe fails and we exit,
leaving the workspace in a broken half-configured state. Probe
must run BEFORE registerAsPoll for the same reason it must run
before mcp.connect — fail without side effects.
Reordered boot sequence:
loadCursors → probeCursorSupport (parallel via allSettled) →
mcp.connect → registerAsPoll → poll loop.
Also parallelized the probe with Promise.allSettled — sequentially
it was up to N × 15s on a hung platform, which adds up for
multi-workspace channels.
README updated to document that 401/403/404/5xx from the probe are
treated as inconclusive (orthogonal to cursor support — usually
auth or transient) and the plugin continues; this avoids users
debugging a phantom probe failure when the real issue is their
token.
Bumps to 0.2.2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
8da0c47955 |
feat: probe cursor support at startup (v0.2.1)
Plugin v0.2 polls /activity?since_id=<cursor>. Older platforms (pre molecule-core#2354) silently ignore the param and return whatever the default time window covers — every poll then re-delivers the same activities to Claude as fresh turns. That's a worse failure mode than any v0.1 bug. Add a startup probe: send since_id=00000000-…-000000000000 (the all-zero UUID, which gen_random_uuid() can never produce per RFC 4122 §4.4 so a valid 410 is unambiguous). #2354+ answers 410 Gone for any unknown cursor; pre-#2354 answers 200 OK. On 200 the plugin exits with code 2 and a clear "upgrade your platform or downgrade the plugin" message. 401/403/404/5xx are inconclusive (orthogonal to cursor support) — log a warning, let the normal poll loop surface the real failure. Bumps package.json + the MCP server-name version string to 0.2.1 (the 0.1.0 in the Server constructor was already stale, fixed at the same time). README's compat section documents the probe + exit code so users debugging an immediate exit-code-2 know what's going on. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
18657ebf59 |
feat: v0.2 — cursor-based polling + auto-register as poll-mode
Switches from the v0.1 since_secs+seenIds time-window scheme to a
Telegram-shaped since_id cursor (Molecule-AI/molecule-core#2354), and
adds a startup self-register that sets the workspace's delivery_mode
to poll (Molecule-AI/molecule-core#2353) so the platform's a2a_proxy
short-circuits inbound A2A into activity_logs without requiring a
public URL.
What changed:
- Cursor persistence: ~/.claude/channels/molecule/cursor.json, atomic
temp+rename writes, chmod 600. Survives restarts; no replay window,
no in-memory dedup growth.
- pollWorkspace: first run seeds cursor from most-recent without
delivering (events that predate the session are out of context).
Steady state uses ?since_id=<cursor>; ASC-ordered rows strictly
after cursor get delivered in recorded order. 410 → drop cursor +
re-seed next tick.
- registerAsPoll: POST /registry/register {delivery_mode:"poll",
agent_card:{name,description}} for each watched workspace on boot.
Idempotent (upsert). Skipped when MOLECULE_AUTO_REGISTER_POLL=false
for users who configured the workspace another way.
- New env vars: MOLECULE_AGENT_NAME, MOLECULE_AGENT_DESC,
MOLECULE_AUTO_REGISTER_POLL.
- README: cursor section + compat bumped to require molecule-core
PRs #2348/#2353/#2354 (issue #2339).
Compat: requires platform to ship #2339. Plugin works against older
platforms only if you set MOLECULE_AUTO_REGISTER_POLL=false and the
workspace already has delivery_mode=poll set another way.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
d07363cbe5 |
feat: initial scaffold — Molecule channel plugin for Claude Code
Bridges Molecule A2A traffic into a Claude Code session via MCP. Inbound A2A messages from watched workspaces surface as conversation turns (notifications/claude/channel); replies route back through the existing POST /workspaces/:id/a2a endpoint via the reply_to_workspace MCP tool. Architecture: - Polling-based inbound (uses /activity?since_secs= shipped in molecule-core PR #2300). Works through every NAT/firewall, no tunnel required — optimized for laptop-launched Claude Code sessions vs the existing push-based external-agent flow that needs ngrok. - Per-workspace bearer auth (MOLECULE_WORKSPACE_TOKENS, comma-separated to match MOLECULE_WORKSPACE_IDS). Same token covers /activity (read) and /a2a (write). - Singleton lock at ~/.claude/channels/molecule/bot.pid prevents two channel servers racing the dedup state. - Dedup by activity.id; 30s overlap window over a 5s poll interval protects against missed ticks (laptop sleep, transient network blips). v0.1 ships: - .claude-plugin/plugin.json, .mcp.json, package.json, LICENSE (Apache-2.0) - server.ts: MCP server with notification emission + reply_to_workspace tool - README: install + .env config + architecture notes + v0.2 roadmap v0.1 explicit non-goals (tracked in README): - No push-mode inbound (requires tunnel; deferred to v0.2) - No pairing flow (manual .env tokens; canvas pairing in v0.2) - No file-attachment download (URLs surface in meta; host fetches on-demand) - No outbound channel-init (only replies; start_workspace_chat in v0.2) Mirrors the architecture of @claude-plugins-official/telegram v0.0.6 (MCP notification contract: notifications/claude/channel with {content, meta}) so the host's existing channel-handling logic works without custom adapters. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |