@sym-bot/sym 0.2.1 → 0.2.2
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/setup-claude.sh +82 -46
- package/bin/sym-daemon.js +21 -1
- package/integrations/claude-code/mcp-server.js +12 -33
- package/integrations/telegram/bot.js +6 -3
- package/lib/ipc-client.js +7 -21
- package/lib/node.js +1 -0
- package/package.json +1 -1
package/bin/setup-claude.sh
CHANGED
|
@@ -1,30 +1,83 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
# SYM — Setup for Claude Code
|
|
3
|
-
#
|
|
4
|
-
#
|
|
3
|
+
#
|
|
4
|
+
# Installs:
|
|
5
|
+
# 1. sym-daemon (persistent physical mesh node, launchd LaunchAgent)
|
|
6
|
+
# 2. MCP server for Claude Code (virtual node, connects to daemon via IPC)
|
|
7
|
+
# 3. Auto-approves sym_mood tool
|
|
8
|
+
# 4. CLAUDE.md instructions for autonomous mood detection
|
|
9
|
+
#
|
|
10
|
+
# Usage:
|
|
11
|
+
# npx @sym-bot/sym setup # or
|
|
12
|
+
# ./bin/setup-claude.sh [project-dir]
|
|
5
13
|
|
|
6
14
|
set -e
|
|
7
15
|
|
|
8
16
|
SYM_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
9
17
|
MCP_SERVER="$SYM_DIR/integrations/claude-code/mcp-server.js"
|
|
18
|
+
DAEMON_SCRIPT="$SYM_DIR/bin/sym-daemon.js"
|
|
10
19
|
|
|
11
|
-
echo "
|
|
12
|
-
echo "
|
|
20
|
+
echo ""
|
|
21
|
+
echo " SYM Setup for Claude Code"
|
|
22
|
+
echo " ========================="
|
|
23
|
+
echo ""
|
|
24
|
+
|
|
25
|
+
# ── Step 1: Configure relay ─────────────────────────────────
|
|
26
|
+
|
|
27
|
+
RELAY_ENV="$HOME/.sym/relay.env"
|
|
28
|
+
mkdir -p "$HOME/.sym"
|
|
29
|
+
|
|
30
|
+
if [ -f "$RELAY_ENV" ]; then
|
|
31
|
+
echo " ✓ Relay config found: $RELAY_ENV"
|
|
32
|
+
else
|
|
33
|
+
echo " WebSocket Relay (connects your mesh across the internet)"
|
|
34
|
+
echo ""
|
|
35
|
+
read -p " Relay URL (e.g. wss://sym-relay.onrender.com, or empty to skip): " RELAY_URL
|
|
36
|
+
if [ -n "$RELAY_URL" ]; then
|
|
37
|
+
read -p " Relay token (or empty for open access): " RELAY_TOKEN
|
|
38
|
+
echo "SYM_RELAY_URL=$RELAY_URL" > "$RELAY_ENV"
|
|
39
|
+
if [ -n "$RELAY_TOKEN" ]; then
|
|
40
|
+
echo "SYM_RELAY_TOKEN=$RELAY_TOKEN" >> "$RELAY_ENV"
|
|
41
|
+
fi
|
|
42
|
+
echo " ✓ Relay config saved: $RELAY_ENV"
|
|
43
|
+
else
|
|
44
|
+
echo " → Skipping relay — using Bonjour (local network) only"
|
|
45
|
+
fi
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
# ── Step 2: Install sym-daemon ──────────────────────────────
|
|
49
|
+
|
|
50
|
+
echo ""
|
|
51
|
+
echo " Installing sym-daemon (persistent mesh node)..."
|
|
13
52
|
|
|
14
|
-
|
|
15
|
-
|
|
53
|
+
if [ "$(uname)" = "Darwin" ]; then
|
|
54
|
+
# macOS: install as launchd LaunchAgent
|
|
55
|
+
node "$DAEMON_SCRIPT" --install
|
|
56
|
+
echo " ✓ sym-daemon installed as launchd LaunchAgent"
|
|
57
|
+
echo " Auto-starts on login, auto-restarts on crash"
|
|
58
|
+
echo " Logs: ~/Library/Logs/sym-daemon/"
|
|
59
|
+
else
|
|
60
|
+
echo " → macOS not detected. Start the daemon manually:"
|
|
61
|
+
echo " node $DAEMON_SCRIPT"
|
|
62
|
+
echo " (On Linux, create a systemd service)"
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
# ── Step 3: Add MCP server ──────────────────────────────────
|
|
66
|
+
|
|
67
|
+
echo ""
|
|
68
|
+
echo " Adding SYM MCP server to Claude Code..."
|
|
16
69
|
claude mcp add --transport stdio sym --scope user -- node "$MCP_SERVER"
|
|
70
|
+
echo " ✓ MCP server registered"
|
|
17
71
|
|
|
18
|
-
#
|
|
19
|
-
|
|
72
|
+
# ── Step 4: Auto-approve sym_mood ───────────────────────────
|
|
73
|
+
|
|
74
|
+
echo ""
|
|
75
|
+
echo " Auto-approving sym_mood tool..."
|
|
20
76
|
CLAUDE_JSON="$HOME/.claude.json"
|
|
21
77
|
if [ -f "$CLAUDE_JSON" ]; then
|
|
22
|
-
# Use node to safely modify JSON
|
|
23
78
|
node -e "
|
|
24
79
|
const fs = require('fs');
|
|
25
80
|
const config = JSON.parse(fs.readFileSync('$CLAUDE_JSON', 'utf8'));
|
|
26
|
-
|
|
27
|
-
// Find all project entries and add sym_mood to allowedTools
|
|
28
81
|
if (config.projects) {
|
|
29
82
|
for (const [path, project] of Object.entries(config.projects)) {
|
|
30
83
|
if (!project.allowedTools) project.allowedTools = [];
|
|
@@ -33,53 +86,36 @@ if [ -f "$CLAUDE_JSON" ]; then
|
|
|
33
86
|
}
|
|
34
87
|
}
|
|
35
88
|
}
|
|
36
|
-
|
|
37
89
|
fs.writeFileSync('$CLAUDE_JSON', JSON.stringify(config, null, 2));
|
|
38
|
-
console.log(' sym_mood auto-approved for all projects');
|
|
39
90
|
"
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
# 3. Configure relay (optional)
|
|
43
|
-
echo ""
|
|
44
|
-
echo "WebSocket Relay (optional)"
|
|
45
|
-
echo "Connects your mesh across the internet — not just local network."
|
|
46
|
-
echo ""
|
|
47
|
-
|
|
48
|
-
SHELL_RC="$HOME/.zshrc"
|
|
49
|
-
[ -f "$HOME/.bashrc" ] && [ ! -f "$HOME/.zshrc" ] && SHELL_RC="$HOME/.bashrc"
|
|
50
|
-
|
|
51
|
-
if grep -q "SYM_RELAY_URL" "$SHELL_RC" 2>/dev/null; then
|
|
52
|
-
echo " Relay already configured in $SHELL_RC"
|
|
91
|
+
echo " ✓ sym_mood auto-approved"
|
|
53
92
|
else
|
|
54
|
-
|
|
55
|
-
if [ -n "$RELAY_URL" ]; then
|
|
56
|
-
read -p "Relay token (leave empty for open access): " RELAY_TOKEN
|
|
57
|
-
echo "" >> "$SHELL_RC"
|
|
58
|
-
echo "# SYM Mesh" >> "$SHELL_RC"
|
|
59
|
-
echo "export SYM_RELAY_URL=\"$RELAY_URL\"" >> "$SHELL_RC"
|
|
60
|
-
if [ -n "$RELAY_TOKEN" ]; then
|
|
61
|
-
echo "export SYM_RELAY_TOKEN=\"$RELAY_TOKEN\"" >> "$SHELL_RC"
|
|
62
|
-
fi
|
|
63
|
-
echo " Relay configured in $SHELL_RC"
|
|
64
|
-
echo " Run 'source $SHELL_RC' or restart your terminal to activate."
|
|
65
|
-
else
|
|
66
|
-
echo " Skipping relay — using Bonjour (local network) only."
|
|
67
|
-
fi
|
|
93
|
+
echo " → No .claude.json found — approve sym_mood manually when prompted"
|
|
68
94
|
fi
|
|
69
95
|
|
|
70
|
-
#
|
|
96
|
+
# ── Step 5: Install CLAUDE.md ───────────────────────────────
|
|
97
|
+
|
|
71
98
|
echo ""
|
|
72
|
-
echo "Installing CLAUDE.md instructions..."
|
|
73
99
|
PROJECT_DIR="${1:-$(pwd)}"
|
|
74
100
|
CLAUDE_MD="$PROJECT_DIR/CLAUDE.md"
|
|
75
101
|
|
|
76
102
|
if [ -f "$CLAUDE_MD" ] && grep -q "SYM Mesh Agent" "$CLAUDE_MD"; then
|
|
77
|
-
echo " CLAUDE.md already has SYM instructions"
|
|
103
|
+
echo " ✓ CLAUDE.md already has SYM instructions"
|
|
78
104
|
else
|
|
79
105
|
cat "$SYM_DIR/CLAUDE.md" >> "$CLAUDE_MD"
|
|
80
|
-
echo " Added SYM instructions to $CLAUDE_MD"
|
|
106
|
+
echo " ✓ Added SYM instructions to $CLAUDE_MD"
|
|
81
107
|
fi
|
|
82
108
|
|
|
109
|
+
# ── Done ────────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
echo ""
|
|
112
|
+
echo " ──────────────────────────────────────"
|
|
113
|
+
echo " SYM is ready."
|
|
114
|
+
echo ""
|
|
115
|
+
echo " • sym-daemon: running (check: node $DAEMON_SCRIPT --status)"
|
|
116
|
+
echo " • MCP server: registered (restart Claude Code to activate)"
|
|
117
|
+
echo " • Protocol spec: https://sym.bot/protocol"
|
|
118
|
+
echo ""
|
|
119
|
+
echo " Say 'I'm exhausted' in Claude Code —"
|
|
120
|
+
echo " MeloTune will start playing calming music."
|
|
83
121
|
echo ""
|
|
84
|
-
echo "Done. Restart Claude Code to activate SYM."
|
|
85
|
-
echo "Say 'I'm exhausted' — MeloTune will start playing."
|
package/bin/sym-daemon.js
CHANGED
|
@@ -42,7 +42,9 @@ const { SymNode } = require('../lib/node');
|
|
|
42
42
|
// ── Configuration ──────────────────────────────────────────────
|
|
43
43
|
|
|
44
44
|
const SOCKET_PATH = process.env.SYM_SOCKET || '/tmp/sym.sock';
|
|
45
|
-
|
|
45
|
+
// Stable name: use SYM_NODE_NAME env, or 'sym-daemon' (not hostname — macOS
|
|
46
|
+
// appends random suffixes to hostname on WiFi, causing new identity each restart)
|
|
47
|
+
const NODE_NAME = process.env.SYM_NODE_NAME || 'sym-daemon';
|
|
46
48
|
const LOG_DIR = path.join(os.homedir(), 'Library', 'Logs', 'sym-daemon');
|
|
47
49
|
|
|
48
50
|
// Load relay config from ~/.sym/relay.env if env vars not set
|
|
@@ -239,6 +241,24 @@ function forwardEventsToVirtualNodes() {
|
|
|
239
241
|
|
|
240
242
|
node.on('message', (from, content) => {
|
|
241
243
|
broadcastToVirtualNodes({ type: 'event', event: 'message', data: { from, content } });
|
|
244
|
+
|
|
245
|
+
// Wake sleeping peers that might need this message.
|
|
246
|
+
// The daemon acts as wake proxy — it has APNs keys and gossiped wake channels.
|
|
247
|
+
// When a remote peer (Telegram bot) sends a message, and a local peer (MeloTune)
|
|
248
|
+
// is sleeping, the daemon wakes it so the relay can deliver the message.
|
|
249
|
+
node._wakeSleepingPeers('message', {
|
|
250
|
+
type: 'message', from: node._identity.nodeId, fromName: node.name,
|
|
251
|
+
content, timestamp: Date.now(),
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
node.on('mood-accepted', (data) => {
|
|
256
|
+
// Also wake on mood — daemon may receive mood from a remote peer
|
|
257
|
+
// that a sleeping local peer should hear
|
|
258
|
+
node._wakeSleepingPeers('mood', {
|
|
259
|
+
type: 'mood', from: node._identity.nodeId, fromName: node.name,
|
|
260
|
+
mood: data.mood, timestamp: Date.now(),
|
|
261
|
+
});
|
|
242
262
|
});
|
|
243
263
|
|
|
244
264
|
node.on('memory-received', ({ from, entry, decision }) => {
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
const { McpServer } = require('@modelcontextprotocol/sdk/server/mcp.js');
|
|
16
16
|
const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
|
|
17
17
|
const { z } = require('zod');
|
|
18
|
-
const {
|
|
18
|
+
const { connectToDaemon } = require('../../lib/ipc-client');
|
|
19
19
|
const path = require('path');
|
|
20
20
|
const fs = require('fs');
|
|
21
21
|
|
|
@@ -34,32 +34,11 @@ if (!process.env.SYM_RELAY_URL) {
|
|
|
34
34
|
|
|
35
35
|
// ── Node Connection ──────────────────────────────────────────
|
|
36
36
|
|
|
37
|
-
let node;
|
|
38
|
-
let isDaemon; // true if connected to daemon
|
|
39
|
-
let bridge; // ClaudeMemoryBridge (only for standalone mode)
|
|
37
|
+
let node; // SymDaemonClient — connected to sym-daemon via IPC
|
|
40
38
|
|
|
41
39
|
async function initNode() {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
silent: true,
|
|
45
|
-
relay: process.env.SYM_RELAY_URL || null,
|
|
46
|
-
relayToken: process.env.SYM_RELAY_TOKEN || null,
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
node = result.node;
|
|
50
|
-
isDaemon = result.isDaemon;
|
|
51
|
-
|
|
52
|
-
if (isDaemon) {
|
|
53
|
-
process.stderr.write('[SYM MCP] Connected to sym-daemon as virtual node\n');
|
|
54
|
-
} else {
|
|
55
|
-
process.stderr.write('[SYM MCP] Daemon not running — standalone mode\n');
|
|
56
|
-
const { ClaudeMemoryBridge } = require('../../lib/claude-memory-bridge');
|
|
57
|
-
bridge = new ClaudeMemoryBridge(node);
|
|
58
|
-
await node.start();
|
|
59
|
-
bridge.start();
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// Listen for mesh signals (works for both daemon client and standalone node)
|
|
40
|
+
node = await connectToDaemon({ name: 'claude-code' });
|
|
41
|
+
process.stderr.write('[SYM MCP] Connected to sym-daemon as virtual node\n');
|
|
63
42
|
setupMeshSignalHandlers();
|
|
64
43
|
}
|
|
65
44
|
|
|
@@ -74,7 +53,7 @@ server.tool(
|
|
|
74
53
|
async ({ content, tags }) => {
|
|
75
54
|
const tagList = tags ? tags.split(',').map(t => t.trim()).filter(Boolean) : [];
|
|
76
55
|
const entry = node.remember(content, { tags: tagList.length > 0 ? tagList : undefined });
|
|
77
|
-
const peers =
|
|
56
|
+
const peers = await node.peers();
|
|
78
57
|
const coupled = peers.filter(p => p.coupling !== 'rejected');
|
|
79
58
|
return { content: [{ type: 'text', text: `Stored and shared with ${coupled.length}/${peers.length} peer(s). Key: ${entry.key}` }] };
|
|
80
59
|
}
|
|
@@ -86,7 +65,7 @@ server.tool(
|
|
|
86
65
|
{ query: z.string() },
|
|
87
66
|
async ({ query }) => {
|
|
88
67
|
// 1. Mesh memories
|
|
89
|
-
const results =
|
|
68
|
+
const results = await node.recall(query);
|
|
90
69
|
const lines = results.map(r => {
|
|
91
70
|
const source = r._source || r.source || 'unknown';
|
|
92
71
|
const t = (r.tags || []).length > 0 ? ` (tags: ${r.tags.join(', ')})` : '';
|
|
@@ -109,7 +88,7 @@ server.tool(
|
|
|
109
88
|
'Show connected peers with coupling state and drift.',
|
|
110
89
|
{},
|
|
111
90
|
async () => {
|
|
112
|
-
const peers =
|
|
91
|
+
const peers = await node.peers();
|
|
113
92
|
if (peers.length === 0) {
|
|
114
93
|
return { content: [{ type: 'text', text: 'No peers connected.' }] };
|
|
115
94
|
}
|
|
@@ -125,7 +104,7 @@ server.tool(
|
|
|
125
104
|
'Full mesh node status — identity, peers, memory count, coherence.',
|
|
126
105
|
{},
|
|
127
106
|
async () => {
|
|
128
|
-
const status =
|
|
107
|
+
const status = await node.status();
|
|
129
108
|
return { content: [{ type: 'text', text: JSON.stringify(status, null, 2) }] };
|
|
130
109
|
}
|
|
131
110
|
);
|
|
@@ -136,7 +115,7 @@ server.tool(
|
|
|
136
115
|
{ message: z.string() },
|
|
137
116
|
async ({ message }) => {
|
|
138
117
|
node.send(message);
|
|
139
|
-
const peers =
|
|
118
|
+
const peers = await node.peers();
|
|
140
119
|
return { content: [{ type: 'text', text: `Sent to ${peers.length} peer(s): "${message}"` }] };
|
|
141
120
|
}
|
|
142
121
|
);
|
|
@@ -180,7 +159,7 @@ Examples of natural detection:
|
|
|
180
159
|
async ({ mood }) => {
|
|
181
160
|
node.broadcastMood(mood);
|
|
182
161
|
node.remember(`User mood: ${mood}`, { tags: ['mood'] });
|
|
183
|
-
const peers =
|
|
162
|
+
const peers = await node.peers();
|
|
184
163
|
return { content: [{ type: 'text', text: `Mood broadcast to ${peers.length} peer(s)` }] };
|
|
185
164
|
}
|
|
186
165
|
);
|
|
@@ -361,5 +340,5 @@ main().catch((e) => {
|
|
|
361
340
|
});
|
|
362
341
|
|
|
363
342
|
// Graceful shutdown
|
|
364
|
-
process.on('SIGTERM', () => {
|
|
365
|
-
process.on('SIGINT', () => {
|
|
343
|
+
process.on('SIGTERM', () => { node.stop(); });
|
|
344
|
+
process.on('SIGINT', () => { node.stop(); });
|
|
@@ -343,10 +343,13 @@ function handlePlainText(chatId, text) {
|
|
|
343
343
|
const session = getSession(chatId);
|
|
344
344
|
if (!session) return;
|
|
345
345
|
|
|
346
|
-
|
|
347
|
-
|
|
346
|
+
// Send as message (not mood) so MeloTune's command pipeline processes it.
|
|
347
|
+
// Mood signals are for emotional state ("I'm tired"). Direct text like
|
|
348
|
+
// "Play solo cello" is a command that should bypass coupling evaluation.
|
|
349
|
+
session.node.send(text);
|
|
350
|
+
session.node.remember(`Command from Telegram: ${text}`, { tags: ['command', 'telegram'] });
|
|
348
351
|
const peers = session.node.peers();
|
|
349
|
-
tgSendMessage(chatId, `
|
|
352
|
+
tgSendMessage(chatId, `Sent to ${peers.length} peer(s)`);
|
|
350
353
|
}
|
|
351
354
|
|
|
352
355
|
// ── HTTP Server (webhook receiver + health) ───────────────────
|
package/lib/ipc-client.js
CHANGED
|
@@ -209,33 +209,19 @@ class SymDaemonClient extends EventEmitter {
|
|
|
209
209
|
}
|
|
210
210
|
|
|
211
211
|
/**
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
* { node, isDaemon: boolean }
|
|
212
|
+
* Connect to sym-daemon. Throws if daemon is not running.
|
|
213
|
+
* The daemon MUST be running — there is no standalone fallback.
|
|
214
|
+
* Install daemon: node bin/sym-daemon.js --install
|
|
216
215
|
*/
|
|
217
|
-
async function
|
|
216
|
+
async function connectToDaemon(opts = {}) {
|
|
218
217
|
const client = new SymDaemonClient({
|
|
219
218
|
socketPath: opts.socketPath || DEFAULT_SOCKET,
|
|
220
219
|
name: opts.name || 'claude-code',
|
|
221
220
|
cognitiveProfile: opts.cognitiveProfile,
|
|
222
221
|
});
|
|
223
222
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
return { node: client, isDaemon: true };
|
|
227
|
-
} catch {
|
|
228
|
-
// Daemon not running — fall back to standalone SymNode
|
|
229
|
-
const { SymNode } = require('./node');
|
|
230
|
-
const node = new SymNode({
|
|
231
|
-
name: opts.name || 'claude-code',
|
|
232
|
-
cognitiveProfile: opts.cognitiveProfile,
|
|
233
|
-
silent: opts.silent ?? true,
|
|
234
|
-
relay: opts.relay,
|
|
235
|
-
relayToken: opts.relayToken,
|
|
236
|
-
});
|
|
237
|
-
return { node, isDaemon: false };
|
|
238
|
-
}
|
|
223
|
+
await client.connect();
|
|
224
|
+
return client;
|
|
239
225
|
}
|
|
240
226
|
|
|
241
|
-
module.exports = { SymDaemonClient,
|
|
227
|
+
module.exports = { SymDaemonClient, connectToDaemon };
|
package/lib/node.js
CHANGED