@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.
@@ -1,30 +1,83 @@
1
1
  #!/bin/bash
2
2
  # SYM — Setup for Claude Code
3
- # Adds MCP server + auto-approves sym_mood + installs CLAUDE.md instructions
4
- # Optionally configures WebSocket relay for internet-scale mesh
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 "SYM Setup for Claude Code"
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
- # 1. Add MCP server
15
- echo "Adding SYM MCP server..."
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
- # 2. Auto-approve sym_mood (no permission prompt)
19
- echo "Auto-approving sym_mood tool..."
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
- fi
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
- read -p "Relay URL (leave empty to skip): " RELAY_URL
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
- # 4. Append CLAUDE.md instructions if not already present
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
- const NODE_NAME = process.env.SYM_NODE_NAME || os.hostname().split('.')[0].toLowerCase();
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 { connectOrFallback } = require('../../lib/ipc-client');
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; // SymDaemonClient or SymNode same API
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
- const result = await connectOrFallback({
43
- name: 'claude-code',
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 = isDaemon ? await node.peers() : node.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 = isDaemon ? await node.recall(query) : node.recall(query);
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 = isDaemon ? await node.peers() : node.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 = isDaemon ? await node.status() : node.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 = isDaemon ? await node.peers() : node.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 = isDaemon ? await node.peers() : node.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', () => { if (bridge) bridge.stop(); node.stop(); });
365
- process.on('SIGINT', () => { if (bridge) bridge.stop(); node.stop(); });
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
- session.node.broadcastMood(text);
347
- session.node.remember(`User mood: ${text}`, { tags: ['mood', 'telegram'] });
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, `Mood broadcast to ${peers.length} peer(s)`);
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
- * Try to connect to sym-daemon. If not running, fall back to a standalone SymNode.
213
- *
214
- * Returns an object with the same API surface either way:
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 connectOrFallback(opts = {}) {
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
- try {
225
- await client.connect();
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, connectOrFallback };
227
+ module.exports = { SymDaemonClient, connectToDaemon };
package/lib/node.js CHANGED
@@ -323,6 +323,7 @@ class SymNode extends EventEmitter {
323
323
  if (peer) peer.transport.send(frame);
324
324
  } else {
325
325
  this._broadcastToPeers(frame);
326
+ this._wakeSleepingPeers('message', frame);
326
327
  }
327
328
  }
328
329
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sym-bot/sym",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Local AI mesh — every agent is a sovereign node, the mesh is the agents",
5
5
  "main": "lib/node.js",
6
6
  "bin": {