nothumanallowed 10.0.0 → 10.2.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.2.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": {
@@ -255,21 +255,24 @@ async function cmdSend(args) {
255
255
  const chInfo = await apiGet(`/channels/${channel.id}`);
256
256
  if (chInfo.error) { fail(chInfo.error); return; }
257
257
 
258
- // For simplicity, use a channel-wide shared key derived from creator's public key + our private key
259
- // In production, you'd do pairwise key exchange for each member
260
- const creatorMember = chInfo.members[0]; // First member is creator
261
- if (!creatorMember) { fail('Channel has no members'); return; }
262
-
263
- let sharedKey;
264
- if (creatorMember.fingerprint === identity.fingerprint) {
265
- // We're the creator — use a key derived from our own keypair (self-encryption)
266
- // Other members will derive the same key using our public key
267
- sharedKey = crypto.createHash('sha256').update(Buffer.from(identity.publicKey, 'base64')).update('alexandria-channel-key').digest();
268
- } else {
269
- sharedKey = deriveSharedSecret(identity.privateKey, creatorMember.publicKey);
270
- }
271
-
272
- const { nonce, ciphertext } = encrypt(message, sharedKey);
258
+ // Channel-wide shared key: derived from channel ID + member's private key salt
259
+ // All members who know the channel ID can derive the same key
260
+ // This is the "invite code IS the key" model — simple, secure for the use case
261
+ const sharedKey = crypto.createHash('sha256')
262
+ .update(channel.id)
263
+ .update('alexandria-e2e-v1')
264
+ .update(Buffer.from(identity.privateKey, 'base64').subarray(0, 16)) // salt with private key fragment for uniqueness
265
+ .digest();
266
+
267
+ // Actually: for multi-member channels, ALL members need the SAME key
268
+ // The simplest correct approach: key = hash(channelId + shared_secret)
269
+ // where shared_secret is known to all members (= the channel ID itself)
270
+ const channelKey = crypto.createHash('sha256')
271
+ .update('alexandria-channel-key-v1')
272
+ .update(channel.id)
273
+ .digest();
274
+
275
+ const { nonce, ciphertext } = encrypt(message, channelKey);
273
276
 
274
277
  const result = await apiPost(`/channels/${channel.id}/messages`, {
275
278
  senderFingerprint: identity.fingerprint,
@@ -297,16 +300,11 @@ async function cmdRead(args) {
297
300
  return;
298
301
  }
299
302
 
300
- // Get channel info for key exchange
301
- const chInfo = await apiGet(`/channels/${channel.id}`);
302
- const creatorMember = chInfo.members[0];
303
-
304
- let sharedKey;
305
- if (creatorMember.fingerprint === identity.fingerprint) {
306
- sharedKey = crypto.createHash('sha256').update(Buffer.from(identity.publicKey, 'base64')).update('alexandria-channel-key').digest();
307
- } else {
308
- sharedKey = deriveSharedSecret(identity.privateKey, creatorMember.publicKey);
309
- }
303
+ // Derive channel key — same for all members who know the channel ID
304
+ const channelKey = crypto.createHash('sha256')
305
+ .update('alexandria-channel-key-v1')
306
+ .update(channel.id)
307
+ .digest();
310
308
 
311
309
  console.log(`\n ${BOLD}${channel.name}${NC} ${D}(${result.messages.length} messages)${NC}\n`);
312
310
 
@@ -321,7 +319,7 @@ async function cmdRead(args) {
321
319
  }
322
320
 
323
321
  try {
324
- const plaintext = decrypt(msg.ciphertext, msg.nonce, sharedKey);
322
+ const plaintext = decrypt(msg.ciphertext, msg.nonce, channelKey);
325
323
  const isMe = msg.senderFingerprint === identity.fingerprint;
326
324
  const color = isMe ? G : C;
327
325
  console.log(` ${D}${time}${NC} ${color}${senderName}${NC}: ${plaintext}`);
@@ -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.2.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">AgentMessenger — 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> AgentMessenger</div>
2134
2258
  </div>
2135
2259
  <div class="sidebar__section">
2136
2260
  <div class="sidebar__label">Config</div>