agent-office 0.3.1 → 0.4.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/README.md +18 -40
- package/dist/cli.js +23 -64
- package/dist/commands/communicator.d.ts +2 -2
- package/dist/commands/communicator.js +175 -40
- package/dist/commands/notifier.d.ts +11 -0
- package/dist/commands/notifier.js +100 -0
- package/dist/commands/screensaver.d.ts +1 -1
- package/dist/commands/screensaver.js +47 -3
- package/dist/commands/serve.d.ts +0 -2
- package/dist/commands/serve.js +1 -26
- package/dist/commands/worker.d.ts +1 -4
- package/dist/commands/worker.js +2 -64
- package/dist/db/index.d.ts +1 -0
- package/dist/db/postgresql-storage.d.ts +7 -1
- package/dist/db/postgresql-storage.js +37 -14
- package/dist/db/sqlite-storage.d.ts +7 -1
- package/dist/db/sqlite-storage.js +51 -12
- package/dist/db/storage-base.d.ts +7 -1
- package/dist/db/storage-base.js +1 -1
- package/dist/db/storage.d.ts +7 -1
- package/dist/lib/notifier.d.ts +18 -0
- package/dist/lib/notifier.js +15 -0
- package/dist/manage/components/SessionList.js +0 -266
- package/dist/manage/hooks/useApi.d.ts +0 -24
- package/dist/manage/hooks/useApi.js +0 -24
- package/dist/server/index.d.ts +1 -2
- package/dist/server/index.js +3 -3
- package/dist/server/routes.d.ts +2 -3
- package/dist/server/routes.js +72 -252
- package/package.json +4 -4
- package/dist/server/memory.d.ts +0 -87
- package/dist/server/memory.js +0 -348
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import express from "express";
|
|
2
2
|
// ── API helpers ───────────────────────────────────────────────────────────────
|
|
3
|
-
async function apiFetch(agentUrl,
|
|
3
|
+
async function apiFetch(agentUrl, password, path, init = {}) {
|
|
4
4
|
const res = await fetch(`${agentUrl}${path}`, {
|
|
5
5
|
...init,
|
|
6
6
|
headers: {
|
|
7
7
|
"Content-Type": "application/json",
|
|
8
|
-
"Authorization": `Bearer ${
|
|
8
|
+
"Authorization": `Bearer ${password}`,
|
|
9
9
|
...(init.headers ?? {}),
|
|
10
10
|
},
|
|
11
11
|
});
|
|
@@ -15,19 +15,22 @@ async function apiFetch(agentUrl, secret, path, init = {}) {
|
|
|
15
15
|
}
|
|
16
16
|
return res.json();
|
|
17
17
|
}
|
|
18
|
-
async function getHumanName(agentUrl,
|
|
19
|
-
const cfg = await apiFetch(agentUrl,
|
|
18
|
+
async function getHumanName(agentUrl, password) {
|
|
19
|
+
const cfg = await apiFetch(agentUrl, password, "/config");
|
|
20
20
|
return cfg.human_name ?? "Human";
|
|
21
21
|
}
|
|
22
|
-
async function
|
|
23
|
-
|
|
22
|
+
async function fetchSessions(agentUrl, password) {
|
|
23
|
+
return apiFetch(agentUrl, password, "/sessions");
|
|
24
|
+
}
|
|
25
|
+
async function fetchCoworkerStatus(agentUrl, password, coworker) {
|
|
26
|
+
const sessions = await fetchSessions(agentUrl, password);
|
|
24
27
|
const session = sessions.find((s) => s.name === coworker);
|
|
25
28
|
return session?.status ?? null;
|
|
26
29
|
}
|
|
27
|
-
async function fetchMessages(agentUrl,
|
|
30
|
+
async function fetchMessages(agentUrl, password, humanName, coworker) {
|
|
28
31
|
const [sent, received] = await Promise.all([
|
|
29
|
-
apiFetch(agentUrl,
|
|
30
|
-
apiFetch(agentUrl,
|
|
32
|
+
apiFetch(agentUrl, password, `/messages/${encodeURIComponent(humanName)}?sent=true`),
|
|
33
|
+
apiFetch(agentUrl, password, `/messages/${encodeURIComponent(humanName)}`),
|
|
31
34
|
]);
|
|
32
35
|
// sent: from humanName → coworker
|
|
33
36
|
const sentToCoworker = sent.filter((m) => m.to_name === coworker);
|
|
@@ -37,8 +40,8 @@ async function fetchMessages(agentUrl, secret, humanName, coworker) {
|
|
|
37
40
|
all.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
|
|
38
41
|
return all;
|
|
39
42
|
}
|
|
40
|
-
async function markRead(agentUrl,
|
|
41
|
-
await apiFetch(agentUrl,
|
|
43
|
+
async function markRead(agentUrl, password, id) {
|
|
44
|
+
await apiFetch(agentUrl, password, `/messages/${id}/read`, { method: "POST" });
|
|
42
45
|
}
|
|
43
46
|
// ── HTML helpers ──────────────────────────────────────────────────────────────
|
|
44
47
|
function escapeHtml(str) {
|
|
@@ -83,14 +86,31 @@ function renderMessages(msgs, humanName) {
|
|
|
83
86
|
return `<div id="messages-inner" data-last-id="${lastId}">${inner}</div>`;
|
|
84
87
|
}
|
|
85
88
|
// ── Full page ─────────────────────────────────────────────────────────────────
|
|
86
|
-
function
|
|
89
|
+
function renderDropdown(coworker, coworkers) {
|
|
90
|
+
const selected = coworker ?? "";
|
|
91
|
+
const dropdownOptions = coworkers.filter(c => !c.isHuman);
|
|
92
|
+
const totalUnread = dropdownOptions.reduce((sum, c) => sum + (c.unreadMessages ?? 0), 0);
|
|
93
|
+
const unreadDot = totalUnread > 0 ? '<span class="unread-badge"></span>' : '';
|
|
94
|
+
const coworkerOptions = dropdownOptions.map(c => {
|
|
95
|
+
const unreadBadge = c.unreadMessages && c.unreadMessages > 0 ? ` (${c.unreadMessages})` : '';
|
|
96
|
+
return `<option value="${escapeHtml(c.name)}" ${c.name === selected ? 'selected' : ''}>${escapeHtml(c.name)}${unreadBadge}</option>`;
|
|
97
|
+
}).join('');
|
|
98
|
+
return `<select id="coworker-select" class="coworker-select" onchange="switchCoworker(this.value)">
|
|
99
|
+
<option value="">Select coworker…</option>
|
|
100
|
+
${coworkerOptions}
|
|
101
|
+
</select>
|
|
102
|
+
${unreadDot}`;
|
|
103
|
+
}
|
|
104
|
+
function renderPage(coworker, coworkers, msgs, humanName) {
|
|
87
105
|
const msgsHtml = renderMessages(msgs, humanName);
|
|
106
|
+
const selected = coworker ?? "";
|
|
107
|
+
const dropdownHtml = renderDropdown(coworker, coworkers);
|
|
88
108
|
return `<!DOCTYPE html>
|
|
89
109
|
<html lang="en">
|
|
90
110
|
<head>
|
|
91
111
|
<meta charset="UTF-8">
|
|
92
112
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
|
93
|
-
<title>${escapeHtml(
|
|
113
|
+
<title>${selected ? escapeHtml(selected) + ' — ' : ''}agent-office</title>
|
|
94
114
|
<script src="https://unpkg.com/htmx.org@2.0.4/dist/htmx.min.js"></script>
|
|
95
115
|
<script src="https://cdn.jsdelivr.net/npm/marked@11.1.1/marked.min.js"></script>
|
|
96
116
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/github-markdown-css@5.5.1/github-markdown-light.min.css" media="(prefers-color-scheme: light)">
|
|
@@ -162,6 +182,29 @@ function renderPage(coworker, msgs, humanName) {
|
|
|
162
182
|
}
|
|
163
183
|
|
|
164
184
|
.header-info { flex: 1; min-width: 0; }
|
|
185
|
+
.select-wrapper { display: inline-flex; align-items: center; position: relative; }
|
|
186
|
+
.coworker-select {
|
|
187
|
+
font-weight: 600;
|
|
188
|
+
font-size: 15px;
|
|
189
|
+
background: transparent;
|
|
190
|
+
border: none;
|
|
191
|
+
color: var(--text);
|
|
192
|
+
cursor: pointer;
|
|
193
|
+
padding: 0;
|
|
194
|
+
margin: 0;
|
|
195
|
+
outline: none;
|
|
196
|
+
max-width: 180px;
|
|
197
|
+
}
|
|
198
|
+
.coworker-select:focus { color: var(--accent); }
|
|
199
|
+
.coworker-select option { background: var(--surface); color: var(--text); }
|
|
200
|
+
.unread-badge {
|
|
201
|
+
width: 8px;
|
|
202
|
+
height: 8px;
|
|
203
|
+
background: #ff4444;
|
|
204
|
+
border-radius: 50%;
|
|
205
|
+
margin-left: 6px;
|
|
206
|
+
flex-shrink: 0;
|
|
207
|
+
}
|
|
165
208
|
.header-name {
|
|
166
209
|
font-weight: 600;
|
|
167
210
|
font-size: 15px;
|
|
@@ -418,22 +461,28 @@ function renderPage(coworker, msgs, humanName) {
|
|
|
418
461
|
|
|
419
462
|
<!-- Header -->
|
|
420
463
|
<div class="header">
|
|
421
|
-
<div class="avatar">${escapeHtml(
|
|
464
|
+
<div class="avatar">${selected ? escapeHtml(selected.charAt(0).toUpperCase()) : '?'}</div>
|
|
422
465
|
<div class="header-info">
|
|
423
|
-
<div class="
|
|
466
|
+
<div class="select-wrapper" id="dropdown-wrapper"
|
|
467
|
+
hx-get="/dropdown?coworker=${encodeURIComponent(selected)}"
|
|
468
|
+
hx-trigger="load, every 5s"
|
|
469
|
+
hx-swap="innerHTML">
|
|
470
|
+
${dropdownHtml}
|
|
471
|
+
</div>
|
|
424
472
|
<div class="header-sub"
|
|
425
473
|
id="coworker-status"
|
|
426
|
-
hx-get="/status"
|
|
474
|
+
hx-get="/status?coworker=${encodeURIComponent(selected)}"
|
|
427
475
|
hx-trigger="load, every 5s"
|
|
428
476
|
hx-swap="innerHTML"></div>
|
|
429
477
|
</div>
|
|
430
478
|
<button class="reset-btn"
|
|
431
|
-
hx-post="/reset"
|
|
479
|
+
hx-post="/reset?coworker=${encodeURIComponent(selected)}"
|
|
432
480
|
hx-target="#reset-status"
|
|
433
481
|
hx-swap="innerHTML"
|
|
434
|
-
hx-confirm="Reset
|
|
482
|
+
hx-confirm="Reset session? This will revert them to their first message and re-inject the enrollment prompt."
|
|
435
483
|
hx-on::after-request="showResetStatus()"
|
|
436
|
-
title="Reset session"
|
|
484
|
+
title="Reset session"
|
|
485
|
+
${!selected ? 'disabled' : ''}>
|
|
437
486
|
↺ Reset
|
|
438
487
|
</button>
|
|
439
488
|
<div class="refresh-indicator" id="refresh-dot"
|
|
@@ -449,7 +498,7 @@ function renderPage(coworker, msgs, humanName) {
|
|
|
449
498
|
<!-- Messages -->
|
|
450
499
|
<div class="messages-outer" id="messages-outer">
|
|
451
500
|
<div id="messages"
|
|
452
|
-
hx-get="/messages"
|
|
501
|
+
hx-get="/messages?coworker=${encodeURIComponent(selected)}"
|
|
453
502
|
hx-trigger="load, every 5s"
|
|
454
503
|
hx-swap="innerHTML">
|
|
455
504
|
${msgsHtml}
|
|
@@ -466,11 +515,12 @@ function renderPage(coworker, msgs, humanName) {
|
|
|
466
515
|
hx-on::after-request="handleSent(event)"
|
|
467
516
|
hx-on::before-request="this.querySelector('.send-btn').classList.add('sending')"
|
|
468
517
|
hx-encoding="application/x-www-form-urlencoded">
|
|
518
|
+
<input type="hidden" name="coworker" value="${escapeHtml(selected)}">
|
|
469
519
|
<textarea
|
|
470
520
|
class="input-textarea"
|
|
471
521
|
name="body"
|
|
472
522
|
id="msg-input"
|
|
473
|
-
placeholder="Message
|
|
523
|
+
placeholder="${selected ? 'Message ' + escapeHtml(selected) + '…' : 'Select a coworker to message…'}"
|
|
474
524
|
rows="1"
|
|
475
525
|
autocomplete="off"
|
|
476
526
|
autocorrect="on"
|
|
@@ -554,6 +604,14 @@ function renderPage(coworker, msgs, humanName) {
|
|
|
554
604
|
// Initial scroll
|
|
555
605
|
scrollToBottom()
|
|
556
606
|
|
|
607
|
+
// Switch to a different coworker
|
|
608
|
+
function switchCoworker(name) {
|
|
609
|
+
if (!name) return
|
|
610
|
+
const url = new URL(window.location.href)
|
|
611
|
+
url.searchParams.set('coworker', name)
|
|
612
|
+
window.location.href = url.toString()
|
|
613
|
+
}
|
|
614
|
+
|
|
557
615
|
// Flash the reset status toast then fade it out
|
|
558
616
|
function showResetStatus() {
|
|
559
617
|
const el = document.getElementById('reset-status')
|
|
@@ -569,8 +627,9 @@ function renderPage(coworker, msgs, humanName) {
|
|
|
569
627
|
document.querySelectorAll('.markdown-body[data-markdown-b64]').forEach(el => {
|
|
570
628
|
const b64 = el.getAttribute('data-markdown-b64')
|
|
571
629
|
if (b64 && !el.hasAttribute('data-rendered')) {
|
|
572
|
-
// Decode base64 to get original text with preserved newlines
|
|
573
|
-
const
|
|
630
|
+
// Decode base64 to get original text with preserved newlines and UTF-8 chars
|
|
631
|
+
const binary = atob(b64)
|
|
632
|
+
const text = new TextDecoder().decode(Uint8Array.from(binary, c => c.charCodeAt(0)))
|
|
574
633
|
el.innerHTML = marked.parse(text)
|
|
575
634
|
el.setAttribute('data-rendered', 'true')
|
|
576
635
|
}
|
|
@@ -589,8 +648,8 @@ function renderPage(coworker, msgs, humanName) {
|
|
|
589
648
|
</html>`;
|
|
590
649
|
}
|
|
591
650
|
// ── Express app ───────────────────────────────────────────────────────────────
|
|
592
|
-
export async function appCoworkerChatWeb(
|
|
593
|
-
const { url: agentUrl,
|
|
651
|
+
export async function appCoworkerChatWeb(options) {
|
|
652
|
+
const { url: agentUrl, password, host, port: portStr } = options;
|
|
594
653
|
const port = parseInt(portStr, 10);
|
|
595
654
|
if (isNaN(port) || port < 1 || port > 65535) {
|
|
596
655
|
console.error(`Error: invalid port "${portStr}"`);
|
|
@@ -606,22 +665,76 @@ export async function appCoworkerChatWeb(coworker, options) {
|
|
|
606
665
|
// Resolve human name once at startup
|
|
607
666
|
let humanName = "Human";
|
|
608
667
|
try {
|
|
609
|
-
humanName = await getHumanName(agentUrl,
|
|
668
|
+
humanName = await getHumanName(agentUrl, password);
|
|
610
669
|
}
|
|
611
670
|
catch (err) {
|
|
612
671
|
console.error(`Warning: could not fetch human name from ${agentUrl}: ${err instanceof Error ? err.message : String(err)}`);
|
|
613
|
-
console.error("Check that agent-office serve is running and --
|
|
672
|
+
console.error("Check that agent-office serve is running and --password is correct.");
|
|
614
673
|
}
|
|
615
|
-
console.log(`Communicator: chatting as "${humanName}"
|
|
674
|
+
console.log(`Communicator: chatting as "${humanName}"`);
|
|
616
675
|
const app = express();
|
|
617
676
|
app.use(express.urlencoded({ extended: false }));
|
|
618
677
|
app.use(express.json());
|
|
678
|
+
// ── GET /coworkers — list of coworkers (HTMX) ───────────────────────────────
|
|
679
|
+
app.get("/coworkers", async (_req, res) => {
|
|
680
|
+
res.setHeader("Content-Type", "application/json");
|
|
681
|
+
try {
|
|
682
|
+
const response = await fetch(`${agentUrl}/coworkers`, {
|
|
683
|
+
headers: { "Authorization": `Bearer ${password}` },
|
|
684
|
+
});
|
|
685
|
+
if (!response.ok) {
|
|
686
|
+
res.json([{ name: humanName, status: null, isHuman: true, unreadMessages: 0 }]);
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
const coworkers = await response.json();
|
|
690
|
+
res.json(coworkers);
|
|
691
|
+
}
|
|
692
|
+
catch {
|
|
693
|
+
res.json([{ name: humanName, status: null, isHuman: true, unreadMessages: 0 }]);
|
|
694
|
+
}
|
|
695
|
+
});
|
|
696
|
+
// ── GET /dropdown — dropdown fragment for HTMX polling ────────────────────
|
|
697
|
+
app.get("/dropdown", async (req, res) => {
|
|
698
|
+
const coworker = req.query.coworker;
|
|
699
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
700
|
+
try {
|
|
701
|
+
const response = await fetch(`${agentUrl}/coworkers`, {
|
|
702
|
+
headers: { "Authorization": `Bearer ${password}` },
|
|
703
|
+
});
|
|
704
|
+
if (!response.ok) {
|
|
705
|
+
res.send(renderDropdown(coworker ?? null, [{ name: humanName, status: null, isHuman: true, unreadMessages: 0 }]));
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
const coworkers = await response.json();
|
|
709
|
+
res.send(renderDropdown(coworker ?? null, coworkers));
|
|
710
|
+
}
|
|
711
|
+
catch {
|
|
712
|
+
res.send(renderDropdown(coworker ?? null, [{ name: humanName, status: null, isHuman: true, unreadMessages: 0 }]));
|
|
713
|
+
}
|
|
714
|
+
});
|
|
619
715
|
// ── GET / — full page ────────────────────────────────────────────────────
|
|
620
|
-
app.get("/", async (
|
|
716
|
+
app.get("/", async (req, res) => {
|
|
621
717
|
try {
|
|
622
|
-
|
|
718
|
+
// Fetch coworkers with unread counts from main server
|
|
719
|
+
const response = await fetch(`${agentUrl}/coworkers`, {
|
|
720
|
+
headers: { "Authorization": `Bearer ${password}` },
|
|
721
|
+
});
|
|
722
|
+
let coworkers;
|
|
723
|
+
if (response.ok) {
|
|
724
|
+
coworkers = await response.json();
|
|
725
|
+
}
|
|
726
|
+
else {
|
|
727
|
+
coworkers = [{ name: humanName, status: null, isHuman: true, unreadMessages: 0 }];
|
|
728
|
+
}
|
|
729
|
+
const nonHumans = coworkers.filter(c => !c.isHuman);
|
|
730
|
+
let coworker = req.query.coworker;
|
|
731
|
+
// Default to first non-human coworker if none specified
|
|
732
|
+
if (!coworker && nonHumans.length > 0) {
|
|
733
|
+
coworker = nonHumans[0].name;
|
|
734
|
+
}
|
|
735
|
+
const msgs = coworker ? await fetchMessages(agentUrl, password, humanName, coworker) : [];
|
|
623
736
|
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
624
|
-
res.send(renderPage(coworker, msgs, humanName));
|
|
737
|
+
res.send(renderPage(coworker ?? null, coworkers, msgs, humanName));
|
|
625
738
|
}
|
|
626
739
|
catch (err) {
|
|
627
740
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -629,12 +742,18 @@ export async function appCoworkerChatWeb(coworker, options) {
|
|
|
629
742
|
}
|
|
630
743
|
});
|
|
631
744
|
// ── GET /messages — HTMX fragment (polled every 5s) ──────────────────────
|
|
632
|
-
app.get("/messages", async (
|
|
745
|
+
app.get("/messages", async (req, res) => {
|
|
746
|
+
const coworker = req.query.coworker;
|
|
747
|
+
if (!coworker) {
|
|
748
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
749
|
+
res.send(`<div class="empty-state">Select a coworker to view messages.</div>`);
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
633
752
|
try {
|
|
634
|
-
const msgs = await fetchMessages(agentUrl,
|
|
753
|
+
const msgs = await fetchMessages(agentUrl, password, humanName, coworker);
|
|
635
754
|
// Mark any unread received messages as read
|
|
636
755
|
const unread = msgs.filter((m) => m.from_name === coworker && !m.read);
|
|
637
|
-
await Promise.allSettled(unread.map((m) => markRead(agentUrl,
|
|
756
|
+
await Promise.allSettled(unread.map((m) => markRead(agentUrl, password, m.id)));
|
|
638
757
|
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
639
758
|
res.send(renderMessages(msgs, humanName));
|
|
640
759
|
}
|
|
@@ -646,14 +765,20 @@ export async function appCoworkerChatWeb(coworker, options) {
|
|
|
646
765
|
});
|
|
647
766
|
// ── POST /send — HTMX form submit ────────────────────────────────────────
|
|
648
767
|
app.post("/send", async (req, res) => {
|
|
649
|
-
const body = req.body
|
|
768
|
+
const { body: msgBody, coworker } = req.body;
|
|
769
|
+
const body = msgBody?.trim();
|
|
650
770
|
if (!body) {
|
|
651
771
|
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
652
772
|
res.send(`<span class="send-err">Message cannot be empty.</span>`);
|
|
653
773
|
return;
|
|
654
774
|
}
|
|
775
|
+
if (!coworker) {
|
|
776
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
777
|
+
res.send(`<span class="send-err">No coworker selected.</span>`);
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
655
780
|
try {
|
|
656
|
-
await apiFetch(agentUrl,
|
|
781
|
+
await apiFetch(agentUrl, password, "/messages", {
|
|
657
782
|
method: "POST",
|
|
658
783
|
body: JSON.stringify({ from: humanName, to: [coworker], body }),
|
|
659
784
|
});
|
|
@@ -667,10 +792,15 @@ export async function appCoworkerChatWeb(coworker, options) {
|
|
|
667
792
|
}
|
|
668
793
|
});
|
|
669
794
|
// ── GET /status — coworker status fragment (polled every 5s) ────────────
|
|
670
|
-
app.get("/status", async (
|
|
795
|
+
app.get("/status", async (req, res) => {
|
|
796
|
+
const coworker = req.query.coworker;
|
|
671
797
|
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
798
|
+
if (!coworker) {
|
|
799
|
+
res.send(`<span style="color:var(--text-dim)">—</span>`);
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
672
802
|
try {
|
|
673
|
-
const status = await fetchCoworkerStatus(agentUrl,
|
|
803
|
+
const status = await fetchCoworkerStatus(agentUrl, password, coworker);
|
|
674
804
|
res.send(status ? escapeHtml(status) : `<span style="color:var(--text-dim)">—</span>`);
|
|
675
805
|
}
|
|
676
806
|
catch {
|
|
@@ -678,10 +808,15 @@ export async function appCoworkerChatWeb(coworker, options) {
|
|
|
678
808
|
}
|
|
679
809
|
});
|
|
680
810
|
// ── POST /reset — revert the coworker's session to first message ─────────
|
|
681
|
-
app.post("/reset", async (
|
|
811
|
+
app.post("/reset", async (req, res) => {
|
|
812
|
+
const coworker = req.query.coworker;
|
|
682
813
|
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
814
|
+
if (!coworker) {
|
|
815
|
+
res.send(`<span style="color:var(--red)">✗ No coworker selected.</span>`);
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
683
818
|
try {
|
|
684
|
-
await apiFetch(agentUrl,
|
|
819
|
+
await apiFetch(agentUrl, password, `/sessions/${encodeURIComponent(coworker)}/revert-to-start`, {
|
|
685
820
|
method: "POST",
|
|
686
821
|
});
|
|
687
822
|
res.send(`<span style="color:var(--green)">✓ ${escapeHtml(coworker)} reset and restarted.</span>`);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type AgentOfficeNotifier } from "../lib/notifier.js";
|
|
2
|
+
type Options = {
|
|
3
|
+
agentOfficeUrl?: string;
|
|
4
|
+
password?: string;
|
|
5
|
+
toEmail?: string;
|
|
6
|
+
resendApiKey?: string;
|
|
7
|
+
domain?: string;
|
|
8
|
+
waitMinutes?: string;
|
|
9
|
+
};
|
|
10
|
+
export declare function notifier(options: Options, customNotifier?: AgentOfficeNotifier): Promise<void>;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { Cron } from "croner";
|
|
2
|
+
import { ResendNotifier } from "../lib/notifier.js";
|
|
3
|
+
export async function notifier(options, customNotifier) {
|
|
4
|
+
const toEmail = options.toEmail;
|
|
5
|
+
if (!toEmail) {
|
|
6
|
+
console.error("Error: --to-email or TO_EMAIL env required");
|
|
7
|
+
process.exit(1);
|
|
8
|
+
}
|
|
9
|
+
const domain = options.domain;
|
|
10
|
+
if (!domain) {
|
|
11
|
+
console.error("Error: --domain or EMAIL_DOMAIN env required");
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
const agentOfficeUrl = options.agentOfficeUrl ?? "http://127.0.0.1:7654";
|
|
15
|
+
const password = options.password;
|
|
16
|
+
if (!password) {
|
|
17
|
+
console.error("Error: --password or AGENT_OFFICE_PASSWORD env required");
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
const waitMinutes = parseInt(options.waitMinutes ?? "15", 10);
|
|
21
|
+
const waitHours = waitMinutes / 60;
|
|
22
|
+
let notify;
|
|
23
|
+
if (customNotifier) {
|
|
24
|
+
notify = customNotifier;
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
const resendApiKey = options.resendApiKey;
|
|
28
|
+
if (!resendApiKey) {
|
|
29
|
+
console.error("Error: --resend-api-key or RESEND_API_KEY env required");
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
notify = new ResendNotifier(resendApiKey);
|
|
33
|
+
}
|
|
34
|
+
const authHeaders = {
|
|
35
|
+
Authorization: `Bearer ${password}`,
|
|
36
|
+
"Content-Type": "application/json",
|
|
37
|
+
};
|
|
38
|
+
const check = async () => {
|
|
39
|
+
try {
|
|
40
|
+
// Fetch messages old enough to notify about
|
|
41
|
+
const resp = await fetch(`${agentOfficeUrl}/human/unread-old?hours=${waitHours}`, {
|
|
42
|
+
headers: authHeaders,
|
|
43
|
+
});
|
|
44
|
+
if (!resp.ok) {
|
|
45
|
+
console.error(`GET /human/unread-old failed: ${resp.status}`);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const qualifying = (await resp.json());
|
|
49
|
+
if (qualifying.length === 0) {
|
|
50
|
+
// Check if there are any unread messages at all (just not old enough yet)
|
|
51
|
+
const allResp = await fetch(`${agentOfficeUrl}/human/unread-old?hours=0`, {
|
|
52
|
+
headers: authHeaders,
|
|
53
|
+
});
|
|
54
|
+
if (allResp.ok) {
|
|
55
|
+
const allUnread = (await allResp.json());
|
|
56
|
+
if (allUnread.length > 0) {
|
|
57
|
+
console.log(`${allUnread.length} unread message(s) exist but haven't been waiting >${waitMinutes}m yet — skipping notification`);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
console.log("No unread messages — nothing to notify");
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const senders = [...new Set(qualifying.map((m) => m.from_name))];
|
|
66
|
+
for (const sender of senders) {
|
|
67
|
+
const fromAddress = `${sender} <${sender.replace(/\s+/g, "+")}@${domain}>`;
|
|
68
|
+
await notify.send({
|
|
69
|
+
from: fromAddress,
|
|
70
|
+
to: toEmail,
|
|
71
|
+
subject: "Agent Office: Message Waiting",
|
|
72
|
+
text: "There is a message waiting for you at Agent Office.",
|
|
73
|
+
});
|
|
74
|
+
console.log(`Sent out notification for waiting mail | from: ${fromAddress} | to: ${toEmail}`);
|
|
75
|
+
}
|
|
76
|
+
const ids = qualifying.map((m) => m.id);
|
|
77
|
+
const markResp = await fetch(`${agentOfficeUrl}/human/mark-notified`, {
|
|
78
|
+
method: "POST",
|
|
79
|
+
headers: authHeaders,
|
|
80
|
+
body: JSON.stringify({ ids }),
|
|
81
|
+
});
|
|
82
|
+
if (!markResp.ok) {
|
|
83
|
+
console.error(`POST /human/mark-notified failed: ${markResp.status}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch (e) {
|
|
87
|
+
console.error("Notifier cron error:", e);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
const cron = new Cron("0 * * * *", check);
|
|
91
|
+
console.log(`Agent Office notifier started. Notifying for messages unread >${waitMinutes}m. Checking ${agentOfficeUrl} every hour. ^C to stop.`);
|
|
92
|
+
await check();
|
|
93
|
+
const shutdown = async () => {
|
|
94
|
+
console.log("\nShutting down...");
|
|
95
|
+
cron.stop();
|
|
96
|
+
process.exit(0);
|
|
97
|
+
};
|
|
98
|
+
process.on("SIGINT", shutdown);
|
|
99
|
+
process.on("SIGTERM", shutdown);
|
|
100
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import express from "express";
|
|
2
2
|
export async function appScreensaver(options) {
|
|
3
|
-
const { url: agentUrl,
|
|
3
|
+
const { url: agentUrl, password, host, port: portStr } = options;
|
|
4
4
|
const port = parseInt(portStr, 10);
|
|
5
5
|
if (isNaN(port) || port < 1 || port > 65535) {
|
|
6
6
|
console.error(`Error: invalid port "${portStr}"`);
|
|
@@ -16,6 +16,26 @@ export async function appScreensaver(options) {
|
|
|
16
16
|
console.log(`Screensaver: visualizing mail activity from ${agentUrl}/watch`);
|
|
17
17
|
const app = express();
|
|
18
18
|
app.use(express.json());
|
|
19
|
+
// ── GET /coworkers — proxies coworker list from agent-office ───────────────
|
|
20
|
+
app.get("/coworkers", async (_req, res) => {
|
|
21
|
+
res.setHeader("Content-Type", "application/json");
|
|
22
|
+
try {
|
|
23
|
+
const response = await fetch(`${agentUrl}/coworkers`, {
|
|
24
|
+
headers: {
|
|
25
|
+
"Authorization": `Bearer ${password}`,
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
if (!response.ok) {
|
|
29
|
+
res.json([]);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const data = await response.json();
|
|
33
|
+
res.json(data);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
res.json([]);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
19
39
|
// ── GET / — Three.js ring visualization ─────────────────────────────────────
|
|
20
40
|
app.get("/", (_req, res) => {
|
|
21
41
|
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
@@ -40,7 +60,7 @@ export async function appScreensaver(options) {
|
|
|
40
60
|
// Connect to the agent-office /watch endpoint
|
|
41
61
|
const response = await fetch(`${agentUrl}/watch`, {
|
|
42
62
|
headers: {
|
|
43
|
-
"Authorization": `Bearer ${
|
|
63
|
+
"Authorization": `Bearer ${password}`,
|
|
44
64
|
},
|
|
45
65
|
signal: abortController.signal,
|
|
46
66
|
});
|
|
@@ -150,6 +170,7 @@ function renderScreensaverPage() {
|
|
|
150
170
|
let agentNodes = new Map() // name -> { mesh, label, angle }
|
|
151
171
|
let arrowObjects = [] // { from, to, mesh, arrowHead, createdAt, opacity }
|
|
152
172
|
let watchState = {} // latest state from SSE
|
|
173
|
+
let coworkersSet = new Set() // valid coworker names from /coworkers endpoint
|
|
153
174
|
|
|
154
175
|
// Track all messages we've seen: key = "from->to", value = lastSent ISO string
|
|
155
176
|
let messageEdges = new Map() // "from->to" -> { lastSent: Date }
|
|
@@ -1092,11 +1113,15 @@ function renderScreensaverPage() {
|
|
|
1092
1113
|
watchState = state
|
|
1093
1114
|
const nameSet = new Set()
|
|
1094
1115
|
|
|
1095
|
-
// Collect all agent names and message edges
|
|
1116
|
+
// Collect all agent names and message edges (only for valid coworkers)
|
|
1096
1117
|
for (const agentName in state) {
|
|
1118
|
+
// Only include if this is a known coworker
|
|
1119
|
+
if (!coworkersSet.has(agentName)) continue
|
|
1097
1120
|
nameSet.add(agentName)
|
|
1098
1121
|
const senders = state[agentName] || {}
|
|
1099
1122
|
for (const senderName in senders) {
|
|
1123
|
+
// Only include sender if they're also a known coworker
|
|
1124
|
+
if (!coworkersSet.has(senderName)) continue
|
|
1100
1125
|
nameSet.add(senderName)
|
|
1101
1126
|
const edgeKey = senderName + '->' + agentName
|
|
1102
1127
|
const lastSent = new Date(senders[senderName].lastSent)
|
|
@@ -1135,6 +1160,25 @@ function renderScreensaverPage() {
|
|
|
1135
1160
|
updateArrows()
|
|
1136
1161
|
}
|
|
1137
1162
|
|
|
1163
|
+
// ── Fetch coworkers list on startup ──────────────────────────────────────
|
|
1164
|
+
async function fetchCoworkers() {
|
|
1165
|
+
try {
|
|
1166
|
+
const response = await fetch('/coworkers')
|
|
1167
|
+
const coworkers = await response.json()
|
|
1168
|
+
coworkersSet = new Set(coworkers.map(c => c.name))
|
|
1169
|
+
console.log('Loaded coworkers:', Array.from(coworkersSet))
|
|
1170
|
+
// Re-process watch state if we already have data
|
|
1171
|
+
if (Object.keys(watchState).length > 0) {
|
|
1172
|
+
processWatchState(watchState)
|
|
1173
|
+
}
|
|
1174
|
+
} catch (err) {
|
|
1175
|
+
console.error('Failed to fetch coworkers:', err)
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
// Fetch coworkers immediately
|
|
1180
|
+
fetchCoworkers()
|
|
1181
|
+
|
|
1138
1182
|
// ── SSE connection ─────────────────────────────────────────────────────
|
|
1139
1183
|
const es = new EventSource('/watch-stream')
|
|
1140
1184
|
|
package/dist/commands/serve.d.ts
CHANGED
package/dist/commands/serve.js
CHANGED
|
@@ -3,7 +3,6 @@ import { runMigrations } from "../db/migrate.js";
|
|
|
3
3
|
import { OpenCodeCodingServer } from "../lib/opencode-coding-server.js";
|
|
4
4
|
import { createApp } from "../server/index.js";
|
|
5
5
|
import { CronScheduler } from "../server/cron.js";
|
|
6
|
-
import { AgentOfficeFastMemory, AgentOfficeSimpleMemory } from "../server/memory.js";
|
|
7
6
|
export async function serve(options) {
|
|
8
7
|
const password = options.password;
|
|
9
8
|
if (!password) {
|
|
@@ -44,33 +43,10 @@ export async function serve(options) {
|
|
|
44
43
|
const agenticCodingServer = new OpenCodeCodingServer(options.opencodeUrl);
|
|
45
44
|
console.log(`Connecting to OpenCode server at ${options.opencodeUrl}...`);
|
|
46
45
|
const serverUrl = `http://${options.host}:${port}`;
|
|
47
|
-
// Create memory manager
|
|
48
|
-
let memoryManager;
|
|
49
|
-
if (options.simpleMemory) {
|
|
50
|
-
console.log("Using simple SQLite+BM25 memory backend.");
|
|
51
|
-
memoryManager = new AgentOfficeSimpleMemory(options.memoryPath);
|
|
52
|
-
await memoryManager.warmup();
|
|
53
|
-
}
|
|
54
|
-
else {
|
|
55
|
-
memoryManager = new AgentOfficeFastMemory(options.memoryPath);
|
|
56
|
-
console.log("Warming up embedding model...");
|
|
57
|
-
try {
|
|
58
|
-
await memoryManager.warmup();
|
|
59
|
-
console.log("Embedding model ready.");
|
|
60
|
-
}
|
|
61
|
-
catch (err) {
|
|
62
|
-
console.error("Embedding model failed to load:", err);
|
|
63
|
-
console.error("Try deleting the model cache and restarting:");
|
|
64
|
-
console.error(` rm -rf ${options.memoryPath}/.model-cache`);
|
|
65
|
-
memoryManager.closeAll();
|
|
66
|
-
await storage.close();
|
|
67
|
-
process.exit(1);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
46
|
// Create cron scheduler
|
|
71
47
|
const cronScheduler = new CronScheduler();
|
|
72
48
|
// Create Express app
|
|
73
|
-
const app = createApp(storage, agenticCodingServer, password, serverUrl, cronScheduler
|
|
49
|
+
const app = createApp(storage, agenticCodingServer, password, serverUrl, cronScheduler);
|
|
74
50
|
// Start cron scheduler
|
|
75
51
|
await cronScheduler.start(storage, agenticCodingServer, serverUrl);
|
|
76
52
|
// Start server
|
|
@@ -82,7 +58,6 @@ export async function serve(options) {
|
|
|
82
58
|
console.log("\nShutting down...");
|
|
83
59
|
server.close(async () => {
|
|
84
60
|
cronScheduler.stop();
|
|
85
|
-
memoryManager.closeAll();
|
|
86
61
|
await storage.close();
|
|
87
62
|
console.log("Goodbye.");
|
|
88
63
|
process.exit(0);
|
|
@@ -6,13 +6,10 @@ export declare function createCron(token: string, options: {
|
|
|
6
6
|
name: string;
|
|
7
7
|
schedule: string;
|
|
8
8
|
message: string;
|
|
9
|
+
respondTo: string;
|
|
9
10
|
timezone?: string;
|
|
10
11
|
}): Promise<void>;
|
|
11
12
|
export declare function deleteCron(token: string, cronId: number): Promise<void>;
|
|
12
13
|
export declare function enableCron(token: string, cronId: number): Promise<void>;
|
|
13
14
|
export declare function disableCron(token: string, cronId: number): Promise<void>;
|
|
14
15
|
export declare function cronHistory(token: string, cronId: number): Promise<void>;
|
|
15
|
-
export declare function memoryAdd(token: string, content: string): Promise<void>;
|
|
16
|
-
export declare function memorySearch(token: string, query: string, limit: number): Promise<void>;
|
|
17
|
-
export declare function memoryList(token: string, limit: number): Promise<void>;
|
|
18
|
-
export declare function memoryForget(token: string, memoryId: string): Promise<void>;
|