nothumanallowed 10.2.0 → 10.3.1

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.2.0",
3
+ "version": "10.3.1",
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": {
@@ -295,6 +295,34 @@ export async function cmdUI(args) {
295
295
  fs.writeFileSync(idFile, JSON.stringify(identity, null, 2), { mode: 0o600 });
296
296
  }
297
297
 
298
+ // GET /api/collab/channels — list local channels (from CLI + web UI)
299
+ if (collabAction === 'channels' && method === 'GET') {
300
+ const chFile = path.join(collabDir, 'channels.json');
301
+ let localChannels = [];
302
+ if (fs.existsSync(chFile)) {
303
+ try { localChannels = JSON.parse(fs.readFileSync(chFile, 'utf-8')); } catch {}
304
+ }
305
+ sendJSON(res, 200, { channels: localChannels, identity: { fingerprint: identity.fingerprint, displayName: identity.displayName } });
306
+ logRequest(method, pathname, 200, Date.now() - start);
307
+ return;
308
+ }
309
+
310
+ // POST /api/collab/channels — save channel to local file (sync web UI → CLI)
311
+ if (collabAction === 'channels' && method === 'POST') {
312
+ const body = await parseBody(req);
313
+ const chFile = path.join(collabDir, 'channels.json');
314
+ let localChannels = [];
315
+ if (fs.existsSync(chFile)) { try { localChannels = JSON.parse(fs.readFileSync(chFile, 'utf-8')); } catch {} }
316
+ if (!localChannels.find((c) => c.id === body.id)) {
317
+ localChannels.push({ id: body.id, name: body.name, active: true, role: body.role || 'member', createdAt: new Date().toISOString() });
318
+ localChannels.forEach((c) => { if (c.id !== body.id) c.active = false; });
319
+ fs.writeFileSync(chFile, JSON.stringify(localChannels, null, 2), { mode: 0o600 });
320
+ }
321
+ sendJSON(res, 200, { ok: true });
322
+ logRequest(method, pathname, 200, Date.now() - start);
323
+ return;
324
+ }
325
+
298
326
  if (collabAction === 'create' && method === 'POST') {
299
327
  const body = await parseBody(req);
300
328
  const r = await fetch(ALEX_API + '/channels', {
@@ -326,18 +354,46 @@ export async function cmdUI(args) {
326
354
  const chId = url.searchParams.get('channelId');
327
355
  if (!chId) { sendJSON(res, 400, { error: 'channelId required' }); return; }
328
356
  const r = await fetch(ALEX_API + '/channels/' + chId + '/messages?fp=' + identity.fingerprint);
329
- sendJSON(res, 200, await r.json());
357
+ const data = await r.json();
358
+ // Decrypt messages client-side using channel key
359
+ const channelKey = crypto.createHash('sha256').update('alexandria-channel-key-v1').update(chId).digest();
360
+ if (data.messages) {
361
+ for (const msg of data.messages) {
362
+ if (msg.type === 'system' || !msg.ciphertext || !msg.nonce) continue;
363
+ try {
364
+ const nonce = Buffer.from(msg.nonce, 'base64');
365
+ const raw = Buffer.from(msg.ciphertext, 'base64');
366
+ const tag = raw.subarray(raw.length - 16);
367
+ const encrypted = raw.subarray(0, raw.length - 16);
368
+ const decipher = crypto.createDecipheriv('aes-256-gcm', channelKey, nonce);
369
+ decipher.setAuthTag(tag);
370
+ msg.content = decipher.update(encrypted) + decipher.final('utf-8');
371
+ } catch {
372
+ msg.content = '[cannot decrypt]';
373
+ }
374
+ // Add sender name from members list
375
+ const sender = data.members?.find((m) => m.fingerprint === msg.senderFingerprint);
376
+ msg.senderName = sender?.displayName || msg.senderFingerprint?.slice(0, 8) || 'Unknown';
377
+ }
378
+ }
379
+ sendJSON(res, 200, data);
330
380
  logRequest(method, pathname, 200, Date.now() - start);
331
381
  return;
332
382
  }
333
383
 
334
384
  if (collabAction === 'send' && method === 'POST') {
335
385
  const body = await parseBody(req);
336
- // For simplicity in web UI, send as plaintext (public channel mode)
337
- // In private mode, encryption happens client-side
386
+ // Encrypt with the same channel key used by CLI
387
+ const channelKey = crypto.createHash('sha256').update('alexandria-channel-key-v1').update(body.channelId).digest();
388
+ const nonce = crypto.randomBytes(12);
389
+ const cipher = crypto.createCipheriv('aes-256-gcm', channelKey, nonce);
390
+ const encrypted = Buffer.concat([cipher.update(body.message, 'utf-8'), cipher.final()]);
391
+ const tag = cipher.getAuthTag();
392
+ const ciphertext = Buffer.concat([encrypted, tag]).toString('base64');
393
+
338
394
  const r = await fetch(ALEX_API + '/channels/' + body.channelId + '/messages', {
339
395
  method: 'POST', headers: { 'Content-Type': 'application/json' },
340
- body: JSON.stringify({ senderFingerprint: identity.fingerprint, nonce: 'webui', ciphertext: Buffer.from(body.message).toString('base64'), type: 'text' }),
396
+ body: JSON.stringify({ senderFingerprint: identity.fingerprint, nonce: nonce.toString('base64'), ciphertext, type: 'text' }),
341
397
  });
342
398
  sendJSON(res, 200, await r.json());
343
399
  logRequest(method, pathname, 200, Date.now() - start);
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.2.0';
8
+ export const VERSION = '10.3.1';
9
9
  export const BASE_URL = 'https://nothumanallowed.com/cli';
10
10
  export const API_BASE = 'https://nothumanallowed.com/api/v1';
11
11
 
@@ -1608,22 +1608,12 @@ function renderCollab(el){
1608
1608
  h+='<button onclick="publishConversation()" style="padding:6px 12px;background:var(--greendim);border:1px solid var(--green3);border-radius:6px;color:var(--green);font-family:var(--mono);font-size:11px;cursor:pointer">Publish Current Chat</button>';
1609
1609
  h+='</div>';
1610
1610
 
1611
- // Load channels from localStorage
1612
- var saved=[];try{saved=JSON.parse(localStorage.getItem('nha_collab_channels')||'[]');}catch(e){}
1613
- collabChannels=saved;
1614
-
1615
- if(collabChannels.length>0){
1616
- h+='<div style="margin-bottom:16px">';
1617
- for(var i=0;i<collabChannels.length;i++){
1618
- var ch=collabChannels[i];
1619
- var active=collabActiveChannel===ch.id;
1620
- h+='<div onclick="collabSelect(\\x27'+ch.id+'\\x27)" style="padding:8px 12px;cursor:pointer;border-left:3px solid '+(active?'var(--amber)':'transparent')+';background:'+(active?'var(--bg2)':'transparent')+';margin-bottom:2px;border-radius:0 6px 6px 0">';
1621
- h+='<span style="font-size:12px;color:var(--fg)">'+esc(ch.name)+'</span>';
1622
- h+='<span style="font-size:9px;color:var(--dim);margin-left:8px">'+ch.id.slice(0,8)+'...</span>';
1623
- h+='</div>';
1624
- }
1625
- h+='</div>';
1626
- }
1611
+ // Load channels from server (synced with CLI)
1612
+ apiGet('/api/collab/channels').then(function(r){
1613
+ if(r&&r.channels){collabChannels=r.channels;renderCollabChannelList();}
1614
+ }).catch(function(){});
1615
+
1616
+ h+='<div id="collabChannelList" style="margin-bottom:16px"><div style="color:var(--dim);font-size:11px;padding:8px">Loading channels...</div></div>';
1627
1617
 
1628
1618
  // Messages area
1629
1619
  h+='<div id="collabMessages" style="background:var(--bg2);border:1px solid var(--border);border-radius:8px;min-height:300px;max-height:500px;overflow-y:auto;padding:12px;margin-bottom:12px">';
@@ -1644,15 +1634,32 @@ function renderCollab(el){
1644
1634
  if(collabActiveChannel)collabLoadMessages();
1645
1635
  }
1646
1636
 
1637
+ function renderCollabChannelList(){
1638
+ var el=document.getElementById('collabChannelList');if(!el)return;
1639
+ if(collabChannels.length===0){el.innerHTML='<div style="color:var(--dim);font-size:11px;padding:8px">No channels yet</div>';return;}
1640
+ var h='';
1641
+ for(var i=0;i<collabChannels.length;i++){
1642
+ var ch=collabChannels[i];
1643
+ var active=collabActiveChannel===ch.id;
1644
+ h+='<div onclick="collabSelect(\\x27'+ch.id+'\\x27)" style="padding:8px 12px;cursor:pointer;border-left:3px solid '+(active?'var(--amber)':'transparent')+';background:'+(active?'var(--bg2)':'transparent')+';margin-bottom:2px;border-radius:0 6px 6px 0">';
1645
+ h+='<span style="font-size:12px;color:var(--fg)">'+esc(ch.name)+'</span>';
1646
+ h+='<span style="font-size:9px;color:var(--dim);margin-left:8px">'+ch.id.slice(0,8)+'...</span>';
1647
+ h+='</div>';
1648
+ }
1649
+ el.innerHTML=h;
1650
+ if(collabActiveChannel)collabLoadMessages();
1651
+ }
1647
1652
  function collabCreateChannel(){
1648
1653
  var name=prompt('Channel name:');if(!name)return;
1649
1654
  apiPost('/api/collab/create',{name:name}).then(function(r){
1650
1655
  if(r.error){alert(r.error);return;}
1651
- collabChannels.push({id:r.id,name:name});
1652
- collabActiveChannel=r.id;
1653
- localStorage.setItem('nha_collab_channels',JSON.stringify(collabChannels));
1654
- prompt('Share this invite code:',r.id);
1655
- renderCollab(document.getElementById('content'));
1656
+ // Save to local file via server
1657
+ apiPost('/api/collab/channels',{id:r.id,name:name,role:'creator'}).then(function(){
1658
+ collabChannels.push({id:r.id,name:name,role:'creator'});
1659
+ collabActiveChannel=r.id;
1660
+ prompt('Share this invite code with collaborators:',r.id);
1661
+ renderCollabChannelList();
1662
+ });
1656
1663
  });
1657
1664
  }
1658
1665
 
@@ -1660,16 +1667,20 @@ function collabJoinChannel(){
1660
1667
  var code=prompt('Invite code:');if(!code)return;
1661
1668
  apiPost('/api/collab/join',{channelId:code}).then(function(r){
1662
1669
  if(r.error){alert(r.error);return;}
1663
- collabChannels.push({id:code,name:r.name||code.slice(0,8)});
1664
- collabActiveChannel=code;
1665
- localStorage.setItem('nha_collab_channels',JSON.stringify(collabChannels));
1666
- renderCollab(document.getElementById('content'));
1670
+ apiPost('/api/collab/channels',{id:code,name:r.name||code.slice(0,8),role:'member'}).then(function(){
1671
+ collabChannels.push({id:code,name:r.name||code.slice(0,8),role:'member'});
1672
+ collabActiveChannel=code;
1673
+ renderCollabChannelList();
1674
+ });
1667
1675
  });
1668
1676
  }
1669
1677
 
1670
1678
  function collabSelect(id){
1671
1679
  collabActiveChannel=id;
1672
1680
  collabLoadMessages();
1681
+ // Auto-refresh every 5s while channel is selected
1682
+ if(collabPolling)clearInterval(collabPolling);
1683
+ collabPolling=setInterval(function(){if(currentView==='collab'&&collabActiveChannel===id)collabLoadMessages();},5000);
1673
1684
  }
1674
1685
 
1675
1686
  function collabLoadMessages(){