@manuelfedele/postino 0.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/.claude-plugin/plugin.json +21 -0
- package/.mcp.json +7 -0
- package/LICENSE +21 -0
- package/README.md +216 -0
- package/commands/postino.md +20 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +96 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +38 -0
- package/dist/tools/messaging.d.ts +2 -0
- package/dist/tools/messaging.js +276 -0
- package/dist/types.d.ts +22 -0
- package/dist/types.js +20 -0
- package/dist/valkey.d.ts +19 -0
- package/dist/valkey.js +109 -0
- package/dist/web/api.d.ts +2 -0
- package/dist/web/api.js +145 -0
- package/dist/web/public/favicon.svg +9 -0
- package/dist/web/public/index.html +301 -0
- package/dist/web/public/logo-dark.svg +31 -0
- package/dist/web/public/logo-horizontal.svg +32 -0
- package/dist/web/public/logo.svg +141 -0
- package/dist/web/server.d.ts +1 -0
- package/dist/web/server.js +71 -0
- package/hooks/check-messages.sh +30 -0
- package/hooks/hooks.json +17 -0
- package/package.json +46 -0
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>postino</title>
|
|
7
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
|
8
|
+
<style>
|
|
9
|
+
:root {
|
|
10
|
+
--bg: #fafafa; --bg2: #fff; --bg3: #f4f4f5; --bg-hover: #e9e9ec;
|
|
11
|
+
--text: #18181b; --text2: #71717a; --text3: #a1a1aa;
|
|
12
|
+
--accent: #2563eb; --accent-hover: #1d4ed8; --accent-bg: #eff6ff;
|
|
13
|
+
--amber: #d97706; --amber-bg: #fffbeb; --amber-border: #fde68a;
|
|
14
|
+
--green: #059669; --red: #dc2626; --red-bg: #fef2f2;
|
|
15
|
+
--border: #e4e4e7; --radius: 10px;
|
|
16
|
+
--mono: 'SF Mono','Cascadia Code','Fira Code','Consolas',monospace;
|
|
17
|
+
--sans: -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
|
|
18
|
+
--t: 150ms ease;
|
|
19
|
+
}
|
|
20
|
+
@media(prefers-color-scheme:dark){:root{
|
|
21
|
+
--bg:#09090b;--bg2:#18181b;--bg3:#27272a;--bg-hover:#323236;
|
|
22
|
+
--text:#fafafa;--text2:#a1a1aa;--text3:#52525b;
|
|
23
|
+
--accent:#3b82f6;--accent-hover:#60a5fa;--accent-bg:#172554;
|
|
24
|
+
--amber:#fbbf24;--amber-bg:#422006;--amber-border:#78350f;
|
|
25
|
+
--green:#34d399;--red:#f87171;--red-bg:#450a0a;
|
|
26
|
+
--border:#3f3f46;
|
|
27
|
+
}}
|
|
28
|
+
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
|
29
|
+
html,body{height:100%}
|
|
30
|
+
body{font-family:var(--sans);background:var(--bg);color:var(--text);line-height:1.5;overflow:hidden}
|
|
31
|
+
::-webkit-scrollbar{width:5px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:var(--text3);border-radius:3px}
|
|
32
|
+
|
|
33
|
+
/* Full-height app shell */
|
|
34
|
+
.app{display:flex;flex-direction:column;height:100vh;max-width:1100px;margin:0 auto;padding:0 20px}
|
|
35
|
+
|
|
36
|
+
/* Header bar */
|
|
37
|
+
.topbar{display:flex;align-items:center;justify-content:space-between;padding:12px 0;border-bottom:1px solid var(--border);flex-shrink:0;gap:8px}
|
|
38
|
+
.logo{display:flex;align-items:center;gap:8px}
|
|
39
|
+
.logo h1{font-family:var(--mono);font-size:16px;font-weight:700;letter-spacing:-0.5px}
|
|
40
|
+
.pill{font-family:var(--mono);font-size:10px;padding:2px 7px;border-radius:20px}
|
|
41
|
+
.pill-muted{color:var(--text3);background:var(--bg3)}
|
|
42
|
+
.pill-accent{color:var(--accent);background:var(--accent-bg)}
|
|
43
|
+
.meta{display:flex;align-items:center;gap:8px;font-size:11px;color:var(--text2);font-family:var(--mono)}
|
|
44
|
+
.meta .sep{color:var(--border)}
|
|
45
|
+
.dot{width:6px;height:6px;border-radius:50%;display:inline-block}
|
|
46
|
+
.dot-on{background:var(--green)}.dot-off{background:var(--red)}
|
|
47
|
+
|
|
48
|
+
/* Tabs row */
|
|
49
|
+
.tabs{display:flex;gap:2px;padding:8px 0;flex-shrink:0}
|
|
50
|
+
.tab{padding:6px 14px;font-size:13px;font-weight:500;font-family:var(--sans);border:none;background:none;color:var(--text2);cursor:pointer;border-radius:6px;transition:all var(--t)}
|
|
51
|
+
.tab:hover{color:var(--text);background:var(--bg3)}
|
|
52
|
+
.tab.active{color:var(--text);background:var(--bg3);font-weight:600}
|
|
53
|
+
.badge{font-size:10px;font-weight:700;padding:1px 5px;border-radius:8px;color:#fff;margin-left:4px;vertical-align:middle}
|
|
54
|
+
.badge-blue{background:var(--accent)}.badge-amber{background:var(--amber)}
|
|
55
|
+
.badge:empty{display:none}
|
|
56
|
+
|
|
57
|
+
/* Panels fill remaining space */
|
|
58
|
+
.panel{display:none;flex:1;min-height:0;padding-bottom:12px}
|
|
59
|
+
.panel.active{display:flex}
|
|
60
|
+
|
|
61
|
+
/* ===== MESSAGES PANEL ===== */
|
|
62
|
+
.msg-panel{gap:12px}
|
|
63
|
+
|
|
64
|
+
/* Sidebar */
|
|
65
|
+
.sidebar{width:220px;display:flex;flex-direction:column;border:1px solid var(--border);border-radius:var(--radius);background:var(--bg2);overflow:hidden;flex-shrink:0}
|
|
66
|
+
.sidebar-header{padding:8px 10px;border-bottom:1px solid var(--border);display:flex;gap:6px}
|
|
67
|
+
.sidebar-header input{flex:1;padding:6px 8px;font-size:12px;min-width:0}
|
|
68
|
+
.sidebar-header button{padding:6px 10px;font-size:12px}
|
|
69
|
+
.sidebar-list{flex:1;overflow-y:auto}
|
|
70
|
+
|
|
71
|
+
.inbox-item{display:flex;align-items:center;justify-content:space-between;padding:8px 10px;font-size:12px;cursor:pointer;border-bottom:1px solid var(--border);transition:background var(--t);gap:6px}
|
|
72
|
+
.inbox-item:last-child{border-bottom:none}
|
|
73
|
+
.inbox-item:hover{background:var(--bg-hover)}
|
|
74
|
+
.inbox-item.active{background:var(--accent);color:#fff}
|
|
75
|
+
.inbox-item.active .inbox-count{background:rgba(255,255,255,0.25)}
|
|
76
|
+
.inbox-item.active .me-tag{color:#fff;background:rgba(255,255,255,0.2)}
|
|
77
|
+
.inbox-item.active .odot{background:#fff}
|
|
78
|
+
.inbox-left{display:flex;align-items:center;gap:6px;min-width:0}
|
|
79
|
+
.inbox-left span{font-family:var(--mono);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
80
|
+
.odot{width:5px;height:5px;border-radius:50%;flex-shrink:0}
|
|
81
|
+
.odot-on{background:var(--green)}.odot-off{background:var(--text3)}
|
|
82
|
+
.inbox-count{font-size:10px;font-family:var(--mono);font-weight:700;padding:0 5px;border-radius:8px;background:var(--bg3);flex-shrink:0}
|
|
83
|
+
.me-tag{font-size:9px;font-weight:600;color:var(--accent);background:var(--accent-bg);padding:1px 4px;border-radius:3px;flex-shrink:0}
|
|
84
|
+
|
|
85
|
+
/* Thread area */
|
|
86
|
+
.thread{flex:1;display:flex;flex-direction:column;border:1px solid var(--border);border-radius:var(--radius);background:var(--bg2);overflow:hidden;min-width:0}
|
|
87
|
+
.thread-hdr{display:flex;align-items:center;justify-content:space-between;padding:8px 14px;border-bottom:1px solid var(--border);flex-shrink:0}
|
|
88
|
+
.thread-hdr h3{font-size:13px;font-family:var(--mono);font-weight:500}
|
|
89
|
+
.thread-body{flex:1;overflow-y:auto;padding:12px 14px;display:flex;flex-direction:column;gap:8px}
|
|
90
|
+
.thread-empty{flex:1;display:flex;align-items:center;justify-content:center;color:var(--text3);font-size:13px}
|
|
91
|
+
|
|
92
|
+
.msg{padding:8px 12px;border-radius:8px;background:var(--bg3)}
|
|
93
|
+
.msg-meta{display:flex;align-items:center;gap:6px;margin-bottom:2px}
|
|
94
|
+
.msg-from{font-size:11px;font-family:var(--mono);font-weight:600;color:var(--accent)}
|
|
95
|
+
.msg-time{font-size:10px;font-family:var(--mono);color:var(--text3)}
|
|
96
|
+
.msg-body{font-size:13px;white-space:pre-wrap;word-break:break-word}
|
|
97
|
+
|
|
98
|
+
.compose{display:none;padding:8px 10px;border-top:1px solid var(--border);gap:6px;flex-shrink:0}
|
|
99
|
+
.compose.visible{display:flex}
|
|
100
|
+
.compose input{flex:1;padding:6px 10px;font-size:12px}
|
|
101
|
+
.compose input:first-child{max-width:100px}
|
|
102
|
+
.compose button{padding:6px 12px;font-size:12px}
|
|
103
|
+
|
|
104
|
+
/* ===== BROADCASTS PANEL ===== */
|
|
105
|
+
.bc-panel{flex-direction:column;gap:12px}
|
|
106
|
+
.bc-form{display:flex;gap:8px;flex-shrink:0}
|
|
107
|
+
.bc-form input{flex:1;padding:8px 12px;font-size:13px}
|
|
108
|
+
.bc-list{flex:1;overflow-y:auto;display:flex;flex-direction:column;gap:8px}
|
|
109
|
+
.bc-item{padding:10px 14px;border-radius:var(--radius);background:var(--amber-bg);border:1px solid var(--amber-border)}
|
|
110
|
+
.bc-meta{display:flex;align-items:center;gap:6px;margin-bottom:2px}
|
|
111
|
+
.bc-from{font-size:11px;font-family:var(--mono);font-weight:600;color:var(--amber)}
|
|
112
|
+
.bc-body{font-size:13px;white-space:pre-wrap;word-break:break-word}
|
|
113
|
+
|
|
114
|
+
/* Shared */
|
|
115
|
+
input{font-family:var(--mono);font-size:13px;padding:8px 12px;border:1px solid var(--border);border-radius:6px;background:var(--bg2);color:var(--text);outline:none;transition:border-color var(--t),box-shadow var(--t)}
|
|
116
|
+
input:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(37,99,235,0.12)}
|
|
117
|
+
input::placeholder{color:var(--text3)}
|
|
118
|
+
.btn{font-family:var(--sans);font-size:13px;font-weight:500;padding:8px 14px;border:none;border-radius:6px;cursor:pointer;transition:all var(--t);white-space:nowrap}
|
|
119
|
+
.btn-primary{background:var(--accent);color:#fff}.btn-primary:hover{background:var(--accent-hover)}
|
|
120
|
+
.btn-amber{background:var(--amber);color:#fff}.btn-amber:hover{opacity:.9}
|
|
121
|
+
.btn-danger{background:none;color:var(--red)}.btn-danger:hover{background:var(--red-bg)}
|
|
122
|
+
.empty{padding:40px;text-align:center;color:var(--text3);font-size:13px}
|
|
123
|
+
|
|
124
|
+
/* Toast */
|
|
125
|
+
.toast-box{position:fixed;bottom:16px;right:16px;display:flex;flex-direction:column;gap:6px;z-index:200}
|
|
126
|
+
.toast{padding:8px 14px;border-radius:6px;font-size:12px;font-family:var(--sans);background:var(--bg2);color:var(--text);border:1px solid var(--border);box-shadow:0 2px 8px rgba(0,0,0,0.15);animation:t-in .2s ease;max-width:280px}
|
|
127
|
+
.toast-ok{border-left:3px solid var(--green)}.toast-info{border-left:3px solid var(--accent)}
|
|
128
|
+
@keyframes t-in{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
|
|
129
|
+
|
|
130
|
+
@media(max-width:700px){
|
|
131
|
+
.sidebar{width:100%}
|
|
132
|
+
.msg-panel{flex-direction:column}
|
|
133
|
+
.topbar{flex-wrap:wrap}
|
|
134
|
+
}
|
|
135
|
+
</style>
|
|
136
|
+
</head>
|
|
137
|
+
<body>
|
|
138
|
+
<div class="app">
|
|
139
|
+
|
|
140
|
+
<!-- Top bar -->
|
|
141
|
+
<div class="topbar">
|
|
142
|
+
<div class="logo">
|
|
143
|
+
<h1>postino</h1>
|
|
144
|
+
<span class="pill pill-muted">v0.1.0</span>
|
|
145
|
+
<span class="pill pill-accent" id="agent-id"></span>
|
|
146
|
+
</div>
|
|
147
|
+
<div class="meta">
|
|
148
|
+
<span id="stat-agents">-</span>
|
|
149
|
+
<span class="sep">|</span>
|
|
150
|
+
<span id="stat-msg">-</span>
|
|
151
|
+
<span class="sep">|</span>
|
|
152
|
+
<span id="stat-bc">-</span>
|
|
153
|
+
<span class="sep">|</span>
|
|
154
|
+
<span class="dot dot-off" id="status-dot"></span>
|
|
155
|
+
<span id="status-text">connecting</span>
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
<!-- Tabs -->
|
|
160
|
+
<div class="tabs">
|
|
161
|
+
<button class="tab active" data-tab="messages">Messages <span class="badge badge-blue" id="msg-badge"></span></button>
|
|
162
|
+
<button class="tab" data-tab="broadcasts">Broadcasts <span class="badge badge-amber" id="bc-badge"></span></button>
|
|
163
|
+
</div>
|
|
164
|
+
|
|
165
|
+
<!-- ===== MESSAGES ===== -->
|
|
166
|
+
<div class="panel msg-panel active" id="panel-messages">
|
|
167
|
+
<div class="sidebar">
|
|
168
|
+
<div class="sidebar-header">
|
|
169
|
+
<input id="new-inbox" type="text" placeholder="New inbox...">
|
|
170
|
+
<button class="btn btn-primary" id="create-inbox-btn">+</button>
|
|
171
|
+
</div>
|
|
172
|
+
<div class="sidebar-list" id="inbox-list">
|
|
173
|
+
<div class="empty" style="padding:20px">No agents yet</div>
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
<div class="thread">
|
|
177
|
+
<div class="thread-hdr">
|
|
178
|
+
<h3 id="thread-title">Select an inbox</h3>
|
|
179
|
+
<button class="btn btn-danger" id="clear-inbox-btn" style="display:none">Clear</button>
|
|
180
|
+
</div>
|
|
181
|
+
<div class="thread-body" id="thread-body">
|
|
182
|
+
<div class="thread-empty">Select an inbox to view messages</div>
|
|
183
|
+
</div>
|
|
184
|
+
<div class="compose" id="msg-compose">
|
|
185
|
+
<input id="msg-from" type="text" placeholder="From...">
|
|
186
|
+
<input id="msg-body" type="text" placeholder="Message... (Enter)">
|
|
187
|
+
<button class="btn btn-primary" id="send-btn">Send</button>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
<!-- ===== BROADCASTS ===== -->
|
|
193
|
+
<div class="panel bc-panel" id="panel-broadcasts">
|
|
194
|
+
<div class="bc-form">
|
|
195
|
+
<input id="bc-body" type="text" placeholder="Broadcast to all agents... (Enter)">
|
|
196
|
+
<button class="btn btn-amber" id="bc-send-btn">Broadcast</button>
|
|
197
|
+
<button class="btn btn-danger" id="bc-clear-btn">Clear all</button>
|
|
198
|
+
</div>
|
|
199
|
+
<div class="bc-list" id="bc-list">
|
|
200
|
+
<div class="empty">No broadcasts yet</div>
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
</div>
|
|
205
|
+
|
|
206
|
+
<div class="toast-box" id="toasts"></div>
|
|
207
|
+
|
|
208
|
+
<script>
|
|
209
|
+
const API=location.origin+'/api';
|
|
210
|
+
let agents=[],broadcasts=[],activeInbox=null,myAgent=null;
|
|
211
|
+
|
|
212
|
+
function toast(m,t='info'){const e=document.createElement('div');e.className='toast toast-'+(t==='success'?'ok':t);e.textContent=m;document.getElementById('toasts').appendChild(e);setTimeout(()=>{e.style.opacity='0';setTimeout(()=>e.remove(),200)},3000)}
|
|
213
|
+
|
|
214
|
+
async function api(p,o={}){try{const r=await fetch(API+p,{headers:{'Content-Type':'application/json'},...o});return r.json()}catch{toast('Connection error');return null}}
|
|
215
|
+
|
|
216
|
+
// Tabs
|
|
217
|
+
document.querySelectorAll('.tab').forEach(t=>t.addEventListener('click',()=>{
|
|
218
|
+
document.querySelectorAll('.tab').forEach(x=>x.classList.remove('active'));
|
|
219
|
+
document.querySelectorAll('.panel').forEach(x=>x.classList.remove('active'));
|
|
220
|
+
t.classList.add('active');document.getElementById('panel-'+t.dataset.tab).classList.add('active');
|
|
221
|
+
}));
|
|
222
|
+
|
|
223
|
+
// Detect self
|
|
224
|
+
async function detectMe(){
|
|
225
|
+
const r=await api('/agents');if(!r)return;agents=r;
|
|
226
|
+
const on=agents.filter(a=>a.online);
|
|
227
|
+
if(on.length===1)myAgent=on[0].name;
|
|
228
|
+
const el=document.getElementById('agent-id');
|
|
229
|
+
el.textContent=myAgent||'';el.title=myAgent?'You':'';
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Agents
|
|
233
|
+
async function loadAgents(){const r=await api('/agents');if(!r)return;agents=r;renderInbox();updateBadge()}
|
|
234
|
+
|
|
235
|
+
function renderInbox(){
|
|
236
|
+
const l=document.getElementById('inbox-list');
|
|
237
|
+
if(!agents.length){l.innerHTML='<div class="empty" style="padding:20px">No agents yet</div>';return}
|
|
238
|
+
l.innerHTML=agents.map(a=>{
|
|
239
|
+
const me=a.name===myAgent,act=a.name===activeInbox;
|
|
240
|
+
return`<div class="inbox-item${act?' active':''}" onclick="selInbox('${esc(a.name)}')">
|
|
241
|
+
<div class="inbox-left"><div class="odot ${a.online?'odot-on':'odot-off'}"></div><span>${esc(a.name)}</span>${me?'<span class="me-tag">you</span>':''}</div>
|
|
242
|
+
${a.messageCount>0?`<span class="inbox-count">${a.messageCount}</span>`:''}
|
|
243
|
+
</div>`}).join('');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function updateBadge(){const t=agents.reduce((s,a)=>s+a.messageCount,0);document.getElementById('msg-badge').textContent=t>0?t:''}
|
|
247
|
+
|
|
248
|
+
window.selInbox=async function(n){
|
|
249
|
+
activeInbox=n;renderInbox();
|
|
250
|
+
document.getElementById('thread-title').textContent=n;
|
|
251
|
+
document.getElementById('clear-inbox-btn').style.display='';
|
|
252
|
+
document.getElementById('msg-compose').classList.add('visible');
|
|
253
|
+
const msgs=await api('/messages/'+encodeURIComponent(n));
|
|
254
|
+
const b=document.getElementById('thread-body');
|
|
255
|
+
if(!msgs||!msgs.length){b.innerHTML='<div class="thread-empty">No messages</div>';return}
|
|
256
|
+
b.innerHTML=msgs.map(m=>`<div class="msg"><div class="msg-meta"><span class="msg-from">${esc(m.from)}</span><span class="msg-time">${fmtT(m.timestamp)}</span></div><div class="msg-body">${esc(m.body)}</div></div>`).join('');
|
|
257
|
+
b.scrollTop=b.scrollHeight;
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
document.getElementById('clear-inbox-btn').onclick=async()=>{if(!activeInbox||!confirm('Clear "'+activeInbox+'"?'))return;await api('/messages/'+encodeURIComponent(activeInbox),{method:'DELETE'});toast('Cleared','success');await loadAgents();selInbox(activeInbox)};
|
|
261
|
+
|
|
262
|
+
document.getElementById('create-inbox-btn').onclick=async()=>{const n=document.getElementById('new-inbox').value.trim();if(!n)return;await api('/messages',{method:'POST',body:JSON.stringify({to:n,from:'system',body:'Inbox created'})});document.getElementById('new-inbox').value='';toast('"'+n+'" created','success');await loadAgents();selInbox(n)};
|
|
263
|
+
document.getElementById('new-inbox').addEventListener('keydown',e=>{if(e.key==='Enter')document.getElementById('create-inbox-btn').click()});
|
|
264
|
+
|
|
265
|
+
async function sendMsg(){if(!activeInbox)return;const f=document.getElementById('msg-from').value.trim()||'web-ui',b=document.getElementById('msg-body').value.trim();if(!b)return;await api('/messages',{method:'POST',body:JSON.stringify({to:activeInbox,from:f,body:b})});document.getElementById('msg-body').value='';await loadAgents();selInbox(activeInbox)}
|
|
266
|
+
document.getElementById('send-btn').onclick=sendMsg;
|
|
267
|
+
document.getElementById('msg-body').addEventListener('keydown',e=>{if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();sendMsg()}});
|
|
268
|
+
|
|
269
|
+
// Broadcasts
|
|
270
|
+
async function loadBc(){const r=await api('/broadcasts');if(!r)return;broadcasts=r;renderBc();document.getElementById('bc-badge').textContent=broadcasts.length>0?broadcasts.length:''}
|
|
271
|
+
|
|
272
|
+
function renderBc(){
|
|
273
|
+
const l=document.getElementById('bc-list');
|
|
274
|
+
if(!broadcasts.length){l.innerHTML='<div class="empty">No broadcasts yet</div>';return}
|
|
275
|
+
l.innerHTML=broadcasts.slice().reverse().map(b=>`<div class="bc-item"><div class="bc-meta"><span class="bc-from">${esc(b.from)}</span><span class="msg-time">${fmtT(b.timestamp)}</span></div><div class="bc-body">${esc(b.body)}</div></div>`).join('');
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async function sendBc(){const b=document.getElementById('bc-body').value.trim();if(!b)return;await api('/broadcasts',{method:'POST',body:JSON.stringify({body:b})});document.getElementById('bc-body').value='';toast('Broadcast sent','success');await loadBc()}
|
|
279
|
+
document.getElementById('bc-send-btn').onclick=sendBc;
|
|
280
|
+
document.getElementById('bc-body').addEventListener('keydown',e=>{if(e.key==='Enter')sendBc()});
|
|
281
|
+
document.getElementById('bc-clear-btn').onclick=async()=>{if(!confirm('Clear all broadcasts?'))return;await api('/broadcasts',{method:'DELETE'});toast('Cleared','success');await loadBc()};
|
|
282
|
+
|
|
283
|
+
// SSE
|
|
284
|
+
function connectSSE(){
|
|
285
|
+
const es=new EventSource(API+'/events'),d=document.getElementById('status-dot'),t=document.getElementById('status-text');
|
|
286
|
+
es.addEventListener('update',e=>{loadAgents();loadBc();loadStats();try{const x=JSON.parse(e.data);if(x.type==='msg_send'&&x.to===myAgent)toast('Message from '+x.from,'info');if(x.type==='broadcast')toast('Broadcast from '+x.from,'info');if(x.type==='agent_rename')toast(x.oldName+' \u2192 '+x.newName,'info')}catch{}});
|
|
287
|
+
es.onopen=()=>{d.className='dot dot-on';t.textContent='live'};
|
|
288
|
+
es.onerror=()=>{d.className='dot dot-off';t.textContent='reconnecting'};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async function loadStats(){const s=await api('/stats');if(!s)return;document.getElementById('stat-agents').textContent=s.agentCount+' agents';document.getElementById('stat-msg').textContent=s.messageCount+' msg';document.getElementById('stat-bc').textContent=s.broadcastCount+' bc'}
|
|
292
|
+
|
|
293
|
+
function esc(s){if(!s)return'';const d=document.createElement('div');d.textContent=s;return d.innerHTML}
|
|
294
|
+
function fmtT(i){try{const d=new Date(i),n=new Date();return d.toDateString()===n.toDateString()?d.toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'}):d.toLocaleDateString([],{month:'short',day:'numeric'})+' '+d.toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'})}catch{return i}}
|
|
295
|
+
|
|
296
|
+
document.addEventListener('keydown',e=>{if(e.target.tagName==='INPUT')return;if(e.key==='1')document.querySelector('[data-tab="messages"]').click();if(e.key==='2')document.querySelector('[data-tab="broadcasts"]').click()});
|
|
297
|
+
|
|
298
|
+
detectMe().then(()=>{loadAgents();loadBc();loadStats();connectSSE()});
|
|
299
|
+
</script>
|
|
300
|
+
</body>
|
|
301
|
+
</html>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<svg width="440" height="60" viewBox="0 0 440 60" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<defs>
|
|
3
|
+
<clipPath id="pc">
|
|
4
|
+
<rect x="42" y="12" width="116" height="36"/>
|
|
5
|
+
</clipPath>
|
|
6
|
+
</defs>
|
|
7
|
+
<g transform="translate(100,30)">
|
|
8
|
+
<rect x="-58" y="-18" width="116" height="36" rx="18" fill="none" stroke="white" stroke-width="2.5"/>
|
|
9
|
+
<circle cx="-58" cy="0" r="18" fill="none" stroke="white" stroke-width="2.5"/>
|
|
10
|
+
<line x1="-58" y1="-9" x2="-58" y2="9" stroke="white" stroke-width="2.5" stroke-linecap="round"/>
|
|
11
|
+
<g clip-path="url(#pc)" transform="translate(-100,-30)">
|
|
12
|
+
<g transform="translate(72,30)">
|
|
13
|
+
<rect x="-13" y="-9" width="26" height="18" rx="2" fill="white"/>
|
|
14
|
+
<polyline points="-13,-9 0,-1 13,-9" fill="none" stroke="#333" stroke-width="0.8"/>
|
|
15
|
+
<line x1="-13" y1="9" x2="0" y2="1" stroke="#333" stroke-width="0.8" fill="none"/>
|
|
16
|
+
<line x1="13" y1="9" x2="0" y2="1" stroke="#333" stroke-width="0.8" fill="none"/>
|
|
17
|
+
</g>
|
|
18
|
+
<g transform="translate(120,30)">
|
|
19
|
+
<rect x="-13" y="-9" width="26" height="18" rx="2" fill="white" opacity="0.5"/>
|
|
20
|
+
</g>
|
|
21
|
+
<g transform="translate(168,30)">
|
|
22
|
+
<rect x="-13" y="-9" width="26" height="18" rx="2" fill="white" opacity="0.2"/>
|
|
23
|
+
</g>
|
|
24
|
+
</g>
|
|
25
|
+
<line x1="-88" y1="0" x2="-76" y2="0" stroke="white" stroke-width="2" fill="none" stroke-linecap="round"/>
|
|
26
|
+
<line x1="62" y1="-6" x2="80" y2="-16" stroke="white" stroke-width="1.5" fill="none" stroke-linecap="round" opacity="0.9"/>
|
|
27
|
+
<line x1="62" y1="6" x2="80" y2="16" stroke="white" stroke-width="1.5" fill="none" stroke-linecap="round" opacity="0.35"/>
|
|
28
|
+
</g>
|
|
29
|
+
<text x="210" y="24" text-anchor="start" font-size="28" font-weight="500" fill="white" letter-spacing="-0.5px" font-family="ui-sans-serif,system-ui,sans-serif">postino</text>
|
|
30
|
+
<text x="210" y="43" text-anchor="start" font-size="9" fill="rgba(255,255,255,0.45)" letter-spacing="1.5px" font-family="ui-monospace,monospace">message broker for agents</text>
|
|
31
|
+
</svg>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<svg width="440" height="60" viewBox="0 0 440 60" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<defs>
|
|
3
|
+
<marker id="ar-sm" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="4" markerHeight="4" orient="auto-start-reverse">
|
|
4
|
+
<path d="M2 1L8 5L2 9" fill="none" stroke="#e63030" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
|
5
|
+
</marker>
|
|
6
|
+
<clipPath id="pipe-clip-h">
|
|
7
|
+
<rect x="42" y="12" width="116" height="36" rx="0"/>
|
|
8
|
+
</clipPath>
|
|
9
|
+
</defs>
|
|
10
|
+
<g transform="translate(100,30)">
|
|
11
|
+
<rect x="-58" y="-18" width="116" height="36" rx="18" fill="none" stroke="#e63030" stroke-width="2.5"/>
|
|
12
|
+
<circle cx="-58" cy="0" r="18" fill="none" stroke="#e63030" stroke-width="2.5"/>
|
|
13
|
+
<line x1="-58" y1="-9" x2="-58" y2="9" stroke="#e63030" stroke-width="2.5" stroke-linecap="round"/>
|
|
14
|
+
<g clip-path="url(#pipe-clip-h)" transform="translate(-100,-30)">
|
|
15
|
+
<g transform="translate(72,30)">
|
|
16
|
+
<rect x="-13" y="-9" width="26" height="18" rx="2" fill="#e63030"/>
|
|
17
|
+
<polyline points="-13,-9 0,-1 13,-9" fill="none" stroke="#b82020" stroke-width="0.8"/>
|
|
18
|
+
</g>
|
|
19
|
+
<g transform="translate(120,30)">
|
|
20
|
+
<rect x="-13" y="-9" width="26" height="18" rx="2" fill="#e63030" opacity="0.55"/>
|
|
21
|
+
</g>
|
|
22
|
+
<g transform="translate(168,30)">
|
|
23
|
+
<rect x="-13" y="-9" width="26" height="18" rx="2" fill="#e63030" opacity="0.22"/>
|
|
24
|
+
</g>
|
|
25
|
+
</g>
|
|
26
|
+
<line x1="-88" y1="0" x2="-62" y2="0" stroke="#e63030" stroke-width="2" fill="none" stroke-linecap="round" marker-end="url(#ar-sm)"/>
|
|
27
|
+
<line x1="62" y1="-6" x2="82" y2="-16" stroke="#e63030" stroke-width="1.5" fill="none" stroke-linecap="round" marker-end="url(#ar-sm)"/>
|
|
28
|
+
<line x1="62" y1="6" x2="82" y2="16" stroke="#e63030" stroke-width="1.5" fill="none" stroke-linecap="round" marker-end="url(#ar-sm)" opacity="0.4"/>
|
|
29
|
+
</g>
|
|
30
|
+
<text x="210" y="24" text-anchor="start" font-size="28" font-weight="500" fill="#111" letter-spacing="-0.5px" font-family="ui-sans-serif,system-ui,sans-serif">postino</text>
|
|
31
|
+
<text x="210" y="43" text-anchor="start" font-size="9" fill="#888" letter-spacing="1.5px" font-family="ui-monospace,monospace">message broker for agents</text>
|
|
32
|
+
</svg>
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
<svg width="680" height="820" viewBox="0 0 680 820" role="img" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<title>Postino logo</title>
|
|
3
|
+
<desc>Pipe concept logo for Postino, an open-source message broker for agents</desc>
|
|
4
|
+
|
|
5
|
+
<defs>
|
|
6
|
+
<marker id="ar" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="5" markerHeight="5" orient="auto-start-reverse">
|
|
7
|
+
<path d="M2 1L8 5L2 9" fill="none" stroke="#e63030" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
|
8
|
+
</marker>
|
|
9
|
+
<marker id="ar-sm" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="4" markerHeight="4" orient="auto-start-reverse">
|
|
10
|
+
<path d="M2 1L8 5L2 9" fill="none" stroke="#e63030" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
|
11
|
+
</marker>
|
|
12
|
+
<clipPath id="pipe-clip">
|
|
13
|
+
<rect x="181" y="126" width="250" height="48" rx="0"/>
|
|
14
|
+
</clipPath>
|
|
15
|
+
<clipPath id="pipe-clip-h">
|
|
16
|
+
<rect x="162" y="367" width="116" height="36" rx="0"/>
|
|
17
|
+
</clipPath>
|
|
18
|
+
</defs>
|
|
19
|
+
|
|
20
|
+
<style>
|
|
21
|
+
text { font-family: ui-sans-serif, system-ui, sans-serif; }
|
|
22
|
+
.label { font-size: 10px; fill: #999; letter-spacing: 1.5px; }
|
|
23
|
+
.divider { stroke: #ddd; stroke-width: 0.5; }
|
|
24
|
+
.wm-lg { font-size: 48px; font-weight: 500; fill: #111; letter-spacing: -1.5px; }
|
|
25
|
+
.wm-md { font-size: 28px; font-weight: 500; fill: #111; letter-spacing: -0.5px; }
|
|
26
|
+
.tag-lg { font-size: 12px; fill: #888; letter-spacing: 2.5px; font-family: ui-monospace, monospace; }
|
|
27
|
+
.tag-sm { font-size: 9px; fill: #888; letter-spacing: 1.5px; font-family: ui-monospace, monospace; }
|
|
28
|
+
.mono { font-family: ui-monospace, monospace; }
|
|
29
|
+
</style>
|
|
30
|
+
|
|
31
|
+
<!-- ── PRIMARY MARK ─────────────────────────── -->
|
|
32
|
+
<text x="340" y="56" text-anchor="middle" class="label mono">primary mark</text>
|
|
33
|
+
|
|
34
|
+
<g transform="translate(340,160)">
|
|
35
|
+
<!-- pipe -->
|
|
36
|
+
<rect x="-110" y="-24" width="220" height="48" rx="24" fill="none" stroke="#e63030" stroke-width="2.5"/>
|
|
37
|
+
<!-- left cap -->
|
|
38
|
+
<circle cx="-110" cy="0" r="24" fill="none" stroke="#e63030" stroke-width="2.5"/>
|
|
39
|
+
<line x1="-110" y1="-12" x2="-110" y2="12" stroke="#e63030" stroke-width="3" stroke-linecap="round"/>
|
|
40
|
+
<!-- envelope 1 -->
|
|
41
|
+
<g clip-path="url(#pipe-clip)" transform="translate(-340,-160)">
|
|
42
|
+
<g transform="translate(255,160)">
|
|
43
|
+
<rect x="-19" y="-13" width="38" height="26" rx="3" fill="#e63030"/>
|
|
44
|
+
<polyline points="-19,-13 0,-1 19,-13" fill="none" stroke="#b82020" stroke-width="1"/>
|
|
45
|
+
<line x1="-19" y1="13" x2="0" y2="1" stroke="#b82020" stroke-width="1" fill="none"/>
|
|
46
|
+
<line x1="19" y1="13" x2="0" y2="1" stroke="#b82020" stroke-width="1" fill="none"/>
|
|
47
|
+
</g>
|
|
48
|
+
<g transform="translate(340,160)">
|
|
49
|
+
<rect x="-19" y="-13" width="38" height="26" rx="3" fill="#e63030" opacity="0.55"/>
|
|
50
|
+
<polyline points="-19,-13 0,-1 19,-13" fill="none" stroke="#b82020" stroke-width="0.8" opacity="0.55"/>
|
|
51
|
+
</g>
|
|
52
|
+
<g transform="translate(425,160)">
|
|
53
|
+
<rect x="-19" y="-13" width="38" height="26" rx="3" fill="#e63030" opacity="0.22"/>
|
|
54
|
+
</g>
|
|
55
|
+
</g>
|
|
56
|
+
<!-- arrow in -->
|
|
57
|
+
<line x1="-160" y1="0" x2="-136" y2="0" stroke="#e63030" stroke-width="2" fill="none" stroke-linecap="round" marker-end="url(#ar)"/>
|
|
58
|
+
<!-- arrows out -->
|
|
59
|
+
<line x1="136" y1="-10" x2="172" y2="-26" stroke="#e63030" stroke-width="2" fill="none" stroke-linecap="round" marker-end="url(#ar)"/>
|
|
60
|
+
<line x1="136" y1="10" x2="172" y2="26" stroke="#e63030" stroke-width="2" fill="none" stroke-linecap="round" marker-end="url(#ar)" opacity="0.4"/>
|
|
61
|
+
</g>
|
|
62
|
+
|
|
63
|
+
<text x="340" y="242" text-anchor="middle" class="wm-lg">postino</text>
|
|
64
|
+
<text x="340" y="266" text-anchor="middle" class="tag-lg">message broker for agents</text>
|
|
65
|
+
|
|
66
|
+
<!-- ── HORIZONTAL LOCKUP ───────────────────── -->
|
|
67
|
+
<line x1="40" y1="310" x2="640" y2="310" class="divider"/>
|
|
68
|
+
<text x="60" y="336" class="label mono">horizontal lockup</text>
|
|
69
|
+
|
|
70
|
+
<g transform="translate(220,385)">
|
|
71
|
+
<rect x="-58" y="-18" width="116" height="36" rx="18" fill="none" stroke="#e63030" stroke-width="2.5"/>
|
|
72
|
+
<circle cx="-58" cy="0" r="18" fill="none" stroke="#e63030" stroke-width="2.5"/>
|
|
73
|
+
<line x1="-58" y1="-9" x2="-58" y2="9" stroke="#e63030" stroke-width="2.5" stroke-linecap="round"/>
|
|
74
|
+
<g clip-path="url(#pipe-clip-h)" transform="translate(-220,-385)">
|
|
75
|
+
<g transform="translate(192,385)">
|
|
76
|
+
<rect x="-13" y="-9" width="26" height="18" rx="2" fill="#e63030"/>
|
|
77
|
+
<polyline points="-13,-9 0,-1 13,-9" fill="none" stroke="#b82020" stroke-width="0.8"/>
|
|
78
|
+
</g>
|
|
79
|
+
<g transform="translate(240,385)">
|
|
80
|
+
<rect x="-13" y="-9" width="26" height="18" rx="2" fill="#e63030" opacity="0.55"/>
|
|
81
|
+
</g>
|
|
82
|
+
<g transform="translate(288,385)">
|
|
83
|
+
<rect x="-13" y="-9" width="26" height="18" rx="2" fill="#e63030" opacity="0.22"/>
|
|
84
|
+
</g>
|
|
85
|
+
</g>
|
|
86
|
+
<line x1="-88" y1="0" x2="-62" y2="0" stroke="#e63030" stroke-width="2" fill="none" stroke-linecap="round" marker-end="url(#ar-sm)"/>
|
|
87
|
+
<line x1="62" y1="-6" x2="82" y2="-16" stroke="#e63030" stroke-width="1.5" fill="none" stroke-linecap="round" marker-end="url(#ar-sm)"/>
|
|
88
|
+
<line x1="62" y1="6" x2="82" y2="16" stroke="#e63030" stroke-width="1.5" fill="none" stroke-linecap="round" marker-end="url(#ar-sm)" opacity="0.4"/>
|
|
89
|
+
</g>
|
|
90
|
+
|
|
91
|
+
<text x="330" y="379" text-anchor="start" class="wm-md">postino</text>
|
|
92
|
+
<text x="330" y="398" text-anchor="start" class="tag-sm">message broker for agents</text>
|
|
93
|
+
|
|
94
|
+
<!-- ── FAVICON SIZES ──────────────────────── -->
|
|
95
|
+
<line x1="40" y1="440" x2="640" y2="440" class="divider"/>
|
|
96
|
+
<text x="60" y="466" class="label mono">small — favicon territory</text>
|
|
97
|
+
|
|
98
|
+
<rect x="60" y="484" width="64" height="64" rx="14" fill="#e63030"/>
|
|
99
|
+
<g transform="translate(92,516)">
|
|
100
|
+
<rect x="-22" y="-9" width="44" height="18" rx="9" fill="none" stroke="white" stroke-width="2"/>
|
|
101
|
+
<line x1="-22" y1="-4" x2="-22" y2="4" stroke="white" stroke-width="2" stroke-linecap="round"/>
|
|
102
|
+
<rect x="-14" y="-5" width="11" height="9" rx="1" fill="white"/>
|
|
103
|
+
<rect x="0" y="-5" width="11" height="9" rx="1" fill="white" opacity="0.55"/>
|
|
104
|
+
</g>
|
|
105
|
+
|
|
106
|
+
<rect x="148" y="500" width="32" height="32" rx="7" fill="#e63030"/>
|
|
107
|
+
<g transform="translate(164,516)">
|
|
108
|
+
<rect x="-10" y="-4" width="20" height="8" rx="4" fill="none" stroke="white" stroke-width="1.5"/>
|
|
109
|
+
<line x1="-10" y1="-2" x2="-10" y2="2" stroke="white" stroke-width="1.5" stroke-linecap="round"/>
|
|
110
|
+
<rect x="-6" y="-2.5" width="6" height="4" rx="0.5" fill="white"/>
|
|
111
|
+
<rect x="1" y="-2.5" width="6" height="4" rx="0.5" fill="white" opacity="0.5"/>
|
|
112
|
+
</g>
|
|
113
|
+
|
|
114
|
+
<text x="200" y="504" class="tag-sm mono">64 px</text>
|
|
115
|
+
<text x="200" y="522" class="tag-sm mono">32 px</text>
|
|
116
|
+
|
|
117
|
+
<!-- ── DARK VARIANT ────────────────────────── -->
|
|
118
|
+
<line x1="40" y1="580" x2="640" y2="580" class="divider"/>
|
|
119
|
+
<text x="60" y="606" class="label mono">dark background variant</text>
|
|
120
|
+
|
|
121
|
+
<rect x="60" y="622" width="560" height="150" rx="12" fill="#0f0f0f"/>
|
|
122
|
+
|
|
123
|
+
<g transform="translate(340,690)">
|
|
124
|
+
<rect x="-110" y="-24" width="220" height="48" rx="24" fill="none" stroke="white" stroke-width="2.5"/>
|
|
125
|
+
<circle cx="-110" cy="0" r="24" fill="none" stroke="white" stroke-width="2.5"/>
|
|
126
|
+
<line x1="-110" y1="-12" x2="-110" y2="12" stroke="white" stroke-width="3" stroke-linecap="round"/>
|
|
127
|
+
<rect x="-85" y="-13" width="38" height="26" rx="3" fill="white"/>
|
|
128
|
+
<polyline points="-85,-13 -66,-1 -47,-13" fill="none" stroke="#0f0f0f" stroke-width="1"/>
|
|
129
|
+
<line x1="-85" y1="13" x2="-66" y2="1" stroke="#0f0f0f" stroke-width="1" fill="none"/>
|
|
130
|
+
<line x1="-47" y1="13" x2="-66" y2="1" stroke="#0f0f0f" stroke-width="1" fill="none"/>
|
|
131
|
+
<rect x="-28" y="-13" width="38" height="26" rx="3" fill="white" opacity="0.5"/>
|
|
132
|
+
<rect x="29" y="-13" width="38" height="26" rx="3" fill="white" opacity="0.2"/>
|
|
133
|
+
<line x1="-160" y1="0" x2="-136" y2="0" stroke="white" stroke-width="2" fill="none" stroke-linecap="round"/>
|
|
134
|
+
<line x1="136" y1="-10" x2="170" y2="-26" stroke="white" stroke-width="1.5" fill="none" stroke-linecap="round" opacity="0.9"/>
|
|
135
|
+
<line x1="136" y1="10" x2="170" y2="26" stroke="white" stroke-width="1.5" fill="none" stroke-linecap="round" opacity="0.35"/>
|
|
136
|
+
</g>
|
|
137
|
+
|
|
138
|
+
<text x="340" y="740" text-anchor="middle" font-size="28" font-weight="500" fill="white" letter-spacing="-0.5px">postino</text>
|
|
139
|
+
<text x="340" y="758" text-anchor="middle" font-size="11" fill="rgba(255,255,255,0.45)" letter-spacing="2.5px" font-family="ui-monospace,monospace">message broker for agents</text>
|
|
140
|
+
|
|
141
|
+
</svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function startWebServer(port: number, attempt?: number): void;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { cors } from "hono/cors";
|
|
3
|
+
import { serve } from "@hono/node-server";
|
|
4
|
+
import { api } from "./api.js";
|
|
5
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
6
|
+
import { join, dirname } from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
// Resolve a file from the public directory (checks dist and src locations)
|
|
10
|
+
function findPublicFile(filename) {
|
|
11
|
+
const candidates = [
|
|
12
|
+
join(__dirname, "public", filename),
|
|
13
|
+
join(__dirname, "..", "web", "public", filename),
|
|
14
|
+
join(__dirname, "..", "..", "src", "web", "public", filename),
|
|
15
|
+
];
|
|
16
|
+
for (const p of candidates) {
|
|
17
|
+
if (existsSync(p))
|
|
18
|
+
return p;
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
const MIME = {
|
|
23
|
+
".svg": "image/svg+xml",
|
|
24
|
+
".png": "image/png",
|
|
25
|
+
".ico": "image/x-icon",
|
|
26
|
+
".json": "application/json",
|
|
27
|
+
};
|
|
28
|
+
const app = new Hono();
|
|
29
|
+
app.use("/*", cors());
|
|
30
|
+
// Mount API
|
|
31
|
+
app.route("/api", api);
|
|
32
|
+
// Serve static files (favicon, logo, etc.)
|
|
33
|
+
app.get("/:file{.+\\.(svg|png|ico|json)$}", (c) => {
|
|
34
|
+
const file = c.req.param("file");
|
|
35
|
+
const filePath = findPublicFile(file);
|
|
36
|
+
if (!filePath)
|
|
37
|
+
return c.notFound();
|
|
38
|
+
const ext = file.substring(file.lastIndexOf("."));
|
|
39
|
+
const content = readFileSync(filePath);
|
|
40
|
+
return c.body(content, { headers: { "Content-Type": MIME[ext] || "application/octet-stream" } });
|
|
41
|
+
});
|
|
42
|
+
// Serve the GUI
|
|
43
|
+
app.get("/", (c) => {
|
|
44
|
+
const htmlPath = findPublicFile("index.html");
|
|
45
|
+
if (!htmlPath) {
|
|
46
|
+
return c.text("GUI not found. Make sure index.html is in the web/public directory.", 500);
|
|
47
|
+
}
|
|
48
|
+
const html = readFileSync(htmlPath, "utf-8");
|
|
49
|
+
return c.html(html);
|
|
50
|
+
});
|
|
51
|
+
const MAX_PORT_ATTEMPTS = 10;
|
|
52
|
+
export function startWebServer(port, attempt = 0) {
|
|
53
|
+
try {
|
|
54
|
+
const server = serve({ fetch: app.fetch, port }, () => {
|
|
55
|
+
process.stderr.write(`postino GUI: http://localhost:${port}\n`);
|
|
56
|
+
});
|
|
57
|
+
server.on("error", (err) => {
|
|
58
|
+
if (err.code === "EADDRINUSE" && attempt < MAX_PORT_ATTEMPTS) {
|
|
59
|
+
process.stderr.write(`postino: port ${port} in use, trying ${port + 1}...\n`);
|
|
60
|
+
startWebServer(port + 1, attempt + 1);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
process.stderr.write(`postino GUI failed to start: ${err.message}\n`);
|
|
64
|
+
process.stderr.write(`MCP tools are still available, but the web GUI is not.\n`);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
process.stderr.write(`postino GUI failed to start: ${err}\n`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Postino: check for unread messages via HTTP API (zero MCP tokens)
|
|
3
|
+
# Outputs nothing if no new activity (no token cost)
|
|
4
|
+
|
|
5
|
+
PORT="${POSTINO_WEB_PORT:-3333}"
|
|
6
|
+
|
|
7
|
+
# Resolve agent name (same logic as src/types.ts)
|
|
8
|
+
if [ -n "$POSTINO_AGENT_NAME" ]; then
|
|
9
|
+
AGENT="$POSTINO_AGENT_NAME"
|
|
10
|
+
elif [ -n "$TERM_SESSION_ID" ]; then
|
|
11
|
+
AGENT="agent-$(echo "$TERM_SESSION_ID" | rev | cut -d: -f1 | rev | cut -c1-8)"
|
|
12
|
+
elif [ -n "$ITERM_SESSION_ID" ]; then
|
|
13
|
+
AGENT="agent-$(echo "$ITERM_SESSION_ID" | rev | cut -d: -f1 | rev | cut -c1-8)"
|
|
14
|
+
else
|
|
15
|
+
# Can't determine agent, fall back to generic stats
|
|
16
|
+
exit 0
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
result=$(curl -s --connect-timeout 1 --max-time 2 "http://127.0.0.1:${PORT}/api/check/${AGENT}" 2>/dev/null) || exit 0
|
|
20
|
+
|
|
21
|
+
msg=$(echo "$result" | grep -o '"unreadMessages":[0-9]*' | cut -d: -f2)
|
|
22
|
+
bc=$(echo "$result" | grep -o '"unseenBroadcasts":[0-9]*' | cut -d: -f2)
|
|
23
|
+
|
|
24
|
+
# Only output if there's something new
|
|
25
|
+
if [ "${msg:-0}" -gt 0 ] || [ "${bc:-0}" -gt 0 ]; then
|
|
26
|
+
parts=""
|
|
27
|
+
[ "${msg:-0}" -gt 0 ] && parts="${msg} unread message(s)"
|
|
28
|
+
[ "${bc:-0}" -gt 0 ] && { [ -n "$parts" ] && parts="$parts, "; parts="${parts}${bc} new broadcast(s)"; }
|
|
29
|
+
echo "[postino] ${parts}. Use msg_whoami to check details."
|
|
30
|
+
fi
|
package/hooks/hooks.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"description": "Check for postino messages on each user prompt",
|
|
3
|
+
"hooks": {
|
|
4
|
+
"UserPromptSubmit": [
|
|
5
|
+
{
|
|
6
|
+
"matcher": ".*",
|
|
7
|
+
"hooks": [
|
|
8
|
+
{
|
|
9
|
+
"type": "command",
|
|
10
|
+
"command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/check-messages.sh",
|
|
11
|
+
"timeout": 3
|
|
12
|
+
}
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
}
|