nothumanallowed 10.3.3 → 10.5.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nothumanallowed",
3
- "version": "10.3.3",
3
+ "version": "10.5.0",
4
4
  "description": "NotHumanAllowed — 38 AI agents, 53 tools. Email, calendar, browser automation, screen capture, canvas, cron/heartbeat, GitHub, Notion, Slack, voice chat, 28 languages. Zero-dependency CLI.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -395,7 +395,20 @@ export async function cmdUI(args) {
395
395
  method: 'POST', headers: { 'Content-Type': 'application/json' },
396
396
  body: JSON.stringify({ senderFingerprint: identity.fingerprint, nonce: nonce.toString('base64'), ciphertext, type: 'text' }),
397
397
  });
398
- sendJSON(res, 200, await r.json());
398
+ const result = await r.json();
399
+ // Broadcast own message via WS immediately (so other tabs/views see it)
400
+ wsBroadcast({
401
+ type: 'collab_message',
402
+ channelId: body.channelId,
403
+ message: {
404
+ senderName: identity.displayName || 'You',
405
+ senderFingerprint: identity.fingerprint,
406
+ content: body.message,
407
+ timestamp: result.timestamp || new Date().toISOString(),
408
+ type: 'text',
409
+ },
410
+ });
411
+ sendJSON(res, 200, result);
399
412
  logRequest(method, pathname, 200, Date.now() - start);
400
413
  return;
401
414
  }
@@ -2050,16 +2063,26 @@ export async function cmdUI(args) {
2050
2063
 
2051
2064
  const server = http.createServer(handleRequest);
2052
2065
 
2053
- // ── WebSocket server (ws package) relay daemon events to browser ──
2066
+ // ── WebSocket server daemon relay + Alexandria real-time ──────────
2067
+ let wsBroadcast = (msg) => {}; // no-op until WS is ready
2054
2068
  try {
2055
- const { WebSocketServer } = await import('ws');
2069
+ const { WebSocketServer, WebSocket: WsClient } = await import('ws');
2056
2070
  const wss = new WebSocketServer({ server });
2071
+ const wsClients = new Set();
2072
+
2073
+ wsBroadcast = (msg) => {
2074
+ const data = JSON.stringify(msg);
2075
+ for (const ws of wsClients) {
2076
+ if (ws.readyState === 1) try { ws.send(data); } catch {}
2077
+ }
2078
+ };
2057
2079
 
2058
2080
  wss.on('connection', (browserWs) => {
2081
+ wsClients.add(browserWs);
2082
+
2059
2083
  // Connect to daemon WS on port 3848 and relay messages
2060
2084
  let daemonWs = null;
2061
2085
  try {
2062
- const { WebSocket: WsClient } = require('ws');
2063
2086
  daemonWs = new WsClient('ws://127.0.0.1:3848');
2064
2087
  daemonWs.on('message', (data) => {
2065
2088
  if (browserWs.readyState === 1) browserWs.send(data.toString());
@@ -2068,11 +2091,76 @@ export async function cmdUI(args) {
2068
2091
  daemonWs.on('close', () => { daemonWs = null; });
2069
2092
  } catch {}
2070
2093
 
2071
- browserWs.on('close', () => { if (daemonWs) try { daemonWs.close(); } catch {} });
2072
- browserWs.on('error', () => { if (daemonWs) try { daemonWs.close(); } catch {} });
2094
+ browserWs.on('close', () => { wsClients.delete(browserWs); if (daemonWs) try { daemonWs.close(); } catch {} });
2095
+ browserWs.on('error', () => { wsClients.delete(browserWs); if (daemonWs) try { daemonWs.close(); } catch {} });
2073
2096
  });
2097
+
2098
+ // ── Alexandria real-time polling → WS push ──────────────────────
2099
+ // Server-side polling: check Alexandria channels for new messages every 2s
2100
+ // Push new messages to browser via WebSocket — browser does ZERO polling
2101
+ const collabDir = path.join(NHA_DIR, 'collab');
2102
+ const ALEX_API = 'https://nothumanallowed.com/api/v1/alexandria';
2103
+ const channelMessageCounts = new Map(); // channelId → last known count
2104
+
2105
+ setInterval(async () => {
2106
+ if (wsClients.size === 0) return; // no browsers connected
2107
+ try {
2108
+ const chFile = path.join(collabDir, 'channels.json');
2109
+ if (!fs.existsSync(chFile)) return;
2110
+ const channels = JSON.parse(fs.readFileSync(chFile, 'utf-8'));
2111
+ const idFile = path.join(collabDir, 'identity.json');
2112
+ if (!fs.existsSync(idFile)) return;
2113
+ const identity = JSON.parse(fs.readFileSync(idFile, 'utf-8'));
2114
+
2115
+ for (const ch of channels) {
2116
+ const r = await fetch(ALEX_API + '/channels/' + ch.id + '/messages?fp=' + identity.fingerprint);
2117
+ if (!r.ok) continue;
2118
+ const data = await r.json();
2119
+ if (!data.messages) continue;
2120
+
2121
+ const prevCount = channelMessageCounts.get(ch.id) || 0;
2122
+ const newCount = data.messages.length;
2123
+
2124
+ if (newCount > prevCount && prevCount > 0) {
2125
+ // New messages! Decrypt and push via WS
2126
+ const channelKey = crypto.createHash('sha256').update('alexandria-channel-key-v1').update(ch.id).digest();
2127
+ const newMsgs = data.messages.slice(prevCount);
2128
+
2129
+ for (const msg of newMsgs) {
2130
+ if (msg.type === 'system' || !msg.ciphertext || !msg.nonce) continue;
2131
+ let content = '[encrypted]';
2132
+ try {
2133
+ const nonce = Buffer.from(msg.nonce, 'base64');
2134
+ const raw = Buffer.from(msg.ciphertext, 'base64');
2135
+ const tag = raw.subarray(raw.length - 16);
2136
+ const encrypted = raw.subarray(0, raw.length - 16);
2137
+ const decipher = crypto.createDecipheriv('aes-256-gcm', channelKey, nonce);
2138
+ decipher.setAuthTag(tag);
2139
+ content = decipher.update(encrypted) + decipher.final('utf-8');
2140
+ } catch {}
2141
+
2142
+ const sender = data.members?.find(m => m.fingerprint === msg.senderFingerprint);
2143
+ wsBroadcast({
2144
+ type: 'collab_message',
2145
+ channelId: ch.id,
2146
+ channelName: ch.name,
2147
+ message: {
2148
+ senderName: sender?.displayName || msg.senderFingerprint?.slice(0, 8) || 'Unknown',
2149
+ senderFingerprint: msg.senderFingerprint,
2150
+ content,
2151
+ timestamp: msg.timestamp,
2152
+ type: msg.type,
2153
+ },
2154
+ });
2155
+ }
2156
+ }
2157
+ channelMessageCounts.set(ch.id, newCount);
2158
+ }
2159
+ } catch {}
2160
+ }, 10000); // 10s interval — gentle on server, WS handles own messages instantly
2161
+
2074
2162
  } catch {
2075
- // ws package not available — live updates disabled, everything else works
2163
+ // ws package not available — live updates disabled
2076
2164
  }
2077
2165
 
2078
2166
  server.on('error', (err) => {
package/src/constants.mjs CHANGED
@@ -5,7 +5,7 @@ import { fileURLToPath } from 'url';
5
5
  const __filename = fileURLToPath(import.meta.url);
6
6
  const __dirname = path.dirname(__filename);
7
7
 
8
- export const VERSION = '10.3.3';
8
+ export const VERSION = '10.5.0';
9
9
  export const BASE_URL = 'https://nothumanallowed.com/cli';
10
10
  export const API_BASE = 'https://nothumanallowed.com/api/v1';
11
11
 
@@ -1596,6 +1596,48 @@ var collabChannels=[];
1596
1596
  var collabMessages=[];
1597
1597
  var collabActiveChannel=null;
1598
1598
  var collabPolling=null;
1599
+ var collabLastMessageCount=0;
1600
+ var collabUnreadCount=0;
1601
+ var collabGlobalPolling=null;
1602
+
1603
+ // No client-side polling needed — server pushes via WebSocket
1604
+ function startCollabGlobalPolling(){
1605
+ // Just load channels list once
1606
+ apiGet('/api/collab/channels').then(function(r){
1607
+ if(r&&r.channels)collabChannels=r.channels;
1608
+ }).catch(function(){});
1609
+ }
1610
+
1611
+ function updateCollabBadge(){
1612
+ var badge=document.getElementById('collabBadge');
1613
+ if(badge){
1614
+ if(collabUnreadCount>0){
1615
+ badge.textContent=collabUnreadCount>99?'99+':collabUnreadCount;
1616
+ badge.style.display='inline-block';
1617
+ } else {
1618
+ badge.style.display='none';
1619
+ }
1620
+ }
1621
+ }
1622
+
1623
+ function renderCollabMessages(){
1624
+ var el=document.getElementById('collabMessages');if(!el)return;
1625
+ if(collabMessages.length===0){el.innerHTML='<div style="text-align:center;color:var(--dim);padding:20px;font-size:11px">No messages yet</div>';return;}
1626
+ var h='';
1627
+ for(var i=0;i<collabMessages.length;i++){
1628
+ var m=collabMessages[i];
1629
+ var time=new Date(m.timestamp).toLocaleTimeString();
1630
+ var sender=m.senderName||m.senderFingerprint?.slice(0,8)||'Unknown';
1631
+ var content=m.content||m.plaintext||'[encrypted]';
1632
+ if(m.type==='system'){h+='<div style="text-align:center;color:var(--dim);font-size:10px;margin:4px 0">'+esc(sender)+' joined</div>';continue;}
1633
+ h+='<div style="margin-bottom:8px"><span style="font-size:10px;color:var(--dim)">'+time+'</span> <span style="font-size:11px;color:var(--amber);font-weight:600">'+esc(sender)+'</span><div style="font-size:12px;color:var(--fg);margin-top:2px;white-space:pre-wrap">'+esc(content)+'</div></div>';
1634
+ }
1635
+ el.innerHTML=h;
1636
+ el.scrollTop=el.scrollHeight;
1637
+ }
1638
+
1639
+ // Start global polling on page load
1640
+ setTimeout(startCollabGlobalPolling,2000);
1599
1641
 
1600
1642
  function renderCollab(el){
1601
1643
  var h='<div style="max-width:800px;margin:0 auto;padding:20px">';
@@ -1691,29 +1733,27 @@ function collabDeleteChannel(id){
1691
1733
  function collabSelect(id){
1692
1734
  collabActiveChannel=id;
1693
1735
  collabLoadMessages();
1694
- // Auto-refresh every 5s while channel is selected
1736
+ // Auto-refresh every 2s while channel is selected
1695
1737
  if(collabPolling)clearInterval(collabPolling);
1696
- collabPolling=setInterval(function(){if(currentView==='collab'&&collabActiveChannel===id)collabLoadMessages();},5000);
1738
+ collabPolling=setInterval(function(){
1739
+ if(currentView==='collab'&&collabActiveChannel===id){
1740
+ collabLoadMessages();
1741
+ }
1742
+ },2000);
1697
1743
  }
1698
1744
 
1699
1745
  function collabLoadMessages(){
1700
1746
  if(!collabActiveChannel)return;
1747
+ // Reset unread when viewing
1748
+ collabUnreadCount=0;
1749
+ updateCollabBadge();
1701
1750
  apiGet('/api/collab/messages?channelId='+collabActiveChannel).then(function(r){
1702
1751
  if(!r||!r.messages)return;
1703
1752
  collabMessages=r.messages;
1704
- var el=document.getElementById('collabMessages');if(!el)return;
1705
- if(collabMessages.length===0){el.innerHTML='<div style="text-align:center;color:var(--dim);padding:20px;font-size:11px">No messages yet</div>';return;}
1706
- var h='';
1707
- for(var i=0;i<collabMessages.length;i++){
1708
- var m=collabMessages[i];
1709
- var time=new Date(m.timestamp).toLocaleTimeString();
1710
- var sender=m.senderName||m.senderFingerprint?.slice(0,8)||'Unknown';
1711
- var content=m.content||m.plaintext||'[encrypted]';
1712
- if(m.type==='system'){h+='<div style="text-align:center;color:var(--dim);font-size:10px;margin:4px 0">'+esc(sender)+' joined</div>';continue;}
1713
- h+='<div style="margin-bottom:8px"><span style="font-size:10px;color:var(--dim)">'+time+'</span> <span style="font-size:11px;color:var(--amber);font-weight:600">'+esc(sender)+'</span><div style="font-size:12px;color:var(--fg);margin-top:2px;white-space:pre-wrap">'+esc(content)+'</div></div>';
1714
- }
1715
- el.innerHTML=h;
1716
- el.scrollTop=el.scrollHeight;
1753
+ // Update last count for this channel
1754
+ var ch=collabChannels.find(function(c){return c.id===collabActiveChannel});
1755
+ if(ch)ch._lastCount=r.messages.length;
1756
+ renderCollabMessages();
1717
1757
  });
1718
1758
  }
1719
1759
 
@@ -1721,9 +1761,13 @@ function collabSend(){
1721
1761
  var inp=document.getElementById('collabInput');if(!inp)return;
1722
1762
  var msg=inp.value.trim();if(!msg||!collabActiveChannel)return;
1723
1763
  inp.value='';
1764
+ // Optimistic: show message immediately
1765
+ collabMessages.push({senderName:'You',timestamp:new Date().toISOString(),content:msg,type:'text'});
1766
+ renderCollabMessages();
1724
1767
  apiPost('/api/collab/send',{channelId:collabActiveChannel,message:msg}).then(function(r){
1725
1768
  if(r.error){alert(r.error);return;}
1726
- collabLoadMessages();
1769
+ // Reload to get server timestamp
1770
+ setTimeout(collabLoadMessages,500);
1727
1771
  });
1728
1772
  }
1729
1773
 
@@ -2135,6 +2179,27 @@ function handleDaemonEvent(msg) {
2135
2179
  showToast('plan', 'Daily Plan Ready', 'Your plan for ' + msg.data.date + ' has been generated.', 10000);
2136
2180
  if (currentView === 'plan') renderPlan(document.getElementById('content'));
2137
2181
  break;
2182
+
2183
+ case 'collab_message':
2184
+ // Real-time Alexandria message via WebSocket
2185
+ var cm = msg.message;
2186
+ if (cm) {
2187
+ // Add to messages if viewing this channel
2188
+ if (currentView === 'collab' && collabActiveChannel === msg.channelId) {
2189
+ // Avoid duplicates
2190
+ var isDup = collabMessages.some(function(m) { return m.timestamp === cm.timestamp && m.content === cm.content; });
2191
+ if (!isDup) {
2192
+ collabMessages.push({senderName: cm.senderName, timestamp: cm.timestamp, content: cm.content, type: cm.type});
2193
+ renderCollabMessages();
2194
+ }
2195
+ } else {
2196
+ // Not viewing this channel — show badge + toast
2197
+ collabUnreadCount++;
2198
+ updateCollabBadge();
2199
+ showToast('collab', 'AgentMessenger', cm.senderName + ': ' + (cm.content || '').slice(0, 100), 5000);
2200
+ }
2201
+ }
2202
+ break;
2138
2203
  }
2139
2204
  }
2140
2205
 
@@ -2278,7 +2343,7 @@ init();
2278
2343
  <div class="sidebar__section">
2279
2344
  <div class="sidebar__label">AI</div>
2280
2345
  <div class="nav-item" data-view="agents" onclick="switchView('agents')"><span class="nav-item__icon">&#129302;</span> Agents</div>
2281
- <div class="nav-item" data-view="collab" onclick="switchView('collab')"><span class="nav-item__icon">&#128274;</span> AgentMessenger</div>
2346
+ <div class="nav-item" data-view="collab" onclick="switchView('collab')"><span class="nav-item__icon">&#128274;</span> AgentMessenger <span id="collabBadge" style="display:none;background:var(--red);color:#fff;font-size:9px;padding:1px 5px;border-radius:8px;margin-left:4px;font-family:var(--mono)">0</span></div>
2282
2347
  </div>
2283
2348
  <div class="sidebar__section">
2284
2349
  <div class="sidebar__label">Config</div>