@sym-bot/mesh-channel 0.3.20 → 0.3.22
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/.claude-plugin/plugin.json +1 -1
- package/.mcp.json +1 -1
- package/CHANGELOG.md +12 -0
- package/package.json +2 -2
- package/server.js +48 -28
package/.mcp.json
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.22
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Per-project identity via `$CLAUDE_PROJECT_DIR/.sym/node.json`.** A named role agent (e.g. a CTO node `claude-code-mac` on `sym-bot-team`, or `melotune-dev` on `melo-ios`) can now commit `{ "node_name": "...", "group": "..." }` to `.sym/node.json` in its repo, and the plugin reads it on start. This lets the **plugin alone** carry a stable, per-project identity — no parallel `claude-sym-mesh` MCP registration in a project `.mcp.json`, which previously produced a *second* mesh node (the project-scoped server plus the plugin-scoped server are never deduplicated). Because the identity lives in the repo, it survives a plugin reinstall. Precedence is unchanged-and-extended: `SYM_NODE_NAME`/`SYM_GROUP` env still win, then `.sym/node.json`, then the auto `claude-<repo>-<session>` default. A missing or malformed file is ignored (falls back to the auto default) — never a hard fail.
|
|
8
|
+
|
|
9
|
+
## 0.3.21
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- **`sym_inbox` is now a thin adapter over the SDK primitive `node.inbox()`** (`@sym-bot/sym` ^0.7.11). The pull-based receive buffer + drain cursor moved down into the node, where it belongs alongside `node.remember()` (send) — so the SDK is sufficient for send **and** pull on its own, and the wrapper owns no buffering logic. Behaviour is unchanged (FIFO drain, `peek`, `limit`); `sym_fetch` now also resolves SDK inbox ids (`inNNNN`). The wrapper still applies the peer allowlist + prompt-injection filter on the pull path before anything enters context.
|
|
14
|
+
|
|
3
15
|
## 0.3.20
|
|
4
16
|
|
|
5
17
|
### Added
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sym-bot/mesh-channel",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.22",
|
|
4
4
|
"description": "MCP server — real-time agent-to-agent cognition for Claude Code remote teams via the SYM mesh.",
|
|
5
5
|
"main": "server.js",
|
|
6
6
|
"bin": {
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
],
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
25
|
-
"@sym-bot/sym": "^0.7.
|
|
25
|
+
"@sym-bot/sym": "^0.7.11",
|
|
26
26
|
"bonjour-service": "^1.3.0"
|
|
27
27
|
},
|
|
28
28
|
"engines": {
|
package/server.js
CHANGED
|
@@ -232,7 +232,23 @@ function resolveNodeName(base) {
|
|
|
232
232
|
}
|
|
233
233
|
return base;
|
|
234
234
|
}
|
|
235
|
-
|
|
235
|
+
// Per-project identity (v0.3.22): a named role agent (CTO, melotune-dev, …) commits
|
|
236
|
+
// its node name + group to `$CLAUDE_PROJECT_DIR/.sym/node.json`, so the plugin alone
|
|
237
|
+
// carries a stable per-project identity — no parallel MCP registration, and it
|
|
238
|
+
// survives a plugin reinstall because the config lives in the repo, not the plugin.
|
|
239
|
+
// Env (SYM_NODE_NAME/SYM_GROUP) still wins; this only overrides the auto default.
|
|
240
|
+
// Missing/malformed file → {} → auto default; never a hard fail.
|
|
241
|
+
function projectNodeConfig() {
|
|
242
|
+
const fs = require('fs'), path = require('path');
|
|
243
|
+
const dir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
244
|
+
try {
|
|
245
|
+
const cfg = JSON.parse(fs.readFileSync(path.join(dir, '.sym', 'node.json'), 'utf8'));
|
|
246
|
+
const clean = (s) => (typeof s === 'string' && s.trim()) ? s.trim() : undefined;
|
|
247
|
+
return { node_name: clean(cfg.node_name), group: clean(cfg.group) };
|
|
248
|
+
} catch { return {}; }
|
|
249
|
+
}
|
|
250
|
+
const PROJECT_CFG = projectNodeConfig();
|
|
251
|
+
const NODE_NAME = resolveNodeName(process.env.SYM_NODE_NAME || PROJECT_CFG.node_name || defaultNodeName());
|
|
236
252
|
|
|
237
253
|
// ── Mesh group (MMP §5.8) ──────────────────────────────────
|
|
238
254
|
//
|
|
@@ -245,7 +261,7 @@ const NODE_NAME = resolveNodeName(process.env.SYM_NODE_NAME || defaultNodeName()
|
|
|
245
261
|
function resolveServiceType() {
|
|
246
262
|
const explicit = process.env.SYM_SERVICE_TYPE;
|
|
247
263
|
if (explicit) return explicit;
|
|
248
|
-
const group = process.env.SYM_GROUP;
|
|
264
|
+
const group = process.env.SYM_GROUP || PROJECT_CFG.group;
|
|
249
265
|
if (group && group !== 'default') return `_${group}._tcp`;
|
|
250
266
|
return '_sym._tcp';
|
|
251
267
|
}
|
|
@@ -253,7 +269,7 @@ function resolveServiceType() {
|
|
|
253
269
|
// Claude Code restart. Declaring as `let` rather than `const` is the
|
|
254
270
|
// smallest change that makes hot-swap possible.
|
|
255
271
|
let SERVICE_TYPE = resolveServiceType();
|
|
256
|
-
let GROUP = process.env.SYM_GROUP || (SERVICE_TYPE !== '_sym._tcp'
|
|
272
|
+
let GROUP = process.env.SYM_GROUP || PROJECT_CFG.group || (SERVICE_TYPE !== '_sym._tcp'
|
|
257
273
|
? SERVICE_TYPE.replace(/^_/, '').replace(/\._tcp$/, '')
|
|
258
274
|
: 'default');
|
|
259
275
|
let RELAY_URL = process.env.SYM_RELAY_URL || null;
|
|
@@ -651,6 +667,12 @@ mcp.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
651
667
|
}
|
|
652
668
|
|
|
653
669
|
case 'sym_fetch': {
|
|
670
|
+
// inNNNN → SDK delivery inbox (pull path); mNNN → channel-push store.
|
|
671
|
+
if (typeof args.msg_id === 'string' && args.msg_id.startsWith('in')) {
|
|
672
|
+
const m = node.inboxGet(args.msg_id);
|
|
673
|
+
if (!m) return { content: [{ type: 'text', text: `Message ${args.msg_id} not found (expired or invalid ID).` }] };
|
|
674
|
+
return { content: [{ type: 'text', text: `[${m.from}] ${new Date(m.receivedAt).toISOString()}\n\n${m.content}` }] };
|
|
675
|
+
}
|
|
654
676
|
const entry = MESSAGE_STORE.get(args.msg_id);
|
|
655
677
|
if (!entry) {
|
|
656
678
|
return { content: [{ type: 'text', text: `Message ${args.msg_id} not found (expired or invalid ID).` }] };
|
|
@@ -664,35 +686,34 @@ mcp.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
664
686
|
}
|
|
665
687
|
|
|
666
688
|
case 'sym_inbox': {
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
if (seq > inboxCursor) fresh.push({ id, seq, entry });
|
|
672
|
-
}
|
|
673
|
-
fresh.sort((a, b) => a.seq - b.seq);
|
|
674
|
-
// FIFO: drain the OLDEST `limit` first so the cursor advances contiguously
|
|
675
|
-
// and a backlog larger than `limit` is never skipped — the rest come on the
|
|
676
|
-
// next call. (Newest-first would jump the cursor past un-returned items.)
|
|
677
|
-
const slice = fresh.slice(0, limit);
|
|
678
|
-
if (!args.peek && slice.length) {
|
|
679
|
-
inboxCursor = Math.max(inboxCursor, ...slice.map((i) => i.seq));
|
|
680
|
-
}
|
|
681
|
-
if (!slice.length) {
|
|
689
|
+
// Thin adapter over the SDK primitive: the node owns the delivery buffer
|
|
690
|
+
// + drain cursor (node.inbox()). This wrapper only formats for display.
|
|
691
|
+
const { messages, remaining } = node.inbox({ peek: !!args.peek, limit: args.limit });
|
|
692
|
+
if (!messages.length) {
|
|
682
693
|
return { content: [{ type: 'text', text: 'Inbox empty — no new mesh messages since your last check.' }] };
|
|
683
694
|
}
|
|
684
|
-
const more = fresh.length - slice.length;
|
|
685
695
|
const now = Date.now();
|
|
686
|
-
const lines =
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
696
|
+
const lines = messages.map((m) => {
|
|
697
|
+
if (m.from === NODE_NAME) return null; // never surface our own deliveries
|
|
698
|
+
// The security layer still gates the pull path: peer allowlist +
|
|
699
|
+
// prompt-injection filter run on every message before it enters context.
|
|
700
|
+
if (!isPeerAllowed(m.from)) return null;
|
|
701
|
+
const sec = checkSecurity(m.from, m.fields || {}, m.fields?.payload);
|
|
702
|
+
if (!sec.safe) { securityAudit(sec.reason, m.from, sec.excerpt); return null; }
|
|
703
|
+
const age = Math.round((now - m.receivedAt) / 1000);
|
|
704
|
+
const focus = m.fields?.focus?.text || m.content || '';
|
|
705
|
+
const dirTag = m.directed ? ' →you' : '';
|
|
706
|
+
const memTag = m.directed && m.remixed === false ? ' ·not-stored' : '';
|
|
707
|
+
return `[${m.from}${dirTag}] ${String(focus).replace(/\s+/g, ' ').slice(0, 90)}${memTag} [${m.id}] (${age}s ago)`;
|
|
708
|
+
}).filter(Boolean);
|
|
709
|
+
if (!lines.length) {
|
|
710
|
+
return { content: [{ type: 'text', text: 'Inbox empty — no new mesh messages since your last check.' }] };
|
|
711
|
+
}
|
|
712
|
+
const moreNote = remaining > 0 ? ` (+${remaining} more — call sym_inbox again)` : '';
|
|
692
713
|
return {
|
|
693
714
|
content: [{
|
|
694
715
|
type: 'text',
|
|
695
|
-
text: `${
|
|
716
|
+
text: `${lines.length} new mesh message(s)${args.peek ? ' (peek — not drained)' : ''}${moreNote}:\n${lines.join('\n')}\n\nUse sym_fetch <id> for full content; reply via sym_send to=<peer>.`,
|
|
696
717
|
}],
|
|
697
718
|
};
|
|
698
719
|
}
|
|
@@ -909,9 +930,8 @@ mcp.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
909
930
|
// Per COO spec cmb_compact_channel_v0.1.md: push compact headers,
|
|
910
931
|
// store full content for on-demand sym_fetch retrieval. ~10% token
|
|
911
932
|
// savings on mesh traffic without context loss.
|
|
912
|
-
const MESSAGE_STORE = new Map();
|
|
933
|
+
const MESSAGE_STORE = new Map(); // channel-push surface (mNNN) for sym_fetch when channels are enabled
|
|
913
934
|
let msgSeq = 0;
|
|
914
|
-
let inboxCursor = 0; // highest msgSeq drained by sym_inbox (pull-based receive)
|
|
915
935
|
const MAX_STORED = 200;
|
|
916
936
|
|
|
917
937
|
function storeMessage(from, content, header) {
|