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 +1 -1
- package/src/commands/collab.mjs +24 -26
- package/src/commands/ui.mjs +105 -0
- package/src/constants.mjs +1 -1
- package/src/services/web-ui.mjs +124 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nothumanallowed",
|
|
3
|
-
"version": "10.
|
|
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": {
|
package/src/commands/collab.mjs
CHANGED
|
@@ -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
|
-
//
|
|
259
|
-
//
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
//
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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,
|
|
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}`);
|
package/src/commands/ui.mjs
CHANGED
|
@@ -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.
|
|
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
|
|
package/src/services/web-ui.mjs
CHANGED
|
@@ -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">🤖</span> Agents</div>
|
|
2257
|
+
<div class="nav-item" data-view="collab" onclick="switchView('collab')"><span class="nav-item__icon">🔒</span> AgentMessenger</div>
|
|
2134
2258
|
</div>
|
|
2135
2259
|
<div class="sidebar__section">
|
|
2136
2260
|
<div class="sidebar__label">Config</div>
|