nothumanallowed 10.4.0 → 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.4.0",
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.4.0';
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
 
@@ -1600,42 +1600,12 @@ var collabLastMessageCount=0;
1600
1600
  var collabUnreadCount=0;
1601
1601
  var collabGlobalPolling=null;
1602
1602
 
1603
- // Global polling — check for new messages across ALL channels every 3s (even when not on Collab tab)
1603
+ // No client-side polling needed server pushes via WebSocket
1604
1604
  function startCollabGlobalPolling(){
1605
- if(collabGlobalPolling)return;
1606
- // Load channels first
1605
+ // Just load channels list once
1607
1606
  apiGet('/api/collab/channels').then(function(r){
1608
1607
  if(r&&r.channels)collabChannels=r.channels;
1609
1608
  }).catch(function(){});
1610
- collabGlobalPolling=setInterval(function(){
1611
- if(collabChannels.length===0)return;
1612
- // Check each channel for new messages
1613
- for(var i=0;i<collabChannels.length;i++){
1614
- (function(ch){
1615
- apiGet('/api/collab/messages?channelId='+ch.id).then(function(r){
1616
- if(!r||!r.messages)return;
1617
- var prevCount=ch._lastCount||0;
1618
- var newCount=r.messages.length;
1619
- if(newCount>prevCount&&prevCount>0){
1620
- var diff=newCount-prevCount;
1621
- collabUnreadCount+=diff;
1622
- updateCollabBadge();
1623
- // Show toast notification
1624
- var lastMsg=r.messages[r.messages.length-1];
1625
- if(lastMsg&&currentView!=='collab'){
1626
- showToast('collab','AgentMessenger','New message in '+ch.name+': '+(lastMsg.content||'[encrypted]').slice(0,80),5000);
1627
- }
1628
- // Update messages if viewing this channel
1629
- if(collabActiveChannel===ch.id&&currentView==='collab'){
1630
- collabMessages=r.messages;
1631
- renderCollabMessages();
1632
- }
1633
- }
1634
- ch._lastCount=newCount;
1635
- }).catch(function(){});
1636
- })(collabChannels[i]);
1637
- }
1638
- },3000);
1639
1609
  }
1640
1610
 
1641
1611
  function updateCollabBadge(){
@@ -2209,6 +2179,27 @@ function handleDaemonEvent(msg) {
2209
2179
  showToast('plan', 'Daily Plan Ready', 'Your plan for ' + msg.data.date + ' has been generated.', 10000);
2210
2180
  if (currentView === 'plan') renderPlan(document.getElementById('content'));
2211
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;
2212
2203
  }
2213
2204
  }
2214
2205