nothumanallowed 10.0.0 → 10.1.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.0.0",
3
+ "version": "10.1.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": {
@@ -269,6 +269,111 @@ export async function cmdUI(args) {
269
269
  return;
270
270
  }
271
271
 
272
+ // ── Collab (Alexandria proxy) ─────────────────────────────────────
273
+ if (pathname.startsWith('/api/collab/')) {
274
+ const collabAction = pathname.split('/').pop();
275
+ const ALEX_API = 'https://nothumanallowed.com/api/v1/alexandria';
276
+
277
+ // Get or create collab identity
278
+ const collabDir = path.join(NHA_DIR, 'collab');
279
+ const idFile = path.join(collabDir, 'identity.json');
280
+ let identity;
281
+ if (fs.existsSync(idFile)) {
282
+ identity = JSON.parse(fs.readFileSync(idFile, 'utf-8'));
283
+ } else {
284
+ fs.mkdirSync(collabDir, { recursive: true });
285
+ const { publicKey, privateKey } = crypto.generateKeyPairSync('x25519', {
286
+ publicKeyEncoding: { type: 'spki', format: 'der' },
287
+ privateKeyEncoding: { type: 'pkcs8', format: 'der' },
288
+ });
289
+ identity = {
290
+ publicKey: publicKey.toString('base64'),
291
+ privateKey: privateKey.toString('base64'),
292
+ fingerprint: crypto.createHash('sha256').update(publicKey).digest('hex').slice(0, 16),
293
+ displayName: config.profile?.name || 'User',
294
+ };
295
+ fs.writeFileSync(idFile, JSON.stringify(identity, null, 2), { mode: 0o600 });
296
+ }
297
+
298
+ if (collabAction === 'create' && method === 'POST') {
299
+ const body = await parseBody(req);
300
+ const r = await fetch(ALEX_API + '/channels', {
301
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
302
+ body: JSON.stringify({ name: body.name, creatorFingerprint: identity.fingerprint, creatorPublicKey: identity.publicKey, creatorDisplayName: identity.displayName, visibility: body.visibility || 'private' }),
303
+ });
304
+ sendJSON(res, 200, await r.json());
305
+ logRequest(method, pathname, 200, Date.now() - start);
306
+ return;
307
+ }
308
+
309
+ if (collabAction === 'join' && method === 'POST') {
310
+ const body = await parseBody(req);
311
+ const r = await fetch(ALEX_API + '/channels/' + body.channelId + '/join', {
312
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
313
+ body: JSON.stringify({ fingerprint: identity.fingerprint, publicKey: identity.publicKey, displayName: identity.displayName }),
314
+ });
315
+ const data = await r.json();
316
+ // Get channel info for name
317
+ const info = await fetch(ALEX_API + '/channels/' + body.channelId);
318
+ const chInfo = await info.json();
319
+ data.name = chInfo.name || body.channelId;
320
+ sendJSON(res, 200, data);
321
+ logRequest(method, pathname, 200, Date.now() - start);
322
+ return;
323
+ }
324
+
325
+ if (collabAction === 'messages' && method === 'GET') {
326
+ const chId = url.searchParams.get('channelId');
327
+ if (!chId) { sendJSON(res, 400, { error: 'channelId required' }); return; }
328
+ const r = await fetch(ALEX_API + '/channels/' + chId + '/messages?fp=' + identity.fingerprint);
329
+ sendJSON(res, 200, await r.json());
330
+ logRequest(method, pathname, 200, Date.now() - start);
331
+ return;
332
+ }
333
+
334
+ if (collabAction === 'send' && method === 'POST') {
335
+ 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
338
+ const r = await fetch(ALEX_API + '/channels/' + body.channelId + '/messages', {
339
+ 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' }),
341
+ });
342
+ sendJSON(res, 200, await r.json());
343
+ logRequest(method, pathname, 200, Date.now() - start);
344
+ return;
345
+ }
346
+
347
+ if (collabAction === 'publish' && method === 'POST') {
348
+ const body = await parseBody(req);
349
+ // Load conversation and publish as public channel
350
+ const conv = loadConversation(body.conversationId);
351
+ if (!conv || !conv.messages || conv.messages.length === 0) {
352
+ sendJSON(res, 400, { error: 'Conversation not found or empty' });
353
+ logRequest(method, pathname, 400, Date.now() - start);
354
+ return;
355
+ }
356
+ const r = await fetch(ALEX_API + '/channels/publish-conversation', {
357
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
358
+ body: JSON.stringify({
359
+ name: body.title || conv.title || 'Published Conversation',
360
+ description: body.description || '',
361
+ creatorFingerprint: identity.fingerprint,
362
+ creatorPublicKey: identity.publicKey,
363
+ creatorDisplayName: identity.displayName,
364
+ messages: conv.messages,
365
+ }),
366
+ });
367
+ sendJSON(res, 200, await r.json());
368
+ logRequest(method, pathname, 200, Date.now() - start);
369
+ return;
370
+ }
371
+
372
+ sendJSON(res, 404, { error: 'Unknown collab action' });
373
+ logRequest(method, pathname, 404, Date.now() - start);
374
+ return;
375
+ }
376
+
272
377
  // GET /api/health — simple health check
273
378
  if (method === 'GET' && pathname === '/api/health') {
274
379
  sendJSON(res, 200, { ok: true, version: VERSION });
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.0.0';
8
+ export const VERSION = '10.1.0';
9
9
  export const BASE_URL = 'https://nothumanallowed.com/cli';
10
10
  export const API_BASE = 'https://nothumanallowed.com/api/v1';
11
11
 
@@ -376,6 +376,7 @@ function render(){
376
376
  case 'slack':renderSlack(el);break;
377
377
  case 'birthdays':renderBirthdays(el);break;
378
378
  case 'agents':renderAgents(el);break;
379
+ case 'collab':renderCollab(el);break;
379
380
  case 'settings':renderSettings(el);break;
380
381
  }
381
382
  }
@@ -1590,6 +1591,128 @@ function completeMsTodo(taskId,listId){
1590
1591
  apiPost('/api/mstodo/'+taskId+'/complete',{listId:listId}).then(function(){mstodoData=null;renderMsTodo(document.getElementById('content'))});
1591
1592
  }
1592
1593
 
1594
+ // ---- COLLAB (Alexandria) ----
1595
+ var collabChannels=[];
1596
+ var collabMessages=[];
1597
+ var collabActiveChannel=null;
1598
+ var collabPolling=null;
1599
+
1600
+ function renderCollab(el){
1601
+ var h='<div style="max-width:800px;margin:0 auto;padding:20px">';
1602
+ h+='<h2 style="font-family:var(--term);color:var(--amber);font-size:18px;margin-bottom:16px">Alexandria — Encrypted Communication</h2>';
1603
+
1604
+ // Channel list
1605
+ h+='<div style="display:flex;gap:8px;margin-bottom:16px;flex-wrap:wrap">';
1606
+ h+='<button onclick="collabCreateChannel()" style="padding:6px 12px;background:var(--amberdim);border:1px solid var(--amber3);border-radius:6px;color:var(--amber);font-family:var(--mono);font-size:11px;cursor:pointer">+ Create Channel</button>';
1607
+ h+='<button onclick="collabJoinChannel()" style="padding:6px 12px;background:var(--bg3);border:1px solid var(--border2);border-radius:6px;color:var(--fg);font-family:var(--mono);font-size:11px;cursor:pointer">Join Channel</button>';
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
+ h+='</div>';
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
+ }
1627
+
1628
+ // Messages area
1629
+ 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">';
1630
+ if(!collabActiveChannel){
1631
+ h+='<div style="text-align:center;color:var(--dim);padding:40px;font-size:12px">Select or create a channel to start</div>';
1632
+ }
1633
+ h+='</div>';
1634
+
1635
+ // Send bar
1636
+ h+='<div style="display:flex;gap:8px">';
1637
+ h+='<input id="collabInput" placeholder="Type encrypted message..." style="flex:1;padding:8px 12px;background:var(--bg);border:1px solid var(--border2);border-radius:6px;color:var(--fg);font-family:var(--mono);font-size:12px" onkeydown="if(event.key===\\x27Enter\\x27)collabSend()">';
1638
+ h+='<button onclick="collabSend()" style="padding:8px 16px;background:var(--amberdim);border:1px solid var(--amber3);border-radius:6px;color:var(--amber);font-family:var(--mono);font-size:11px;cursor:pointer">Send</button>';
1639
+ h+='</div>';
1640
+
1641
+ h+='</div>';
1642
+ el.innerHTML=h;
1643
+
1644
+ if(collabActiveChannel)collabLoadMessages();
1645
+ }
1646
+
1647
+ function collabCreateChannel(){
1648
+ var name=prompt('Channel name:');if(!name)return;
1649
+ apiPost('/api/collab/create',{name:name}).then(function(r){
1650
+ 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
+ });
1657
+ }
1658
+
1659
+ function collabJoinChannel(){
1660
+ var code=prompt('Invite code:');if(!code)return;
1661
+ apiPost('/api/collab/join',{channelId:code}).then(function(r){
1662
+ 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'));
1667
+ });
1668
+ }
1669
+
1670
+ function collabSelect(id){
1671
+ collabActiveChannel=id;
1672
+ collabLoadMessages();
1673
+ }
1674
+
1675
+ function collabLoadMessages(){
1676
+ if(!collabActiveChannel)return;
1677
+ apiGet('/api/collab/messages?channelId='+collabActiveChannel).then(function(r){
1678
+ if(!r||!r.messages)return;
1679
+ collabMessages=r.messages;
1680
+ var el=document.getElementById('collabMessages');if(!el)return;
1681
+ if(collabMessages.length===0){el.innerHTML='<div style="text-align:center;color:var(--dim);padding:20px;font-size:11px">No messages yet</div>';return;}
1682
+ var h='';
1683
+ for(var i=0;i<collabMessages.length;i++){
1684
+ var m=collabMessages[i];
1685
+ var time=new Date(m.timestamp).toLocaleTimeString();
1686
+ var sender=m.senderName||m.senderFingerprint?.slice(0,8)||'Unknown';
1687
+ var content=m.content||m.plaintext||'[encrypted]';
1688
+ if(m.type==='system'){h+='<div style="text-align:center;color:var(--dim);font-size:10px;margin:4px 0">'+esc(sender)+' joined</div>';continue;}
1689
+ 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>';
1690
+ }
1691
+ el.innerHTML=h;
1692
+ el.scrollTop=el.scrollHeight;
1693
+ });
1694
+ }
1695
+
1696
+ function collabSend(){
1697
+ var inp=document.getElementById('collabInput');if(!inp)return;
1698
+ var msg=inp.value.trim();if(!msg||!collabActiveChannel)return;
1699
+ inp.value='';
1700
+ apiPost('/api/collab/send',{channelId:collabActiveChannel,message:msg}).then(function(r){
1701
+ if(r.error){alert(r.error);return;}
1702
+ collabLoadMessages();
1703
+ });
1704
+ }
1705
+
1706
+ function publishConversation(){
1707
+ if(!activeConvId||chatHistory.length===0){alert('No conversation to publish. Open a chat first.');return;}
1708
+ var title=prompt('Title for the published conversation:');if(!title)return;
1709
+ var desc=prompt('Short description (optional):','');
1710
+ apiPost('/api/collab/publish',{conversationId:activeConvId,title:title,description:desc||''}).then(function(r){
1711
+ if(r.error){alert('Error: '+r.error);return;}
1712
+ alert('Published on Alexandria!\\nURL: https://nothumanallowed.com/alexandria/'+r.id+'\\nMessages: '+r.messageCount);
1713
+ });
1714
+ }
1715
+
1593
1716
  function renderAgents(el){
1594
1717
  if(agentsList.length===0){el.innerHTML='<div style="text-align:center;padding:40px"><div class="spinner"></div><div style="color:var(--dim)">Loading agents...</div></div>';loadAgents().then(function(){renderAgents(el)});return}
1595
1718
 
@@ -2131,6 +2254,7 @@ init();
2131
2254
  <div class="sidebar__section">
2132
2255
  <div class="sidebar__label">AI</div>
2133
2256
  <div class="nav-item" data-view="agents" onclick="switchView('agents')"><span class="nav-item__icon">&#129302;</span> Agents</div>
2257
+ <div class="nav-item" data-view="collab" onclick="switchView('collab')"><span class="nav-item__icon">&#128274;</span> Collab</div>
2134
2258
  </div>
2135
2259
  <div class="sidebar__section">
2136
2260
  <div class="sidebar__label">Config</div>