agent-office 0.0.0 → 0.0.1
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/cli.js +18 -0
- package/dist/commands/serve.js +3 -2
- package/dist/commands/worker.d.ts +2 -0
- package/dist/commands/worker.js +73 -5
- package/dist/db/index.d.ts +13 -0
- package/dist/db/migrate.js +30 -0
- package/dist/manage/app.js +23 -2
- package/dist/manage/components/MyMail.d.ts +8 -0
- package/dist/manage/components/MyMail.js +70 -0
- package/dist/manage/components/Profile.d.ts +8 -0
- package/dist/manage/components/Profile.js +60 -0
- package/dist/manage/components/ReadMail.d.ts +8 -0
- package/dist/manage/components/ReadMail.js +110 -0
- package/dist/manage/components/SendMessage.d.ts +8 -0
- package/dist/manage/components/SendMessage.js +76 -0
- package/dist/manage/components/SessionSidebar.d.ts +6 -0
- package/dist/manage/components/SessionSidebar.js +18 -0
- package/dist/manage/hooks/useApi.d.ts +31 -0
- package/dist/manage/hooks/useApi.js +35 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +3 -3
- package/dist/server/routes.d.ts +2 -2
- package/dist/server/routes.js +289 -17
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -38,4 +38,22 @@ workerCmd
|
|
|
38
38
|
const { clockIn } = await import("./commands/worker.js");
|
|
39
39
|
await clockIn(token);
|
|
40
40
|
});
|
|
41
|
+
workerCmd
|
|
42
|
+
.command("list-coworkers")
|
|
43
|
+
.description("List all other workers (coworkers)")
|
|
44
|
+
.argument("<token>", "Agent token in the format <agent_code>@<server-url>")
|
|
45
|
+
.action(async (token) => {
|
|
46
|
+
const { listCoworkers } = await import("./commands/worker.js");
|
|
47
|
+
await listCoworkers(token);
|
|
48
|
+
});
|
|
49
|
+
workerCmd
|
|
50
|
+
.command("send-message")
|
|
51
|
+
.description("Send a message to one or more coworkers")
|
|
52
|
+
.argument("<token>", "Agent token in the format <agent_code>@<server-url>")
|
|
53
|
+
.requiredOption("--name <name>", "Recipient name (can be specified multiple times)", (val, prev) => [...prev, val], [])
|
|
54
|
+
.requiredOption("--body <body>", "Message body")
|
|
55
|
+
.action(async (token, options) => {
|
|
56
|
+
const { sendMessage } = await import("./commands/worker.js");
|
|
57
|
+
await sendMessage(token, options.name, options.body);
|
|
58
|
+
});
|
|
41
59
|
program.parse();
|
package/dist/commands/serve.js
CHANGED
|
@@ -34,11 +34,12 @@ export async function serve(options) {
|
|
|
34
34
|
}
|
|
35
35
|
// Init OpenCode client
|
|
36
36
|
const opencode = createOpencodeClient(options.opencodeUrl);
|
|
37
|
+
const serverUrl = `http://${options.host}:${port}`;
|
|
37
38
|
// Create Express app
|
|
38
|
-
const app = createApp(sql, opencode, password);
|
|
39
|
+
const app = createApp(sql, opencode, password, serverUrl);
|
|
39
40
|
// Start server
|
|
40
41
|
const server = app.listen(port, options.host, () => {
|
|
41
|
-
console.log(`agent-office server listening on
|
|
42
|
+
console.log(`agent-office server listening on ${serverUrl}`);
|
|
42
43
|
});
|
|
43
44
|
// Graceful shutdown
|
|
44
45
|
const shutdown = async () => {
|
package/dist/commands/worker.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
// UUID v4 pattern
|
|
2
1
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
3
|
-
|
|
4
|
-
// Parse <agent_code>@<url> — URL may contain @ itself (unlikely but safe with lastIndexOf)
|
|
2
|
+
function parseToken(token) {
|
|
5
3
|
const atIndex = token.indexOf("@");
|
|
6
4
|
if (atIndex === -1) {
|
|
7
5
|
console.error("Error: token must be in the format <agent_code>@<server-url>");
|
|
@@ -14,14 +12,76 @@ export async function clockIn(token) {
|
|
|
14
12
|
console.error(`Error: "${agentCode}" is not a valid UUID agent code`);
|
|
15
13
|
process.exit(1);
|
|
16
14
|
}
|
|
17
|
-
let parsedUrl;
|
|
18
15
|
try {
|
|
19
|
-
|
|
16
|
+
new URL(serverUrl);
|
|
20
17
|
}
|
|
21
18
|
catch {
|
|
22
19
|
console.error(`Error: "${serverUrl}" is not a valid URL`);
|
|
23
20
|
process.exit(1);
|
|
24
21
|
}
|
|
22
|
+
return { agentCode, serverUrl };
|
|
23
|
+
}
|
|
24
|
+
async function fetchWorker(token, endpoint) {
|
|
25
|
+
const { agentCode, serverUrl } = parseToken(token);
|
|
26
|
+
const url = `${serverUrl}${endpoint}?code=${encodeURIComponent(agentCode)}`;
|
|
27
|
+
let res;
|
|
28
|
+
try {
|
|
29
|
+
res = await fetch(url);
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
console.error(`Error: could not reach ${serverUrl}`);
|
|
33
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
let body;
|
|
37
|
+
try {
|
|
38
|
+
body = await res.json();
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
console.error(`Error: invalid response from server`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
if (!res.ok) {
|
|
45
|
+
const msg = body.error ?? `HTTP ${res.status}`;
|
|
46
|
+
console.error(`Error: ${msg}`);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
return body;
|
|
50
|
+
}
|
|
51
|
+
async function postWorker(token, endpoint, payload) {
|
|
52
|
+
const { agentCode, serverUrl } = parseToken(token);
|
|
53
|
+
const url = `${serverUrl}${endpoint}?code=${encodeURIComponent(agentCode)}`;
|
|
54
|
+
let res;
|
|
55
|
+
try {
|
|
56
|
+
res = await fetch(url, {
|
|
57
|
+
method: "POST",
|
|
58
|
+
headers: { "Content-Type": "application/json" },
|
|
59
|
+
body: JSON.stringify(payload),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
console.error(`Error: could not reach ${serverUrl}`);
|
|
64
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
let body;
|
|
68
|
+
try {
|
|
69
|
+
body = await res.json();
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
console.error(`Error: invalid response from server`);
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
if (!res.ok) {
|
|
76
|
+
const msg = body.error ?? `HTTP ${res.status}`;
|
|
77
|
+
console.error(`Error: ${msg}`);
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
return body;
|
|
81
|
+
}
|
|
82
|
+
export async function clockIn(token) {
|
|
83
|
+
const { agentCode, serverUrl } = parseToken(token);
|
|
84
|
+
const parsedUrl = new URL(serverUrl);
|
|
25
85
|
const clockInUrl = `${parsedUrl.origin}/worker/clock-in?code=${encodeURIComponent(agentCode)}`;
|
|
26
86
|
let res;
|
|
27
87
|
try {
|
|
@@ -48,3 +108,11 @@ export async function clockIn(token) {
|
|
|
48
108
|
const { message } = body;
|
|
49
109
|
console.log(message);
|
|
50
110
|
}
|
|
111
|
+
export async function listCoworkers(token) {
|
|
112
|
+
const workers = await fetchWorker(token, "/worker/list-coworkers");
|
|
113
|
+
console.log(JSON.stringify(workers, null, 2));
|
|
114
|
+
}
|
|
115
|
+
export async function sendMessage(token, recipients, body) {
|
|
116
|
+
const result = await postWorker(token, "/worker/send-message", { to: recipients, body });
|
|
117
|
+
console.log(JSON.stringify(result, null, 2));
|
|
118
|
+
}
|
package/dist/db/index.d.ts
CHANGED
|
@@ -8,3 +8,16 @@ export interface SessionRow {
|
|
|
8
8
|
agent_code: string;
|
|
9
9
|
created_at: Date;
|
|
10
10
|
}
|
|
11
|
+
export interface ConfigRow {
|
|
12
|
+
key: string;
|
|
13
|
+
value: string;
|
|
14
|
+
}
|
|
15
|
+
export interface MessageRow {
|
|
16
|
+
id: number;
|
|
17
|
+
from_name: string;
|
|
18
|
+
to_name: string;
|
|
19
|
+
body: string;
|
|
20
|
+
read: boolean;
|
|
21
|
+
injected: boolean;
|
|
22
|
+
created_at: Date;
|
|
23
|
+
}
|
package/dist/db/migrate.js
CHANGED
|
@@ -20,6 +20,36 @@ const MIGRATIONS = [
|
|
|
20
20
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_sessions_agent_code ON sessions(agent_code);
|
|
21
21
|
`,
|
|
22
22
|
},
|
|
23
|
+
{
|
|
24
|
+
version: 3,
|
|
25
|
+
name: "create_config_table",
|
|
26
|
+
sql: `
|
|
27
|
+
CREATE TABLE IF NOT EXISTS config (
|
|
28
|
+
key VARCHAR(255) PRIMARY KEY,
|
|
29
|
+
value TEXT NOT NULL
|
|
30
|
+
);
|
|
31
|
+
INSERT INTO config (key, value) VALUES ('human_name', 'Human') ON CONFLICT DO NOTHING;
|
|
32
|
+
INSERT INTO config (key, value) VALUES ('human_description', '') ON CONFLICT DO NOTHING;
|
|
33
|
+
`,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
version: 4,
|
|
37
|
+
name: "create_messages_table",
|
|
38
|
+
sql: `
|
|
39
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
40
|
+
id SERIAL PRIMARY KEY,
|
|
41
|
+
from_name VARCHAR(255) NOT NULL,
|
|
42
|
+
to_name VARCHAR(255) NOT NULL,
|
|
43
|
+
body TEXT NOT NULL,
|
|
44
|
+
read BOOLEAN NOT NULL DEFAULT FALSE,
|
|
45
|
+
injected BOOLEAN NOT NULL DEFAULT FALSE,
|
|
46
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
47
|
+
);
|
|
48
|
+
CREATE INDEX IF NOT EXISTS idx_messages_to_name ON messages(to_name);
|
|
49
|
+
CREATE INDEX IF NOT EXISTS idx_messages_from_name ON messages(from_name);
|
|
50
|
+
CREATE INDEX IF NOT EXISTS idx_messages_read ON messages(read);
|
|
51
|
+
`,
|
|
52
|
+
},
|
|
23
53
|
];
|
|
24
54
|
export async function runMigrations(sql) {
|
|
25
55
|
await sql `
|
package/dist/manage/app.js
CHANGED
|
@@ -9,16 +9,25 @@ import { DeleteSession } from "./components/DeleteSession.js";
|
|
|
9
9
|
import { TailMessages } from "./components/TailMessages.js";
|
|
10
10
|
import { InjectText } from "./components/InjectText.js";
|
|
11
11
|
import { AgentCode } from "./components/AgentCode.js";
|
|
12
|
+
import { ReadMail } from "./components/ReadMail.js";
|
|
13
|
+
import { SendMessage } from "./components/SendMessage.js";
|
|
14
|
+
import { Profile } from "./components/Profile.js";
|
|
15
|
+
import { MyMail } from "./components/MyMail.js";
|
|
16
|
+
import { SessionSidebar } from "./components/SessionSidebar.js";
|
|
12
17
|
const MENU_OPTIONS = [
|
|
18
|
+
{ label: "Send message", value: "send-message" },
|
|
19
|
+
{ label: "My mail", value: "my-mail" },
|
|
13
20
|
{ label: "List sessions", value: "list" },
|
|
14
21
|
{ label: "Create session", value: "create" },
|
|
15
22
|
{ label: "Delete session", value: "delete" },
|
|
16
23
|
{ label: "Tail messages", value: "tail" },
|
|
17
24
|
{ label: "Inject text", value: "inject" },
|
|
18
25
|
{ label: "Agent code", value: "agent-code" },
|
|
26
|
+
{ label: "Read mail", value: "read-mail" },
|
|
27
|
+
{ label: "My profile", value: "profile" },
|
|
19
28
|
{ label: "Quit", value: "quit" },
|
|
20
29
|
];
|
|
21
|
-
const SUB_SCREENS = ["list", "create", "delete", "tail", "inject", "agent-code"];
|
|
30
|
+
const SUB_SCREENS = ["list", "create", "delete", "tail", "inject", "agent-code", "read-mail", "send-message", "my-mail", "profile"];
|
|
22
31
|
const FOOTER_HINTS = {
|
|
23
32
|
connecting: "",
|
|
24
33
|
"auth-error": "",
|
|
@@ -29,6 +38,10 @@ const FOOTER_HINTS = {
|
|
|
29
38
|
tail: "↑↓ scroll · Esc back to menu",
|
|
30
39
|
inject: "Enter submit · Esc back to menu",
|
|
31
40
|
"agent-code": "r reveal/hide · g regenerate · Esc back to menu",
|
|
41
|
+
"read-mail": "r received · s sent · Esc back to menu",
|
|
42
|
+
"send-message": "Enter submit · Esc back to menu",
|
|
43
|
+
"my-mail": "r received · s sent · Esc back to menu",
|
|
44
|
+
"profile": "Enter submit · Esc back to menu",
|
|
32
45
|
};
|
|
33
46
|
function Header({ serverUrl }) {
|
|
34
47
|
return (_jsxs(Box, { borderStyle: "single", borderColor: "cyan", paddingX: 1, justifyContent: "space-between", children: [_jsx(Text, { bold: true, color: "cyan", children: "agent-office" }), _jsx(Text, { dimColor: true, children: serverUrl })] }));
|
|
@@ -83,7 +96,7 @@ export function App({ serverUrl, password }) {
|
|
|
83
96
|
case "auth-error":
|
|
84
97
|
return (_jsxs(Box, { height: contentHeight, flexDirection: "column", alignItems: "center", justifyContent: "center", gap: 1, children: [_jsx(Text, { color: "red", bold: true, children: "Connection failed" }), _jsxs(Text, { children: ["Could not reach ", _jsx(Text, { color: "cyan", children: serverUrl })] }), _jsx(Text, { dimColor: true, children: "Check that agent-office serve is running and your --password is correct." })] }));
|
|
85
98
|
case "menu":
|
|
86
|
-
return (_jsxs(Box, { height: contentHeight, flexDirection: "column", paddingX: 2, paddingY: 1,
|
|
99
|
+
return (_jsxs(Box, { height: contentHeight, flexDirection: "row", flexGrow: 1, children: [_jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, flexGrow: 1, children: [_jsx(Text, { bold: true, children: "Main Menu" }), _jsx(Select, { options: MENU_OPTIONS, onChange: handleMenuSelect })] }), _jsx(SessionSidebar, { serverUrl: serverUrl, password: password })] }));
|
|
87
100
|
case "list":
|
|
88
101
|
return (_jsx(Box, { height: contentHeight, flexDirection: "column", paddingX: 2, paddingY: 1, children: _jsx(SessionList, { serverUrl: serverUrl, password: password, onBack: goBack, contentHeight: contentHeight - 2 }) }));
|
|
89
102
|
case "create":
|
|
@@ -96,6 +109,14 @@ export function App({ serverUrl, password }) {
|
|
|
96
109
|
return (_jsx(Box, { height: contentHeight, flexDirection: "column", paddingX: 2, paddingY: 1, children: _jsx(InjectText, { serverUrl: serverUrl, password: password, onBack: goBack, contentHeight: contentHeight - 2 }) }));
|
|
97
110
|
case "agent-code":
|
|
98
111
|
return (_jsx(Box, { height: contentHeight, flexDirection: "column", paddingX: 2, paddingY: 1, children: _jsx(AgentCode, { serverUrl: serverUrl, password: password, onBack: goBack, contentHeight: contentHeight - 2 }) }));
|
|
112
|
+
case "read-mail":
|
|
113
|
+
return (_jsx(Box, { height: contentHeight, flexDirection: "column", paddingX: 2, paddingY: 1, children: _jsx(ReadMail, { serverUrl: serverUrl, password: password, onBack: goBack, contentHeight: contentHeight - 2 }) }));
|
|
114
|
+
case "send-message":
|
|
115
|
+
return (_jsx(Box, { height: contentHeight, flexDirection: "column", paddingX: 2, paddingY: 1, children: _jsx(SendMessage, { serverUrl: serverUrl, password: password, onBack: goBack, contentHeight: contentHeight - 2 }) }));
|
|
116
|
+
case "profile":
|
|
117
|
+
return (_jsx(Box, { height: contentHeight, flexDirection: "column", paddingX: 2, paddingY: 1, children: _jsx(Profile, { serverUrl: serverUrl, password: password, onBack: goBack, contentHeight: contentHeight - 2 }) }));
|
|
118
|
+
case "my-mail":
|
|
119
|
+
return (_jsx(Box, { height: contentHeight, flexDirection: "column", paddingX: 2, paddingY: 1, children: _jsx(MyMail, { serverUrl: serverUrl, password: password, onBack: goBack, contentHeight: contentHeight - 2 }) }));
|
|
99
120
|
}
|
|
100
121
|
};
|
|
101
122
|
return (_jsxs(Box, { flexDirection: "column", width: termWidth, children: [_jsx(Header, { serverUrl: serverUrl }), renderContent(), screen !== "connecting" && (_jsx(Footer, { hint: FOOTER_HINTS[screen] }))] }));
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { Box, Text, useInput } from "ink";
|
|
4
|
+
import { useApi } from "../hooks/useApi.js";
|
|
5
|
+
export function MyMail({ serverUrl, password, onBack, contentHeight }) {
|
|
6
|
+
const { getConfig, getMailMessages } = useApi(serverUrl, password);
|
|
7
|
+
const [humanName, setHumanName] = useState("Human");
|
|
8
|
+
const [viewTab, setViewTab] = useState("received");
|
|
9
|
+
const [messages, setMessages] = useState([]);
|
|
10
|
+
const [error, setError] = useState(null);
|
|
11
|
+
const [scrollOffset, setScrollOffset] = useState(0);
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
getConfig().then((config) => {
|
|
14
|
+
setHumanName(config.human_name ?? "Human");
|
|
15
|
+
loadMessages(config.human_name ?? "Human");
|
|
16
|
+
}).catch((err) => {
|
|
17
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
18
|
+
});
|
|
19
|
+
}, []);
|
|
20
|
+
const loadMessages = async (name) => {
|
|
21
|
+
const sent = viewTab === "sent";
|
|
22
|
+
try {
|
|
23
|
+
const msgs = await getMailMessages(name, { sent });
|
|
24
|
+
setMessages(msgs);
|
|
25
|
+
setScrollOffset(0);
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
const handleTabSwitch = (newTab) => {
|
|
32
|
+
setViewTab(newTab);
|
|
33
|
+
setScrollOffset(0);
|
|
34
|
+
loadMessages(humanName);
|
|
35
|
+
};
|
|
36
|
+
useInput((input, key) => {
|
|
37
|
+
if (key.upArrow)
|
|
38
|
+
setScrollOffset((o) => Math.max(0, o - 1));
|
|
39
|
+
if (key.downArrow)
|
|
40
|
+
setScrollOffset((o) => o + 1);
|
|
41
|
+
if (input === "r" && viewTab !== "received") {
|
|
42
|
+
handleTabSwitch("received");
|
|
43
|
+
}
|
|
44
|
+
if (input === "s" && viewTab !== "sent") {
|
|
45
|
+
handleTabSwitch("sent");
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
const renderMessages = () => {
|
|
49
|
+
const lines = [];
|
|
50
|
+
const maxNameLen = Math.max(...messages.map((m) => m.from_name.length), humanName.length);
|
|
51
|
+
for (const msg of messages) {
|
|
52
|
+
const timestamp = new Date(msg.created_at).toLocaleString();
|
|
53
|
+
lines.push({ text: `─`, color: "gray" });
|
|
54
|
+
lines.push({ text: `${msg.from_name.padEnd(maxNameLen)} → ${msg.to_name}`, color: "cyan" });
|
|
55
|
+
lines.push({ text: `${timestamp}`, color: "gray" });
|
|
56
|
+
lines.push({ text: msg.read ? "" : " [unread]" });
|
|
57
|
+
lines.push({ text: "" });
|
|
58
|
+
for (const line of msg.body.split("\n")) {
|
|
59
|
+
lines.push({ text: ` ${line}` });
|
|
60
|
+
}
|
|
61
|
+
lines.push({ text: "" });
|
|
62
|
+
}
|
|
63
|
+
const viewHeight = contentHeight - 8;
|
|
64
|
+
const maxOffset = Math.max(0, lines.length - viewHeight);
|
|
65
|
+
const clampedOffset = Math.min(scrollOffset, maxOffset);
|
|
66
|
+
const visible = lines.slice(clampedOffset, clampedOffset + viewHeight);
|
|
67
|
+
return (_jsx(Box, { flexDirection: "column", height: viewHeight, overflow: "hidden", children: visible.length === 0 ? (_jsx(Text, { dimColor: true, children: "No messages." })) : (visible.map((line, i) => (_jsx(Text, { color: line.color, children: line.text }, i)))) }));
|
|
68
|
+
};
|
|
69
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 0, children: [_jsxs(Box, { gap: 2, marginBottom: 1, children: [_jsx(Text, { bold: true, children: "My Mail" }), _jsx(Text, { color: "cyan", children: humanName }), _jsxs(Text, { dimColor: true, children: ["(", messages.length, " ", viewTab, ")"] })] }), _jsxs(Box, { gap: 2, marginBottom: 1, children: [viewTab === "received" ? (_jsx(Text, { bold: true, color: "green", children: "[Received]" })) : (_jsx(Text, { dimColor: true, children: "Received (press r)" })), viewTab === "sent" ? (_jsx(Text, { bold: true, color: "yellow", children: "[Sent]" })) : (_jsx(Text, { dimColor: true, children: "Sent (press s)" }))] }), renderMessages()] }));
|
|
70
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
interface ProfileProps {
|
|
2
|
+
serverUrl: string;
|
|
3
|
+
password: string;
|
|
4
|
+
onBack: () => void;
|
|
5
|
+
contentHeight: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function Profile({ serverUrl, password, onBack, contentHeight }: ProfileProps): import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { Box, Text } from "ink";
|
|
4
|
+
import { Spinner, TextInput } from "@inkjs/ui";
|
|
5
|
+
import { useApi } from "../hooks/useApi.js";
|
|
6
|
+
export function Profile({ serverUrl, password, onBack, contentHeight }) {
|
|
7
|
+
const { getConfig, setConfig } = useApi(serverUrl, password);
|
|
8
|
+
const [name, setName] = useState("");
|
|
9
|
+
const [description, setDescription] = useState("");
|
|
10
|
+
const [stage, setStage] = useState("loading");
|
|
11
|
+
const [error, setError] = useState(null);
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
getConfig()
|
|
14
|
+
.then((config) => {
|
|
15
|
+
setName(config.human_name ?? "Human");
|
|
16
|
+
setDescription(config.human_description ?? "");
|
|
17
|
+
setStage("editing-name");
|
|
18
|
+
})
|
|
19
|
+
.catch((err) => {
|
|
20
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
21
|
+
setStage("error");
|
|
22
|
+
});
|
|
23
|
+
}, []);
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (stage === "done" || stage === "error") {
|
|
26
|
+
const timer = setTimeout(onBack, 1500);
|
|
27
|
+
return () => clearTimeout(timer);
|
|
28
|
+
}
|
|
29
|
+
}, [stage, onBack]);
|
|
30
|
+
const handleNameSubmit = (newName) => {
|
|
31
|
+
setName(newName);
|
|
32
|
+
setStage("editing-description");
|
|
33
|
+
};
|
|
34
|
+
const handleDescriptionSubmit = async (newDesc) => {
|
|
35
|
+
setDescription(newDesc);
|
|
36
|
+
setStage("saving");
|
|
37
|
+
try {
|
|
38
|
+
await setConfig("human_name", name);
|
|
39
|
+
await setConfig("human_description", newDesc);
|
|
40
|
+
setStage("done");
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
44
|
+
setStage("error");
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
if (stage === "loading") {
|
|
48
|
+
return (_jsx(Box, { height: contentHeight, alignItems: "center", justifyContent: "center", children: _jsx(Spinner, { label: "Loading profile..." }) }));
|
|
49
|
+
}
|
|
50
|
+
if (stage === "error") {
|
|
51
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Text, { bold: true, children: "My Profile" }), _jsxs(Text, { color: "red", children: ["Error: ", error] }), _jsx(Text, { dimColor: true, children: "Returning to menu..." })] }));
|
|
52
|
+
}
|
|
53
|
+
if (stage === "saving") {
|
|
54
|
+
return (_jsx(Box, { height: contentHeight, alignItems: "center", justifyContent: "center", children: _jsx(Spinner, { label: "Saving profile..." }) }));
|
|
55
|
+
}
|
|
56
|
+
if (stage === "done") {
|
|
57
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Text, { bold: true, children: "My Profile" }), _jsx(Text, { color: "green", children: "Profile saved successfully!" }), _jsx(Text, { dimColor: true, children: "Returning to menu..." })] }));
|
|
58
|
+
}
|
|
59
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Text, { bold: true, children: "My Profile" }), _jsx(Text, { dimColor: true, children: "Configure your name and description (visible to agents)" }), _jsxs(Box, { flexDirection: "column", gap: 1, marginTop: 1, children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { bold: true, children: "Name:" }), stage === "editing-name" ? (_jsx(TextInput, { placeholder: name || "Your name", onSubmit: handleNameSubmit })) : (_jsx(Text, { color: "cyan", children: name }))] }), stage === "editing-description" && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { bold: true, children: "Description:" }), _jsx(TextInput, { placeholder: description || "Optional description", onSubmit: handleDescriptionSubmit })] }))] }), stage === "editing-name" && (_jsx(Text, { dimColor: true, children: "Enter to continue \u00B7 Esc to cancel" })), stage === "editing-description" && (_jsx(Text, { dimColor: true, children: "Enter to save \u00B7 Esc to cancel" }))] }));
|
|
60
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
interface ReadMailProps {
|
|
2
|
+
serverUrl: string;
|
|
3
|
+
password: string;
|
|
4
|
+
onBack: () => void;
|
|
5
|
+
contentHeight: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function ReadMail({ serverUrl, password, onBack, contentHeight }: ReadMailProps): import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { Box, Text, useInput } from "ink";
|
|
4
|
+
import { Select, Spinner } from "@inkjs/ui";
|
|
5
|
+
import { useApi, useAsyncState } from "../hooks/useApi.js";
|
|
6
|
+
export function ReadMail({ serverUrl, password, onBack, contentHeight }) {
|
|
7
|
+
const { listSessions, getMailMessages, markMessageRead } = useApi(serverUrl, password);
|
|
8
|
+
const { run: runList } = useAsyncState();
|
|
9
|
+
const [sessions, setSessions] = useState([]);
|
|
10
|
+
const [sessionsLoading, setSessionsLoading] = useState(true);
|
|
11
|
+
const [stage, setStage] = useState("select-session");
|
|
12
|
+
const [selectedName, setSelectedName] = useState(null);
|
|
13
|
+
const [viewTab, setViewTab] = useState("received");
|
|
14
|
+
const [messages, setMessages] = useState([]);
|
|
15
|
+
const [error, setError] = useState(null);
|
|
16
|
+
const [scrollOffset, setScrollOffset] = useState(0);
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
runList(listSessions).then((rows) => {
|
|
19
|
+
setSessions(rows ?? []);
|
|
20
|
+
setSessionsLoading(false);
|
|
21
|
+
});
|
|
22
|
+
}, []);
|
|
23
|
+
const loadMessages = async (name) => {
|
|
24
|
+
setSelectedName(name);
|
|
25
|
+
setStage("loading");
|
|
26
|
+
try {
|
|
27
|
+
const msgs = await getMailMessages(name, { sent: false });
|
|
28
|
+
setMessages(msgs);
|
|
29
|
+
setScrollOffset(0);
|
|
30
|
+
setStage("view-received");
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
34
|
+
setStage("error");
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
const handleTabSwitch = async (newTab) => {
|
|
38
|
+
if (!selectedName)
|
|
39
|
+
return;
|
|
40
|
+
setStage("loading");
|
|
41
|
+
setViewTab(newTab);
|
|
42
|
+
setScrollOffset(0);
|
|
43
|
+
try {
|
|
44
|
+
const msgs = await getMailMessages(selectedName, { sent: newTab === "sent" });
|
|
45
|
+
setMessages(msgs);
|
|
46
|
+
setStage(newTab === "sent" ? "view-sent" : "view-received");
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
50
|
+
setStage("error");
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
useInput((input, key) => {
|
|
54
|
+
if ((stage === "view-received" || stage === "view-sent")) {
|
|
55
|
+
if (key.upArrow)
|
|
56
|
+
setScrollOffset((o) => Math.max(0, o - 1));
|
|
57
|
+
if (key.downArrow)
|
|
58
|
+
setScrollOffset((o) => o + 1);
|
|
59
|
+
if (input === "r" && stage === "view-received") {
|
|
60
|
+
handleTabSwitch("received");
|
|
61
|
+
}
|
|
62
|
+
if (input === "s" && stage === "view-received") {
|
|
63
|
+
handleTabSwitch("sent");
|
|
64
|
+
}
|
|
65
|
+
if (input === "r" && stage === "view-sent") {
|
|
66
|
+
handleTabSwitch("received");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
if (sessionsLoading) {
|
|
71
|
+
return (_jsx(Box, { height: contentHeight, alignItems: "center", justifyContent: "center", children: _jsx(Spinner, { label: "Loading sessions..." }) }));
|
|
72
|
+
}
|
|
73
|
+
if (stage === "select-session") {
|
|
74
|
+
if (sessions.length === 0) {
|
|
75
|
+
return (_jsx(Box, { height: contentHeight, alignItems: "center", justifyContent: "center", children: _jsx(Text, { dimColor: true, children: "No sessions yet. Create one first." }) }));
|
|
76
|
+
}
|
|
77
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Text, { bold: true, children: "Read Mail" }), _jsx(Text, { dimColor: true, children: "Select a session:" }), _jsx(Select, { options: sessions.map((s) => ({ label: s.name, value: s.name })), onChange: loadMessages })] }));
|
|
78
|
+
}
|
|
79
|
+
if (stage === "loading") {
|
|
80
|
+
return (_jsx(Box, { height: contentHeight, alignItems: "center", justifyContent: "center", children: _jsx(Spinner, { label: `Loading ${viewTab} messages...` }) }));
|
|
81
|
+
}
|
|
82
|
+
if (stage === "error") {
|
|
83
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Text, { bold: true, children: "Read Mail" }), _jsxs(Text, { color: "red", children: ["Error: ", error] })] }));
|
|
84
|
+
}
|
|
85
|
+
const renderMessages = () => {
|
|
86
|
+
if (messages.length === 0) {
|
|
87
|
+
return (_jsx(Box, { height: contentHeight - 6, alignItems: "center", justifyContent: "center", children: _jsxs(Text, { dimColor: true, children: ["No ", viewTab, " messages."] }) }));
|
|
88
|
+
}
|
|
89
|
+
const lines = [];
|
|
90
|
+
const maxNameLen = Math.max(...messages.map((m) => m.from_name.length));
|
|
91
|
+
for (const msg of messages) {
|
|
92
|
+
const timestamp = new Date(msg.created_at).toLocaleString();
|
|
93
|
+
lines.push({ text: `─`, color: "gray" });
|
|
94
|
+
lines.push({ text: `${msg.from_name.padEnd(maxNameLen)} → ${msg.to_name}`, color: "cyan" });
|
|
95
|
+
lines.push({ text: `${timestamp}`, color: "gray" });
|
|
96
|
+
lines.push({ text: msg.read ? "" : " [unread]" });
|
|
97
|
+
lines.push({ text: "" });
|
|
98
|
+
for (const line of msg.body.split("\n")) {
|
|
99
|
+
lines.push({ text: ` ${line}` });
|
|
100
|
+
}
|
|
101
|
+
lines.push({ text: "" });
|
|
102
|
+
}
|
|
103
|
+
const viewHeight = contentHeight - 6;
|
|
104
|
+
const maxOffset = Math.max(0, lines.length - viewHeight);
|
|
105
|
+
const clampedOffset = Math.min(scrollOffset, maxOffset);
|
|
106
|
+
const visible = lines.slice(clampedOffset, clampedOffset + viewHeight);
|
|
107
|
+
return (_jsx(Box, { flexDirection: "column", height: viewHeight, overflow: "hidden", children: visible.map((line, i) => (_jsx(Text, { color: line.color, children: line.text }, i))) }));
|
|
108
|
+
};
|
|
109
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 0, children: [_jsxs(Box, { gap: 2, marginBottom: 1, children: [_jsx(Text, { bold: true, children: "Mail" }), _jsx(Text, { color: "cyan", children: selectedName }), _jsxs(Text, { dimColor: true, children: ["(", messages.length, " ", viewTab, " messages)"] })] }), _jsxs(Box, { gap: 2, marginBottom: 1, children: [viewTab === "received" ? (_jsx(Text, { bold: true, color: "green", children: "[Received]" })) : (_jsx(Text, { dimColor: true, children: "Received (press r)" })), viewTab === "sent" ? (_jsx(Text, { bold: true, color: "yellow", children: "[Sent]" })) : (_jsx(Text, { dimColor: true, children: "Sent (press s)" }))] }), renderMessages()] }));
|
|
110
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
interface SendMessageProps {
|
|
2
|
+
serverUrl: string;
|
|
3
|
+
password: string;
|
|
4
|
+
onBack: () => void;
|
|
5
|
+
contentHeight: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function SendMessage({ serverUrl, password, onBack, contentHeight }: SendMessageProps): import("react/jsx-runtime").JSX.Element | null;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { Box, Text } from "ink";
|
|
4
|
+
import { Select, Spinner, TextInput } from "@inkjs/ui";
|
|
5
|
+
import { useApi, useAsyncState } from "../hooks/useApi.js";
|
|
6
|
+
export function SendMessage({ serverUrl, password, onBack, contentHeight }) {
|
|
7
|
+
const { listSessions, sendMailMessage, getConfig } = useApi(serverUrl, password);
|
|
8
|
+
const { run: runList } = useAsyncState();
|
|
9
|
+
const [sessions, setSessions] = useState([]);
|
|
10
|
+
const [sessionsLoading, setSessionsLoading] = useState(true);
|
|
11
|
+
const [stage, setStage] = useState("select-recipients");
|
|
12
|
+
const [recipients, setRecipients] = useState([]);
|
|
13
|
+
const [messageBody, setMessageBody] = useState("");
|
|
14
|
+
const [error, setError] = useState(null);
|
|
15
|
+
const [submitted, setSubmitted] = useState(false);
|
|
16
|
+
const [senderName, setSenderName] = useState("Human");
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
getConfig().then((config) => {
|
|
19
|
+
setSenderName(config.human_name ?? "Human");
|
|
20
|
+
});
|
|
21
|
+
}, [getConfig]);
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
runList(listSessions).then((rows) => {
|
|
24
|
+
setSessions(rows ?? []);
|
|
25
|
+
setSessionsLoading(false);
|
|
26
|
+
});
|
|
27
|
+
}, []);
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (stage === "done" || stage === "error") {
|
|
30
|
+
const timer = setTimeout(onBack, 1500);
|
|
31
|
+
return () => clearTimeout(timer);
|
|
32
|
+
}
|
|
33
|
+
}, [stage, onBack]);
|
|
34
|
+
const handleRecipientSelect = (value) => {
|
|
35
|
+
setRecipients([value]);
|
|
36
|
+
setStage("enter-body");
|
|
37
|
+
};
|
|
38
|
+
const handleBodySubmit = async (body) => {
|
|
39
|
+
const trimmed = body.trim();
|
|
40
|
+
if (!trimmed || recipients.length === 0)
|
|
41
|
+
return;
|
|
42
|
+
setSubmitted(true);
|
|
43
|
+
setMessageBody(trimmed);
|
|
44
|
+
setStage("sending");
|
|
45
|
+
try {
|
|
46
|
+
await sendMailMessage(senderName, recipients, trimmed);
|
|
47
|
+
setStage("done");
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
51
|
+
setStage("error");
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
if (sessionsLoading) {
|
|
55
|
+
return (_jsx(Box, { height: contentHeight, alignItems: "center", justifyContent: "center", children: _jsx(Spinner, { label: "Loading sessions..." }) }));
|
|
56
|
+
}
|
|
57
|
+
if (stage === "select-recipients") {
|
|
58
|
+
if (sessions.length === 0) {
|
|
59
|
+
return (_jsx(Box, { height: contentHeight, alignItems: "center", justifyContent: "center", children: _jsx(Text, { dimColor: true, children: "No sessions yet. Create one first." }) }));
|
|
60
|
+
}
|
|
61
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Text, { bold: true, children: "Send Message" }), _jsx(Text, { dimColor: true, children: "Select a recipient agent:" }), _jsx(Select, { options: sessions.map((s) => ({ label: s.name, value: s.name })), onChange: handleRecipientSelect })] }));
|
|
62
|
+
}
|
|
63
|
+
if (stage === "enter-body" && !submitted) {
|
|
64
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Text, { bold: true, children: "Send Message" }), _jsxs(Box, { gap: 1, children: [_jsx(Text, { children: "From: " }), _jsx(Text, { color: "cyan", children: senderName })] }), _jsxs(Box, { gap: 1, children: [_jsx(Text, { children: "To: " }), _jsx(Text, { color: "cyan", children: recipients[0] })] }), _jsxs(Box, { gap: 1, children: [_jsx(Text, { children: "Body: " }), _jsx(TextInput, { placeholder: "Type your message...", onSubmit: handleBodySubmit })] })] }));
|
|
65
|
+
}
|
|
66
|
+
if (stage === "sending") {
|
|
67
|
+
return (_jsx(Spinner, { label: "Sending message..." }));
|
|
68
|
+
}
|
|
69
|
+
if (stage === "done") {
|
|
70
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Text, { bold: true, children: "Send Message" }), _jsx(Text, { color: "green", children: "Message sent!" }), _jsx(Text, { dimColor: true, children: "Returning to menu..." })] }));
|
|
71
|
+
}
|
|
72
|
+
if (stage === "error") {
|
|
73
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Text, { bold: true, children: "Send Message" }), _jsxs(Text, { color: "red", children: ["Error: ", error] }), _jsx(Text, { dimColor: true, children: "Returning to menu..." })] }));
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect } from "react";
|
|
3
|
+
import { Box, Text } from "ink";
|
|
4
|
+
import { useApi } from "../hooks/useApi.js";
|
|
5
|
+
export function SessionSidebar({ serverUrl, password }) {
|
|
6
|
+
const { listSessions } = useApi(serverUrl, password);
|
|
7
|
+
const [sessions, setSessions] = useState([]);
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
listSessions().then((s) => setSessions(s));
|
|
10
|
+
}, [listSessions]);
|
|
11
|
+
if (sessions.length === 0)
|
|
12
|
+
return null;
|
|
13
|
+
return (_jsxs(Box, { borderStyle: "single", borderColor: "cyan", paddingX: 1, flexDirection: "column", gap: 0, children: [_jsx(Text, { bold: true, color: "cyan", children: "Sessions" }), sessions.map((session, i) => (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: getNodeColor(i), children: "\u25CF" }), _jsx(Text, { children: session.name })] }, session.id)))] }));
|
|
14
|
+
}
|
|
15
|
+
function getNodeColor(index) {
|
|
16
|
+
const colors = ["green", "blue", "yellow", "magenta", "cyan", "red", "white"];
|
|
17
|
+
return colors[index % colors.length];
|
|
18
|
+
}
|
|
@@ -13,6 +13,18 @@ export interface SessionMessage {
|
|
|
13
13
|
role: "user" | "assistant";
|
|
14
14
|
parts: MessagePart[];
|
|
15
15
|
}
|
|
16
|
+
export interface MailMessage {
|
|
17
|
+
id: number;
|
|
18
|
+
from_name: string;
|
|
19
|
+
to_name: string;
|
|
20
|
+
body: string;
|
|
21
|
+
read: boolean;
|
|
22
|
+
injected: boolean;
|
|
23
|
+
created_at: string;
|
|
24
|
+
}
|
|
25
|
+
export interface Config {
|
|
26
|
+
[key: string]: string;
|
|
27
|
+
}
|
|
16
28
|
export declare function useApi(serverUrl: string, password: string): {
|
|
17
29
|
listSessions: () => Promise<Session[]>;
|
|
18
30
|
createSession: (name: string) => Promise<Session>;
|
|
@@ -24,6 +36,25 @@ export declare function useApi(serverUrl: string, password: string): {
|
|
|
24
36
|
messageID: string;
|
|
25
37
|
}>;
|
|
26
38
|
regenerateCode: (name: string) => Promise<Session>;
|
|
39
|
+
getConfig: () => Promise<Config>;
|
|
40
|
+
setConfig: (key: string, value: string) => Promise<{
|
|
41
|
+
ok: boolean;
|
|
42
|
+
key: string;
|
|
43
|
+
value: string;
|
|
44
|
+
}>;
|
|
45
|
+
getMailMessages: (name: string, options?: {
|
|
46
|
+
sent?: boolean;
|
|
47
|
+
unreadOnly?: boolean;
|
|
48
|
+
}) => Promise<MailMessage[]>;
|
|
49
|
+
sendMailMessage: (from: string, to: string[], body: string) => Promise<{
|
|
50
|
+
ok: boolean;
|
|
51
|
+
results: Array<{
|
|
52
|
+
to: string;
|
|
53
|
+
messageId: number;
|
|
54
|
+
injected: boolean;
|
|
55
|
+
}>;
|
|
56
|
+
}>;
|
|
57
|
+
markMessageRead: (id: number) => Promise<MailMessage>;
|
|
27
58
|
};
|
|
28
59
|
export declare function useAsyncState<T>(): {
|
|
29
60
|
run: (fn: () => Promise<T>) => Promise<T | null>;
|
|
@@ -57,7 +57,41 @@ export function useApi(serverUrl, password) {
|
|
|
57
57
|
const regenerateCode = useCallback(async (name) => {
|
|
58
58
|
return apiFetch(`${base}/sessions/${encodeURIComponent(name)}/regenerate-code`, password, { method: "POST" });
|
|
59
59
|
}, [base, password]);
|
|
60
|
-
|
|
60
|
+
const getConfig = useCallback(async () => {
|
|
61
|
+
return apiFetch(`${base}/config`, password);
|
|
62
|
+
}, [base, password]);
|
|
63
|
+
const setConfig = useCallback(async (key, value) => {
|
|
64
|
+
return apiFetch(`${base}/config`, password, { method: "PUT", body: JSON.stringify({ key, value }) });
|
|
65
|
+
}, [base, password]);
|
|
66
|
+
const getMailMessages = useCallback(async (name, options) => {
|
|
67
|
+
const params = new URLSearchParams();
|
|
68
|
+
if (options?.sent)
|
|
69
|
+
params.set("sent", "true");
|
|
70
|
+
if (options?.unreadOnly)
|
|
71
|
+
params.set("unread_only", "true");
|
|
72
|
+
const query = params.toString() ? `?${params.toString()}` : "";
|
|
73
|
+
return apiFetch(`${base}/messages/${encodeURIComponent(name)}${query}`, password);
|
|
74
|
+
}, [base, password]);
|
|
75
|
+
const sendMailMessage = useCallback(async (from, to, body) => {
|
|
76
|
+
return apiFetch(`${base}/messages`, password, { method: "POST", body: JSON.stringify({ from, to, body }) });
|
|
77
|
+
}, [base, password]);
|
|
78
|
+
const markMessageRead = useCallback(async (id) => {
|
|
79
|
+
return apiFetch(`${base}/messages/${id}/read`, password, { method: "POST" });
|
|
80
|
+
}, [base, password]);
|
|
81
|
+
return {
|
|
82
|
+
listSessions,
|
|
83
|
+
createSession,
|
|
84
|
+
deleteSession,
|
|
85
|
+
checkHealth,
|
|
86
|
+
getMessages,
|
|
87
|
+
injectText,
|
|
88
|
+
regenerateCode,
|
|
89
|
+
getConfig,
|
|
90
|
+
setConfig,
|
|
91
|
+
getMailMessages,
|
|
92
|
+
sendMailMessage,
|
|
93
|
+
markMessageRead,
|
|
94
|
+
};
|
|
61
95
|
}
|
|
62
96
|
export function useAsyncState() {
|
|
63
97
|
const [state, setState] = useState({
|
package/dist/server/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { Sql } from "../db/index.js";
|
|
2
2
|
import type { OpencodeClient } from "../lib/opencode.js";
|
|
3
|
-
export declare function createApp(sql: Sql, opencode: OpencodeClient, password: string): import("express-serve-static-core").Express;
|
|
3
|
+
export declare function createApp(sql: Sql, opencode: OpencodeClient, password: string, serverUrl: string): import("express-serve-static-core").Express;
|
package/dist/server/index.js
CHANGED
|
@@ -10,13 +10,13 @@ function authMiddleware(password) {
|
|
|
10
10
|
next();
|
|
11
11
|
};
|
|
12
12
|
}
|
|
13
|
-
export function createApp(sql, opencode, password) {
|
|
13
|
+
export function createApp(sql, opencode, password, serverUrl) {
|
|
14
14
|
const app = express();
|
|
15
15
|
app.use(express.json());
|
|
16
16
|
// Worker routes are unauthenticated — mounted before auth middleware
|
|
17
|
-
app.use("/", createWorkerRouter(sql));
|
|
17
|
+
app.use("/", createWorkerRouter(sql, opencode, serverUrl));
|
|
18
18
|
// Everything else requires Bearer auth
|
|
19
19
|
app.use(authMiddleware(password));
|
|
20
|
-
app.use("/", createRouter(sql, opencode));
|
|
20
|
+
app.use("/", createRouter(sql, opencode, serverUrl));
|
|
21
21
|
return app;
|
|
22
22
|
}
|
package/dist/server/routes.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Router } from "express";
|
|
2
2
|
import type { Sql } from "../db/index.js";
|
|
3
3
|
import type { OpencodeClient } from "../lib/opencode.js";
|
|
4
|
-
export declare function createRouter(sql: Sql, opencode: OpencodeClient): Router;
|
|
5
|
-
export declare function createWorkerRouter(sql: Sql): Router;
|
|
4
|
+
export declare function createRouter(sql: Sql, opencode: OpencodeClient, serverUrl: string): Router;
|
|
5
|
+
export declare function createWorkerRouter(sql: Sql, opencode: OpencodeClient, serverUrl: string): Router;
|
package/dist/server/routes.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import { Router } from "express";
|
|
2
|
-
export function createRouter(sql, opencode) {
|
|
2
|
+
export function createRouter(sql, opencode, serverUrl) {
|
|
3
3
|
const router = Router();
|
|
4
|
-
// GET /health
|
|
5
4
|
router.get("/health", (_req, res) => {
|
|
6
5
|
res.json({ ok: true });
|
|
7
6
|
});
|
|
8
|
-
// GET /sessions
|
|
9
7
|
router.get("/sessions", async (_req, res) => {
|
|
10
8
|
try {
|
|
11
9
|
const rows = await sql `
|
|
@@ -20,7 +18,6 @@ export function createRouter(sql, opencode) {
|
|
|
20
18
|
res.status(500).json({ error: "Internal server error" });
|
|
21
19
|
}
|
|
22
20
|
});
|
|
23
|
-
// POST /sessions { name }
|
|
24
21
|
router.post("/sessions", async (req, res) => {
|
|
25
22
|
const { name } = req.body;
|
|
26
23
|
if (!name || typeof name !== "string" || !name.trim()) {
|
|
@@ -35,7 +32,6 @@ export function createRouter(sql, opencode) {
|
|
|
35
32
|
res.status(409).json({ error: `Session name "${trimmedName}" already exists` });
|
|
36
33
|
return;
|
|
37
34
|
}
|
|
38
|
-
// Create the OpenCode session
|
|
39
35
|
let opencodeSessionId;
|
|
40
36
|
try {
|
|
41
37
|
const session = await opencode.session.create();
|
|
@@ -46,25 +42,42 @@ export function createRouter(sql, opencode) {
|
|
|
46
42
|
res.status(502).json({ error: "Failed to create OpenCode session", detail: String(err) });
|
|
47
43
|
return;
|
|
48
44
|
}
|
|
49
|
-
|
|
45
|
+
let row;
|
|
50
46
|
try {
|
|
51
|
-
const [
|
|
47
|
+
const [inserted] = await sql `
|
|
52
48
|
INSERT INTO sessions (name, session_id)
|
|
53
49
|
VALUES (${trimmedName}, ${opencodeSessionId})
|
|
54
50
|
RETURNING id, name, session_id, agent_code, created_at
|
|
55
51
|
`;
|
|
56
|
-
|
|
52
|
+
row = inserted;
|
|
57
53
|
}
|
|
58
54
|
catch (err) {
|
|
59
55
|
console.error("DB insert error:", err);
|
|
60
56
|
try {
|
|
61
57
|
await opencode.session.delete(opencodeSessionId);
|
|
62
58
|
}
|
|
63
|
-
catch {
|
|
59
|
+
catch { }
|
|
64
60
|
res.status(500).json({ error: "Internal server error" });
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
const providers = await opencode.app.providers();
|
|
65
|
+
const defaultEntry = Object.entries(providers.default)[0];
|
|
66
|
+
if (defaultEntry) {
|
|
67
|
+
const clockInToken = `${row.agent_code}@${serverUrl}`;
|
|
68
|
+
const firstMessage = `Run \`agent-office worker clock-in ${clockInToken}\``;
|
|
69
|
+
await opencode.session.chat(opencodeSessionId, {
|
|
70
|
+
modelID: defaultEntry[0],
|
|
71
|
+
providerID: defaultEntry[1],
|
|
72
|
+
parts: [{ type: "text", text: firstMessage }],
|
|
73
|
+
});
|
|
74
|
+
}
|
|
65
75
|
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
console.warn("Warning: could not send first message to session:", err);
|
|
78
|
+
}
|
|
79
|
+
res.status(201).json(row);
|
|
66
80
|
});
|
|
67
|
-
// POST /sessions/:name/regenerate-code
|
|
68
81
|
router.post("/sessions/:name/regenerate-code", async (req, res) => {
|
|
69
82
|
const { name } = req.params;
|
|
70
83
|
const rows = await sql `
|
|
@@ -88,7 +101,6 @@ export function createRouter(sql, opencode) {
|
|
|
88
101
|
res.status(500).json({ error: "Internal server error" });
|
|
89
102
|
}
|
|
90
103
|
});
|
|
91
|
-
// GET /sessions/:name/messages?limit=N
|
|
92
104
|
router.get("/sessions/:name/messages", async (req, res) => {
|
|
93
105
|
const { name } = req.params;
|
|
94
106
|
const limit = Math.min(parseInt(req.query.limit ?? "20", 10), 100);
|
|
@@ -118,7 +130,6 @@ export function createRouter(sql, opencode) {
|
|
|
118
130
|
res.status(502).json({ error: "Failed to fetch messages from OpenCode", detail: String(err) });
|
|
119
131
|
}
|
|
120
132
|
});
|
|
121
|
-
// POST /sessions/:name/inject { text, modelID?, providerID? }
|
|
122
133
|
router.post("/sessions/:name/inject", async (req, res) => {
|
|
123
134
|
const { name } = req.params;
|
|
124
135
|
const { text, modelID, providerID } = req.body;
|
|
@@ -166,7 +177,6 @@ export function createRouter(sql, opencode) {
|
|
|
166
177
|
res.status(502).json({ error: "Failed to inject message into OpenCode session", detail: String(err) });
|
|
167
178
|
}
|
|
168
179
|
});
|
|
169
|
-
// DELETE /sessions/:name
|
|
170
180
|
router.delete("/sessions/:name", async (req, res) => {
|
|
171
181
|
const { name } = req.params;
|
|
172
182
|
const rows = await sql `
|
|
@@ -194,13 +204,171 @@ export function createRouter(sql, opencode) {
|
|
|
194
204
|
res.status(500).json({ error: "Internal server error" });
|
|
195
205
|
}
|
|
196
206
|
});
|
|
207
|
+
router.get("/config", async (_req, res) => {
|
|
208
|
+
try {
|
|
209
|
+
const rows = await sql `SELECT key, value FROM config`;
|
|
210
|
+
const config = {};
|
|
211
|
+
for (const row of rows) {
|
|
212
|
+
config[row.key] = row.value;
|
|
213
|
+
}
|
|
214
|
+
res.json(config);
|
|
215
|
+
}
|
|
216
|
+
catch (err) {
|
|
217
|
+
console.error("GET /config error:", err);
|
|
218
|
+
res.status(500).json({ error: "Internal server error" });
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
router.put("/config", async (req, res) => {
|
|
222
|
+
const { key, value } = req.body;
|
|
223
|
+
if (!key || typeof key !== "string" || !key.trim()) {
|
|
224
|
+
res.status(400).json({ error: "key is required" });
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
if (typeof value !== "string") {
|
|
228
|
+
res.status(400).json({ error: "value must be a string" });
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
try {
|
|
232
|
+
await sql `
|
|
233
|
+
INSERT INTO config (key, value) VALUES (${key}, ${value})
|
|
234
|
+
ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value
|
|
235
|
+
`;
|
|
236
|
+
res.json({ ok: true, key, value });
|
|
237
|
+
}
|
|
238
|
+
catch (err) {
|
|
239
|
+
console.error("PUT /config error:", err);
|
|
240
|
+
res.status(500).json({ error: "Internal server error" });
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
router.get("/messages/:name", async (req, res) => {
|
|
244
|
+
const { name } = req.params;
|
|
245
|
+
const { sent, unread_only } = req.query;
|
|
246
|
+
try {
|
|
247
|
+
let rows;
|
|
248
|
+
if (sent === "true") {
|
|
249
|
+
rows = await sql `
|
|
250
|
+
SELECT id, from_name, to_name, body, read, injected, created_at
|
|
251
|
+
FROM messages
|
|
252
|
+
WHERE from_name = ${name}
|
|
253
|
+
ORDER BY created_at DESC
|
|
254
|
+
`;
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
if (unread_only === "true") {
|
|
258
|
+
rows = await sql `
|
|
259
|
+
SELECT id, from_name, to_name, body, read, injected, created_at
|
|
260
|
+
FROM messages
|
|
261
|
+
WHERE to_name = ${name} AND read = FALSE
|
|
262
|
+
ORDER BY created_at DESC
|
|
263
|
+
`;
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
rows = await sql `
|
|
267
|
+
SELECT id, from_name, to_name, body, read, injected, created_at
|
|
268
|
+
FROM messages
|
|
269
|
+
WHERE to_name = ${name}
|
|
270
|
+
ORDER BY created_at DESC
|
|
271
|
+
`;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
res.json(rows);
|
|
275
|
+
}
|
|
276
|
+
catch (err) {
|
|
277
|
+
console.error("GET /messages/:name error:", err);
|
|
278
|
+
res.status(500).json({ error: "Internal server error" });
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
router.post("/messages", async (req, res) => {
|
|
282
|
+
const { from, to, body } = req.body;
|
|
283
|
+
if (!from || typeof from !== "string" || !from.trim()) {
|
|
284
|
+
res.status(400).json({ error: "from is required" });
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
if (!to || !Array.isArray(to) || to.length === 0) {
|
|
288
|
+
res.status(400).json({ error: "to must be a non-empty array of recipient names" });
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
if (!body || typeof body !== "string" || !body.trim()) {
|
|
292
|
+
res.status(400).json({ error: "body is required" });
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
const trimmedFrom = from.trim();
|
|
296
|
+
const trimmedBody = body.trim();
|
|
297
|
+
const sessions = await sql `SELECT name, session_id FROM sessions`;
|
|
298
|
+
const sessionMap = new Map(sessions.map((s) => [s.name, s.session_id]));
|
|
299
|
+
const validRecipients = [];
|
|
300
|
+
for (const recipient of to) {
|
|
301
|
+
if (typeof recipient !== "string" || !recipient.trim())
|
|
302
|
+
continue;
|
|
303
|
+
const r = recipient.trim();
|
|
304
|
+
const config = await sql `SELECT value FROM config WHERE key = 'human_name'`;
|
|
305
|
+
const humanName = config[0]?.value ?? "Human";
|
|
306
|
+
if (sessionMap.has(r) || r === humanName) {
|
|
307
|
+
validRecipients.push(r);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
if (validRecipients.length === 0) {
|
|
311
|
+
res.status(400).json({ error: "No valid recipients found" });
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
const results = [];
|
|
315
|
+
for (const recipient of validRecipients) {
|
|
316
|
+
let injected = false;
|
|
317
|
+
const [msgRow] = await sql `
|
|
318
|
+
INSERT INTO messages (from_name, to_name, body)
|
|
319
|
+
VALUES (${trimmedFrom}, ${recipient}, ${trimmedBody})
|
|
320
|
+
RETURNING id, from_name, to_name, body, read, injected, created_at
|
|
321
|
+
`;
|
|
322
|
+
if (sessionMap.has(recipient)) {
|
|
323
|
+
const sessionId = sessionMap.get(recipient);
|
|
324
|
+
try {
|
|
325
|
+
const providers = await opencode.app.providers();
|
|
326
|
+
const defaultEntry = Object.entries(providers.default)[0];
|
|
327
|
+
if (defaultEntry) {
|
|
328
|
+
const injectText = `[Message from "${trimmedFrom}"]: ${trimmedBody}`;
|
|
329
|
+
await opencode.session.chat(sessionId, {
|
|
330
|
+
modelID: defaultEntry[0],
|
|
331
|
+
providerID: defaultEntry[1],
|
|
332
|
+
parts: [{ type: "text", text: injectText }],
|
|
333
|
+
});
|
|
334
|
+
injected = true;
|
|
335
|
+
await sql `UPDATE messages SET injected = TRUE WHERE id = ${msgRow.id}`;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
catch (err) {
|
|
339
|
+
console.warn(`Warning: could not inject message into session ${recipient}:`, err);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
results.push({ to: recipient, messageId: msgRow.id, injected });
|
|
343
|
+
}
|
|
344
|
+
res.status(201).json({ ok: true, results });
|
|
345
|
+
});
|
|
346
|
+
router.post("/messages/:id/read", async (req, res) => {
|
|
347
|
+
const id = parseInt(req.params.id, 10);
|
|
348
|
+
if (isNaN(id)) {
|
|
349
|
+
res.status(400).json({ error: "Invalid message id" });
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
try {
|
|
353
|
+
const [updated] = await sql `
|
|
354
|
+
UPDATE messages SET read = TRUE WHERE id = ${id}
|
|
355
|
+
RETURNING id, from_name, to_name, body, read, injected, created_at
|
|
356
|
+
`;
|
|
357
|
+
if (!updated) {
|
|
358
|
+
res.status(404).json({ error: "Message not found" });
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
res.json(updated);
|
|
362
|
+
}
|
|
363
|
+
catch (err) {
|
|
364
|
+
console.error("POST /messages/:id/read error:", err);
|
|
365
|
+
res.status(500).json({ error: "Internal server error" });
|
|
366
|
+
}
|
|
367
|
+
});
|
|
197
368
|
return router;
|
|
198
369
|
}
|
|
199
|
-
|
|
200
|
-
export function createWorkerRouter(sql) {
|
|
370
|
+
export function createWorkerRouter(sql, opencode, serverUrl) {
|
|
201
371
|
const router = Router();
|
|
202
|
-
// GET /worker/clock-in — no Bearer auth, validated by agent_code
|
|
203
|
-
// Usage: agent-office worker clock-in <agent_code>@<url>
|
|
204
372
|
router.get("/worker/clock-in", async (req, res) => {
|
|
205
373
|
const { code } = req.query;
|
|
206
374
|
if (!code || typeof code !== "string") {
|
|
@@ -224,5 +392,109 @@ export function createWorkerRouter(sql) {
|
|
|
224
392
|
message: `Welcome to the agent office, your name is ${session.name}. Your OpenCode session ID is ${session.session_id}. You are now clocked in and ready to work.`,
|
|
225
393
|
});
|
|
226
394
|
});
|
|
395
|
+
router.get("/worker/list-coworkers", async (req, res) => {
|
|
396
|
+
const { code } = req.query;
|
|
397
|
+
if (!code || typeof code !== "string") {
|
|
398
|
+
res.status(400).json({ error: "code query parameter is required" });
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
const rows = await sql `
|
|
402
|
+
SELECT id, name, session_id, agent_code, created_at
|
|
403
|
+
FROM sessions
|
|
404
|
+
WHERE agent_code = ${code}
|
|
405
|
+
`;
|
|
406
|
+
if (rows.length === 0) {
|
|
407
|
+
res.status(401).json({ error: "Invalid agent code" });
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
const session = rows[0];
|
|
411
|
+
try {
|
|
412
|
+
const sessions = await sql `SELECT name FROM sessions WHERE name != ${session.name}`;
|
|
413
|
+
const config = await sql `SELECT value FROM config WHERE key = 'human_name'`;
|
|
414
|
+
const humanName = config[0]?.value ?? "Human";
|
|
415
|
+
const workers = sessions.map((s) => s.name);
|
|
416
|
+
workers.push(humanName);
|
|
417
|
+
res.json(workers);
|
|
418
|
+
}
|
|
419
|
+
catch (err) {
|
|
420
|
+
console.error("GET /worker/list-coworkers error:", err);
|
|
421
|
+
res.status(500).json({ error: "Internal server error" });
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
router.post("/worker/send-message", async (req, res) => {
|
|
425
|
+
const { code } = req.query;
|
|
426
|
+
const { to, body } = req.body;
|
|
427
|
+
if (!code || typeof code !== "string") {
|
|
428
|
+
res.status(400).json({ error: "code query parameter is required" });
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
const rows = await sql `
|
|
432
|
+
SELECT id, name, session_id, agent_code, created_at
|
|
433
|
+
FROM sessions
|
|
434
|
+
WHERE agent_code = ${code}
|
|
435
|
+
`;
|
|
436
|
+
if (rows.length === 0) {
|
|
437
|
+
res.status(401).json({ error: "Invalid agent code" });
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
const session = rows[0];
|
|
441
|
+
if (!to || !Array.isArray(to) || to.length === 0) {
|
|
442
|
+
res.status(400).json({ error: "to must be a non-empty array of recipient names" });
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
if (!body || typeof body !== "string" || !body.trim()) {
|
|
446
|
+
res.status(400).json({ error: "body is required" });
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
const trimmedBody = body.trim();
|
|
450
|
+
const sessions = await sql `SELECT name, session_id FROM sessions`;
|
|
451
|
+
const sessionMap = new Map(sessions.map((s) => [s.name, s.session_id]));
|
|
452
|
+
const config = await sql `SELECT value FROM config WHERE key = 'human_name'`;
|
|
453
|
+
const humanName = config[0]?.value ?? "Human";
|
|
454
|
+
const validRecipients = [];
|
|
455
|
+
for (const recipient of to) {
|
|
456
|
+
if (typeof recipient !== "string" || !recipient.trim())
|
|
457
|
+
continue;
|
|
458
|
+
const r = recipient.trim();
|
|
459
|
+
if (sessionMap.has(r) || r === humanName) {
|
|
460
|
+
validRecipients.push(r);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
if (validRecipients.length === 0) {
|
|
464
|
+
res.status(400).json({ error: "No valid recipients found" });
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
const results = [];
|
|
468
|
+
for (const recipient of validRecipients) {
|
|
469
|
+
let injected = false;
|
|
470
|
+
const [msgRow] = await sql `
|
|
471
|
+
INSERT INTO messages (from_name, to_name, body)
|
|
472
|
+
VALUES (${session.name}, ${recipient}, ${trimmedBody})
|
|
473
|
+
RETURNING id, from_name, to_name, body, read, injected, created_at
|
|
474
|
+
`;
|
|
475
|
+
if (sessionMap.has(recipient)) {
|
|
476
|
+
const recipientSessionId = sessionMap.get(recipient);
|
|
477
|
+
try {
|
|
478
|
+
const providers = await opencode.app.providers();
|
|
479
|
+
const defaultEntry = Object.entries(providers.default)[0];
|
|
480
|
+
if (defaultEntry) {
|
|
481
|
+
const injectText = `[Message from "${session.name}"]: ${trimmedBody}`;
|
|
482
|
+
await opencode.session.chat(recipientSessionId, {
|
|
483
|
+
modelID: defaultEntry[0],
|
|
484
|
+
providerID: defaultEntry[1],
|
|
485
|
+
parts: [{ type: "text", text: injectText }],
|
|
486
|
+
});
|
|
487
|
+
injected = true;
|
|
488
|
+
await sql `UPDATE messages SET injected = TRUE WHERE id = ${msgRow.id}`;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
catch (err) {
|
|
492
|
+
console.warn(`Warning: could not inject message into session ${recipient}:`, err);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
results.push({ to: recipient, messageId: msgRow.id, injected });
|
|
496
|
+
}
|
|
497
|
+
res.status(201).json({ ok: true, results });
|
|
498
|
+
});
|
|
227
499
|
return router;
|
|
228
500
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-office",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.1",
|
|
4
4
|
"description": "Manage OpenCode sessions with named aliases",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"dist"
|
|
21
21
|
],
|
|
22
22
|
"scripts": {
|
|
23
|
-
"dev:serve": "tsx src/cli.ts serve",
|
|
23
|
+
"dev:serve": "tsx --watch src/cli.ts serve",
|
|
24
24
|
"dev:manage": "tsx src/cli.ts manage",
|
|
25
25
|
"dev:worker": "tsx src/cli.ts worker",
|
|
26
26
|
"build": "tsc",
|