agentchannel 0.5.1 → 0.5.2
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/dist/web.js +106 -80
- package/dist/web.js.map +1 -1
- package/package.json +1 -1
package/dist/web.js
CHANGED
|
@@ -3,15 +3,24 @@ import mqtt from "mqtt";
|
|
|
3
3
|
import { deriveKey, hashRoom, decrypt } from "./crypto.js";
|
|
4
4
|
const MAX_HISTORY = 200;
|
|
5
5
|
const CSS = `*,*::before,*::after{margin:0;padding:0;box-sizing:border-box}
|
|
6
|
-
:root{--bg-primary:#
|
|
6
|
+
:root{--bg-primary:#ffffff;--bg-secondary:#f8f9fa;--bg-sidebar:#1a1a2e;--bg-hover:rgba(0,0,0,0.04);--text-primary:#1a1a2e;--text-secondary:#6b7280;--text-muted:#9ca3af;--text-sidebar:#a0aec0;--text-sidebar-active:#ffffff;--mention-bg:rgba(59,130,246,0.1);--mention-text:#2563eb;--mention-border:rgba(59,130,246,0.2);--system-text:#9ca3af;--channel-bg:rgba(16,185,129,0.1);--channel-text:#059669;--channel-border:rgba(16,185,129,0.2);--divider:rgba(0,0,0,0.08);--sidebar-hover:rgba(255,255,255,0.08);--sidebar-active:rgba(255,255,255,0.15);--font-sans:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;--radius-sm:4px;--radius-lg:12px}
|
|
7
|
+
@media(prefers-color-scheme:dark){:root{--bg-primary:#1e1e2e;--bg-secondary:#181825;--bg-sidebar:#11111b;--bg-hover:rgba(255,255,255,0.05);--text-primary:#cdd6f4;--text-secondary:#a6adc8;--text-muted:#585b70;--mention-bg:rgba(137,180,250,0.15);--mention-text:#89b4fa;--mention-border:rgba(137,180,250,0.25);--system-text:#585b70;--channel-bg:rgba(166,227,161,0.1);--channel-text:#a6e3a1;--channel-border:rgba(166,227,161,0.2);--divider:rgba(255,255,255,0.06);--sidebar-hover:rgba(255,255,255,0.06);--sidebar-active:rgba(255,255,255,0.12)}}
|
|
7
8
|
html{font-size:15px;-webkit-font-smoothing:antialiased}
|
|
8
|
-
body{font-family:var(--font-sans);background:var(--bg-primary);color:var(--text-primary);line-height:1.6;
|
|
9
|
-
.
|
|
10
|
-
.
|
|
11
|
-
.
|
|
12
|
-
.
|
|
13
|
-
.
|
|
14
|
-
.
|
|
9
|
+
body{font-family:var(--font-sans);background:var(--bg-primary);color:var(--text-primary);line-height:1.6;height:100vh;overflow:hidden}
|
|
10
|
+
.app{display:flex;height:100vh}
|
|
11
|
+
.sidebar{width:220px;background:var(--bg-sidebar);display:flex;flex-direction:column;flex-shrink:0}
|
|
12
|
+
.sidebar__header{padding:16px;font-size:1rem;font-weight:700;color:#e94560;border-bottom:1px solid rgba(255,255,255,0.06)}
|
|
13
|
+
.sidebar__channels{flex:1;padding:8px;overflow-y:auto}
|
|
14
|
+
.sidebar__channel{display:flex;align-items:center;gap:8px;padding:6px 12px;border-radius:6px;cursor:pointer;color:var(--text-sidebar);font-size:0.9rem;transition:background 0.1s}
|
|
15
|
+
.sidebar__channel:hover{background:var(--sidebar-hover)}
|
|
16
|
+
.sidebar__channel.active{background:var(--sidebar-active);color:var(--text-sidebar-active);font-weight:600}
|
|
17
|
+
.sidebar__channel .hash{color:#6b7280;font-weight:400}
|
|
18
|
+
.sidebar__channel .unread{background:#e94560;color:#fff;font-size:0.65rem;padding:1px 6px;border-radius:9999px;margin-left:auto}
|
|
19
|
+
.sidebar__status{padding:12px 16px;font-size:0.75rem;color:var(--text-sidebar);border-top:1px solid rgba(255,255,255,0.06)}
|
|
20
|
+
.sidebar__status.connected{color:#4ade80}
|
|
21
|
+
.main{flex:1;display:flex;flex-direction:column;min-width:0}
|
|
22
|
+
.main__header{padding:14px 20px;border-bottom:1px solid var(--divider);font-weight:600;font-size:0.95rem;background:var(--bg-secondary)}
|
|
23
|
+
.message-list{flex:1;overflow-y:auto;padding:8px 0}
|
|
15
24
|
.message{display:flex;padding:2px 20px;transition:background-color 0.1s ease}
|
|
16
25
|
.message:hover{background:var(--bg-hover)}
|
|
17
26
|
.message__gutter{width:40px;flex-shrink:0;margin-right:12px;display:flex;align-items:flex-start;justify-content:center;padding-top:4px}
|
|
@@ -33,8 +42,9 @@ body{font-family:var(--font-sans);background:var(--bg-primary);color:var(--text-
|
|
|
33
42
|
.message--system .message__body{display:flex;align-items:center;gap:10px;padding-left:52px}
|
|
34
43
|
.message__system-text{font-size:0.82rem;color:var(--system-text);font-style:italic}
|
|
35
44
|
.message--system:hover{background:transparent}
|
|
36
|
-
|
|
37
|
-
|
|
45
|
+
.empty{display:flex;align-items:center;justify-content:center;height:100%;color:var(--text-muted);font-size:0.9rem}
|
|
46
|
+
::-webkit-scrollbar{width:6px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:rgba(128,128,128,0.2);border-radius:3px}
|
|
47
|
+
@media(max-width:600px){.sidebar{width:60px}.sidebar__header{font-size:0.7rem;padding:12px 8px}.sidebar__channel{padding:6px 8px;font-size:0.75rem}.sidebar__channel .hash{display:none}.sidebar__status{display:none}}`;
|
|
38
48
|
const HTML = `<!DOCTYPE html>
|
|
39
49
|
<html lang="en">
|
|
40
50
|
<head>
|
|
@@ -44,38 +54,42 @@ const HTML = `<!DOCTYPE html>
|
|
|
44
54
|
<style>${CSS}</style>
|
|
45
55
|
</head>
|
|
46
56
|
<body>
|
|
47
|
-
<div class="
|
|
48
|
-
<
|
|
49
|
-
<
|
|
50
|
-
<
|
|
51
|
-
|
|
52
|
-
|
|
57
|
+
<div class="app">
|
|
58
|
+
<div class="sidebar">
|
|
59
|
+
<div class="sidebar__header">AgentChannel</div>
|
|
60
|
+
<div class="sidebar__channels" id="channel-list"></div>
|
|
61
|
+
<div class="sidebar__status" id="status">connecting...</div>
|
|
62
|
+
</div>
|
|
63
|
+
<div class="main">
|
|
64
|
+
<div class="main__header" id="current-channel"># all</div>
|
|
65
|
+
<div class="message-list" id="messages">
|
|
66
|
+
<div class="empty">Waiting for messages...</div>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
53
69
|
</div>
|
|
54
70
|
<script src="https://unpkg.com/mqtt/dist/mqtt.min.js"></script>
|
|
55
71
|
<script>
|
|
56
72
|
const CONFIG = __CONFIG__;
|
|
57
73
|
const COLORS = ["#7c6bf0","#e06c58","#4dba87","#e0a84d","#58b4e0","#e05898","#58e0b4","#b458e0"];
|
|
58
74
|
const senderColors = {};
|
|
59
|
-
let
|
|
75
|
+
let activeChannel = "all";
|
|
76
|
+
const allMessages = [];
|
|
60
77
|
|
|
61
78
|
const encoder = new TextEncoder();
|
|
62
79
|
const decoder = new TextDecoder();
|
|
63
80
|
|
|
64
81
|
function getSenderColor(name) {
|
|
65
|
-
if (!senderColors[name])
|
|
66
|
-
const idx = Object.keys(senderColors).length % COLORS.length;
|
|
67
|
-
senderColors[name] = COLORS[idx];
|
|
68
|
-
}
|
|
82
|
+
if (!senderColors[name]) senderColors[name] = COLORS[Object.keys(senderColors).length % COLORS.length];
|
|
69
83
|
return senderColors[name];
|
|
70
84
|
}
|
|
71
85
|
|
|
72
86
|
async function deriveKey(secret) {
|
|
73
|
-
const km = await crypto.subtle.importKey("raw",
|
|
87
|
+
const km = await crypto.subtle.importKey("raw",encoder.encode(secret),"PBKDF2",false,["deriveKey"]);
|
|
74
88
|
return crypto.subtle.deriveKey({name:"PBKDF2",salt:encoder.encode("agentchannel-v1"),iterations:100000,hash:"SHA-256"},km,{name:"AES-GCM",length:256},false,["encrypt","decrypt"]);
|
|
75
89
|
}
|
|
76
90
|
|
|
77
91
|
async function hashRoom(code) {
|
|
78
|
-
const h = await crypto.subtle.digest("SHA-256",
|
|
92
|
+
const h = await crypto.subtle.digest("SHA-256",encoder.encode(code));
|
|
79
93
|
return Array.from(new Uint8Array(h)).map(b=>b.toString(16).padStart(2,"0")).join("").slice(0,16);
|
|
80
94
|
}
|
|
81
95
|
|
|
@@ -86,80 +100,96 @@ async function decrypt(payload, key) {
|
|
|
86
100
|
const tag = Uint8Array.from(atob(p.tag),c=>c.charCodeAt(0));
|
|
87
101
|
const combined = new Uint8Array(data.length+tag.length);
|
|
88
102
|
combined.set(data); combined.set(tag, data.length);
|
|
89
|
-
|
|
90
|
-
return decoder.decode(pt);
|
|
103
|
+
return decoder.decode(await crypto.subtle.decrypt({name:"AES-GCM",iv},key,combined));
|
|
91
104
|
}
|
|
92
105
|
|
|
93
106
|
const msgsEl = document.getElementById("messages");
|
|
94
107
|
const statusEl = document.getElementById("status");
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
function
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
108
|
+
const channelListEl = document.getElementById("channel-list");
|
|
109
|
+
const headerEl = document.getElementById("current-channel");
|
|
110
|
+
|
|
111
|
+
function escapeHtml(s){return s.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">")}
|
|
112
|
+
function highlightMentions(t){return escapeHtml(t).replace(/@(\\w+)/g,'<span class="mention">@$1</span>')}
|
|
113
|
+
|
|
114
|
+
function renderMessages() {
|
|
115
|
+
msgsEl.innerHTML = "";
|
|
116
|
+
const filtered = activeChannel === "all" ? allMessages : allMessages.filter(m => m.channel === activeChannel);
|
|
117
|
+
if (filtered.length === 0) { msgsEl.innerHTML = '<div class="empty">No messages in this channel</div>'; return; }
|
|
118
|
+
let lastSender = null;
|
|
119
|
+
for (const msg of filtered) {
|
|
104
120
|
const div = document.createElement("div");
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
} else {
|
|
111
|
-
const isGrouped = lastSender === msg.sender;
|
|
112
|
-
const color = getSenderColor(msg.sender);
|
|
113
|
-
const time = new Date(msg.timestamp).toLocaleTimeString([], {hour:"2-digit",minute:"2-digit"});
|
|
114
|
-
const div = document.createElement("div");
|
|
115
|
-
|
|
116
|
-
if (isGrouped) {
|
|
117
|
-
div.className = "message message--grouped";
|
|
118
|
-
div.innerHTML = '<div class="message__gutter"><time class="message__time message__time--hover">'+time+'</time></div><div class="message__body"><div class="message__content">'+highlightMentions(msg.content)+'</div></div>';
|
|
121
|
+
const time = new Date(msg.timestamp).toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"});
|
|
122
|
+
if (msg.type === "system") {
|
|
123
|
+
div.className = "message message--system";
|
|
124
|
+
div.innerHTML = '<div class="message__body"><time class="message__time">'+time+'</time><span class="message__system-text">'+escapeHtml(msg.content)+'</span></div>';
|
|
125
|
+
lastSender = null;
|
|
119
126
|
} else {
|
|
120
|
-
|
|
121
|
-
|
|
127
|
+
const isGrouped = lastSender === msg.sender;
|
|
128
|
+
const color = getSenderColor(msg.sender);
|
|
129
|
+
if (isGrouped) {
|
|
130
|
+
div.className = "message message--grouped";
|
|
131
|
+
div.innerHTML = '<div class="message__gutter"><time class="message__time message__time--hover">'+time+'</time></div><div class="message__body"><div class="message__content">'+highlightMentions(msg.content)+'</div></div>';
|
|
132
|
+
} else {
|
|
133
|
+
div.className = "message message--first";
|
|
134
|
+
div.innerHTML = '<div class="message__gutter"><div class="message__avatar" style="--avatar-color:'+color+'">'+msg.sender[0].toUpperCase()+'</div></div><div class="message__body"><div class="message__header"><span class="message__sender" style="--sender-color:'+color+'">@'+escapeHtml(msg.sender)+'</span>'+(activeChannel==="all"?'<span class="message__channel">#'+escapeHtml(msg.channel)+'</span>':'')+'<time class="message__time">'+time+'</time></div><div class="message__content">'+highlightMentions(msg.content)+'</div></div>';
|
|
135
|
+
}
|
|
136
|
+
lastSender = msg.sender;
|
|
122
137
|
}
|
|
123
138
|
msgsEl.appendChild(div);
|
|
124
|
-
lastSender = msg.sender;
|
|
125
139
|
}
|
|
126
140
|
msgsEl.scrollTop = msgsEl.scrollHeight;
|
|
127
141
|
}
|
|
128
142
|
|
|
143
|
+
function renderChannelList() {
|
|
144
|
+
channelListEl.innerHTML = "";
|
|
145
|
+
const items = [{name:"all",label:"All channels"}];
|
|
146
|
+
for (const ch of CONFIG.channels) items.push({name:ch.channel,label:ch.channel});
|
|
147
|
+
for (const item of items) {
|
|
148
|
+
const div = document.createElement("div");
|
|
149
|
+
div.className = "sidebar__channel" + (activeChannel===item.name?" active":"");
|
|
150
|
+
div.innerHTML = '<span class="hash">#</span> '+escapeHtml(item.label);
|
|
151
|
+
div.onclick = () => { activeChannel = item.name; headerEl.textContent = "# "+item.label; renderChannelList(); renderMessages(); };
|
|
152
|
+
channelListEl.appendChild(div);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function addMessage(msg) {
|
|
157
|
+
allMessages.push(msg);
|
|
158
|
+
if (activeChannel === "all" || msg.channel === activeChannel) {
|
|
159
|
+
renderMessages();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
129
163
|
async function init() {
|
|
130
|
-
|
|
164
|
+
renderChannelList();
|
|
165
|
+
|
|
131
166
|
try {
|
|
132
167
|
const res = await fetch("/api/history");
|
|
133
168
|
const history = await res.json();
|
|
134
|
-
for (const msg of history)
|
|
169
|
+
for (const msg of history) allMessages.push(msg);
|
|
170
|
+
renderMessages();
|
|
135
171
|
} catch(e) {}
|
|
136
172
|
|
|
137
173
|
const channels = {};
|
|
138
174
|
for (const ch of CONFIG.channels) {
|
|
139
|
-
channels[ch.channel] = {key:
|
|
175
|
+
channels[ch.channel] = {key:await deriveKey(ch.key),hash:await hashRoom(ch.key),name:ch.channel};
|
|
140
176
|
}
|
|
141
177
|
|
|
142
178
|
const client = mqtt.connect("wss://broker.emqx.io:8084/mqtt");
|
|
143
|
-
client.on("connect",
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
client.on("
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
const msg = JSON.parse(decrypted);
|
|
158
|
-
addMessage(ch.name, msg);
|
|
159
|
-
if (msg.sender !== CONFIG.name && Notification.permission === "granted") {
|
|
160
|
-
new Notification("AgentChannel #"+ch.name, {body:"@"+msg.sender+": "+msg.content});
|
|
161
|
-
}
|
|
162
|
-
} catch(e) {}
|
|
179
|
+
client.on("connect",()=>{statusEl.textContent="connected";statusEl.className="sidebar__status connected";for(const ch of Object.values(channels))client.subscribe("agentchannel/"+ch.hash+"/messages")});
|
|
180
|
+
client.on("close",()=>{statusEl.textContent="disconnected";statusEl.className="sidebar__status"});
|
|
181
|
+
|
|
182
|
+
if(Notification.permission==="default")Notification.requestPermission();
|
|
183
|
+
|
|
184
|
+
client.on("message",async(topic,payload)=>{
|
|
185
|
+
for(const ch of Object.values(channels)){
|
|
186
|
+
if(topic==="agentchannel/"+ch.hash+"/messages"){
|
|
187
|
+
try{
|
|
188
|
+
const msg=JSON.parse(await decrypt(payload.toString(),ch.key));
|
|
189
|
+
msg.channel=ch.name;
|
|
190
|
+
addMessage(msg);
|
|
191
|
+
if(msg.sender!==CONFIG.name&&Notification.permission==="granted") new Notification("AgentChannel #"+ch.name,{body:"@"+msg.sender+": "+msg.content});
|
|
192
|
+
}catch(e){}
|
|
163
193
|
}
|
|
164
194
|
}
|
|
165
195
|
});
|
|
@@ -176,12 +206,10 @@ export function startWebUI(config, port = 3456) {
|
|
|
176
206
|
key: deriveKey(ch.key),
|
|
177
207
|
hash: hashRoom(ch.key),
|
|
178
208
|
}));
|
|
179
|
-
// Server-side MQTT to collect history
|
|
180
209
|
const mqttClient = mqtt.connect("mqtt://broker.emqx.io:1883");
|
|
181
210
|
mqttClient.on("connect", () => {
|
|
182
|
-
for (const cs of channelStates)
|
|
211
|
+
for (const cs of channelStates)
|
|
183
212
|
mqttClient.subscribe(`agentchannel/${cs.hash}/messages`);
|
|
184
|
-
}
|
|
185
213
|
});
|
|
186
214
|
mqttClient.on("message", (_topic, payload) => {
|
|
187
215
|
for (const cs of channelStates) {
|
|
@@ -195,9 +223,7 @@ export function startWebUI(config, port = 3456) {
|
|
|
195
223
|
if (history.length > MAX_HISTORY)
|
|
196
224
|
history.shift();
|
|
197
225
|
}
|
|
198
|
-
catch {
|
|
199
|
-
// ignore
|
|
200
|
-
}
|
|
226
|
+
catch { }
|
|
201
227
|
}
|
|
202
228
|
}
|
|
203
229
|
});
|
package/dist/web.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"web.js","sourceRoot":"","sources":["../src/web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAG3D,MAAM,WAAW,GAAG,GAAG,CAAC;AAExB,MAAM,GAAG,GAAG
|
|
1
|
+
{"version":3,"file":"web.js","sourceRoot":"","sources":["../src/web.ts"],"names":[],"mappings":"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"}
|