agent-office 0.0.1 → 0.0.3
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 +104 -3
- package/dist/commands/serve.js +7 -1
- package/dist/commands/worker.d.ts +12 -0
- package/dist/commands/worker.js +51 -0
- package/dist/db/index.d.ts +20 -0
- package/dist/db/migrate.js +43 -0
- package/dist/manage/app.js +42 -43
- package/dist/manage/components/CronList.d.ts +9 -0
- package/dist/manage/components/CronList.js +310 -0
- package/dist/manage/components/ItemSelector.d.ts +7 -0
- package/dist/manage/components/ItemSelector.js +20 -0
- package/dist/manage/components/MenuSelect.d.ts +13 -0
- package/dist/manage/components/MenuSelect.js +22 -0
- package/dist/manage/components/MyMail.d.ts +2 -1
- package/dist/manage/components/MyMail.js +107 -34
- package/dist/manage/components/ReadMail.js +3 -3
- package/dist/manage/components/SendMessage.d.ts +2 -1
- package/dist/manage/components/SendMessage.js +9 -6
- package/dist/manage/components/SessionList.js +472 -31
- package/dist/manage/components/SessionSidebar.js +7 -1
- package/dist/manage/components/TailMessages.js +54 -5
- package/dist/manage/hooks/useApi.d.ts +54 -3
- package/dist/manage/hooks/useApi.js +38 -2
- package/dist/server/cron.d.ts +24 -0
- package/dist/server/cron.js +121 -0
- package/dist/server/index.d.ts +2 -1
- package/dist/server/index.js +2 -2
- package/dist/server/routes.d.ts +2 -1
- package/dist/server/routes.js +916 -42
- package/package.json +3 -1
package/dist/server/routes.js
CHANGED
|
@@ -1,13 +1,141 @@
|
|
|
1
1
|
import { Router } from "express";
|
|
2
|
-
|
|
2
|
+
import { Cron as CronerInstance } from "croner";
|
|
3
|
+
const MAIL_INJECTION_BLURB = [
|
|
4
|
+
``,
|
|
5
|
+
`---`,
|
|
6
|
+
`You have a new message. Please review the injected message above and respond accordingly.`,
|
|
7
|
+
`IMPORTANT: When reading or responding, note that dollar signs ($) and other special`,
|
|
8
|
+
`characters may be interpreted as markdown. The sender may have included them but they`,
|
|
9
|
+
`could appear differently in your session view. Interpret context accordingly.`,
|
|
10
|
+
``,
|
|
11
|
+
`When responding to the sender:`,
|
|
12
|
+
`- Use the \`agent-office worker send-message\` tool so they can see your reply`,
|
|
13
|
+
`- Avoid excessive length - keep responses concise`,
|
|
14
|
+
`- Use markdown front-matter with a "choices" array if offering options`,
|
|
15
|
+
``,
|
|
16
|
+
`Tip: For currency or prices, use code blocks. Example: put numbers in single or`,
|
|
17
|
+
`double quotes to preserve formatting characters like dollar signs.`,
|
|
18
|
+
].join("\n");
|
|
19
|
+
function generateWelcomeMessage(name, mode, status, humanName, humanDescription, token) {
|
|
20
|
+
return [
|
|
21
|
+
`╔══════════════════════════════════════════════════════╗`,
|
|
22
|
+
`║ WELCOME TO THE AGENT OFFICE ║`,
|
|
23
|
+
`╚══════════════════════════════════════════════════════╝`,
|
|
24
|
+
``,
|
|
25
|
+
`You are now clocked in.`,
|
|
26
|
+
` Name: ${name}`,
|
|
27
|
+
...(mode ? [` Mode: ${mode}`] : []),
|
|
28
|
+
...(status ? [` Status: ${status}`] : []),
|
|
29
|
+
` Human manager: ${humanName} — the human who created your`,
|
|
30
|
+
` session, assigns your work, and is your`,
|
|
31
|
+
` primary point of contact for questions,`,
|
|
32
|
+
` updates, and decisions.`,
|
|
33
|
+
...(humanDescription ? [
|
|
34
|
+
` "${humanDescription}"`,
|
|
35
|
+
] : []),
|
|
36
|
+
``,
|
|
37
|
+
`The agent-office CLI is your PRIMARY means of communicating`,
|
|
38
|
+
`with your human manager (${humanName}) and your coworkers.`,
|
|
39
|
+
`Use it to send and receive messages, and to discover who`,
|
|
40
|
+
`else is working.`,
|
|
41
|
+
``,
|
|
42
|
+
`════════════════════════════════════════════════════════`,
|
|
43
|
+
` AVAILABLE COMMANDS`,
|
|
44
|
+
`════════════════════════════════════════════════════════`,
|
|
45
|
+
``,
|
|
46
|
+
` List your coworkers`,
|
|
47
|
+
` agent-office worker list-coworkers \\`,
|
|
48
|
+
` ${token}`,
|
|
49
|
+
``,
|
|
50
|
+
` Set your public status (visible to coworkers and manager)`,
|
|
51
|
+
` agent-office worker set-status \\`,
|
|
52
|
+
` --status "your status here" \\`,
|
|
53
|
+
` ${token}`,
|
|
54
|
+
``,
|
|
55
|
+
` Clear your public status`,
|
|
56
|
+
` agent-office worker set-status \\`,
|
|
57
|
+
` --clear \\`,
|
|
58
|
+
` ${token}`,
|
|
59
|
+
``,
|
|
60
|
+
` Send a message to your manager or a coworker`,
|
|
61
|
+
` agent-office worker send-message \\`,
|
|
62
|
+
` --name <recipient-name> \\`,
|
|
63
|
+
` --body "Your message here" \\`,
|
|
64
|
+
` ${token}`,
|
|
65
|
+
``,
|
|
66
|
+
` Send a message to multiple recipients at once`,
|
|
67
|
+
` agent-office worker send-message \\`,
|
|
68
|
+
` --name alice --name bob \\`,
|
|
69
|
+
` --body "Your message here" \\`,
|
|
70
|
+
` ${token}`,
|
|
71
|
+
``,
|
|
72
|
+
` Manage scheduled tasks (optional)`,
|
|
73
|
+
` agent-office worker cron \\`,
|
|
74
|
+
` ${token}`,
|
|
75
|
+
``,
|
|
76
|
+
`════════════════════════════════════════════════════════`,
|
|
77
|
+
` ⚠ IMPORTANT: YOUR SESSIONS ARE PRIVATE`,
|
|
78
|
+
`════════════════════════════════════════════════════════`,
|
|
79
|
+
``,
|
|
80
|
+
` Nobody — not ${humanName}, not your coworkers — can see`,
|
|
81
|
+
` anything you think, reason, or write inside this session.`,
|
|
82
|
+
` Your work is completely private until you explicitly send`,
|
|
83
|
+
` a message using the send-message command.`,
|
|
84
|
+
``,
|
|
85
|
+
` This means:`,
|
|
86
|
+
` • You must send a message to report progress or ask`,
|
|
87
|
+
` for help. Silence looks like inaction.`,
|
|
88
|
+
` • Finishing a task without messaging ${humanName}`,
|
|
89
|
+
` means they will never know it is done.`,
|
|
90
|
+
` • If you are blocked or uncertain, send a message —`,
|
|
91
|
+
` nobody will know otherwise.`,
|
|
92
|
+
``,
|
|
93
|
+
`════════════════════════════════════════════════════════`,
|
|
94
|
+
` TIPS`,
|
|
95
|
+
`════════════════════════════════════════════════════════`,
|
|
96
|
+
``,
|
|
97
|
+
` - Run list-coworkers to discover who is available and`,
|
|
98
|
+
` what their names are before sending messages.`,
|
|
99
|
+
` - Messages you send are delivered directly into the`,
|
|
100
|
+
` recipient's active session — they will see them`,
|
|
101
|
+
` immediately.`,
|
|
102
|
+
` - Your human manager is ${humanName}. They can send you`,
|
|
103
|
+
` messages at any time and those will appear here in`,
|
|
104
|
+
` your session just like this one. You can reach them`,
|
|
105
|
+
` by sending a message to --name ${humanName}.`,
|
|
106
|
+
` - Optional: Set up recurring scheduled tasks with cron`,
|
|
107
|
+
` jobs. Run 'agent-office worker cron list ${token}' to`,
|
|
108
|
+
` get started.`,
|
|
109
|
+
``,
|
|
110
|
+
].join("\n");
|
|
111
|
+
}
|
|
112
|
+
export function createRouter(sql, opencode, serverUrl, scheduler) {
|
|
3
113
|
const router = Router();
|
|
4
114
|
router.get("/health", (_req, res) => {
|
|
5
115
|
res.json({ ok: true });
|
|
6
116
|
});
|
|
117
|
+
router.get("/modes", async (_req, res) => {
|
|
118
|
+
try {
|
|
119
|
+
const config = await opencode.config.get();
|
|
120
|
+
const agent = config.agent ?? {};
|
|
121
|
+
const modes = Object.entries(agent)
|
|
122
|
+
.filter(([, val]) => val != null)
|
|
123
|
+
.map(([name, val]) => ({
|
|
124
|
+
name,
|
|
125
|
+
description: val.description ?? "",
|
|
126
|
+
model: val.model ?? "",
|
|
127
|
+
}));
|
|
128
|
+
res.json(modes);
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
console.error("GET /modes error:", err);
|
|
132
|
+
res.json([]);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
7
135
|
router.get("/sessions", async (_req, res) => {
|
|
8
136
|
try {
|
|
9
137
|
const rows = await sql `
|
|
10
|
-
SELECT id, name, session_id, agent_code, created_at
|
|
138
|
+
SELECT id, name, session_id, agent_code, mode, status, created_at
|
|
11
139
|
FROM sessions
|
|
12
140
|
ORDER BY created_at DESC
|
|
13
141
|
`;
|
|
@@ -19,12 +147,13 @@ export function createRouter(sql, opencode, serverUrl) {
|
|
|
19
147
|
}
|
|
20
148
|
});
|
|
21
149
|
router.post("/sessions", async (req, res) => {
|
|
22
|
-
const { name } = req.body;
|
|
150
|
+
const { name, mode } = req.body;
|
|
23
151
|
if (!name || typeof name !== "string" || !name.trim()) {
|
|
24
152
|
res.status(400).json({ error: "name is required" });
|
|
25
153
|
return;
|
|
26
154
|
}
|
|
27
155
|
const trimmedName = name.trim();
|
|
156
|
+
const trimmedMode = typeof mode === "string" && mode.trim() ? mode.trim() : null;
|
|
28
157
|
const existing = await sql `
|
|
29
158
|
SELECT id FROM sessions WHERE name = ${trimmedName}
|
|
30
159
|
`;
|
|
@@ -45,9 +174,9 @@ export function createRouter(sql, opencode, serverUrl) {
|
|
|
45
174
|
let row;
|
|
46
175
|
try {
|
|
47
176
|
const [inserted] = await sql `
|
|
48
|
-
INSERT INTO sessions (name, session_id)
|
|
49
|
-
VALUES (${trimmedName}, ${opencodeSessionId})
|
|
50
|
-
RETURNING id, name, session_id, agent_code, created_at
|
|
177
|
+
INSERT INTO sessions (name, session_id, mode)
|
|
178
|
+
VALUES (${trimmedName}, ${opencodeSessionId}, ${trimmedMode})
|
|
179
|
+
RETURNING id, name, session_id, agent_code, mode, created_at
|
|
51
180
|
`;
|
|
52
181
|
row = inserted;
|
|
53
182
|
}
|
|
@@ -65,11 +194,12 @@ export function createRouter(sql, opencode, serverUrl) {
|
|
|
65
194
|
const defaultEntry = Object.entries(providers.default)[0];
|
|
66
195
|
if (defaultEntry) {
|
|
67
196
|
const clockInToken = `${row.agent_code}@${serverUrl}`;
|
|
68
|
-
const
|
|
197
|
+
const enrollmentMessage = `You have been enrolled in the agent office.\n\nTo clock in and receive your full briefing, run:\n\n agent-office worker clock-in ${clockInToken}`;
|
|
69
198
|
await opencode.session.chat(opencodeSessionId, {
|
|
70
199
|
modelID: defaultEntry[0],
|
|
71
200
|
providerID: defaultEntry[1],
|
|
72
|
-
parts: [{ type: "text", text:
|
|
201
|
+
parts: [{ type: "text", text: enrollmentMessage }],
|
|
202
|
+
...(trimmedMode ? { mode: trimmedMode } : {}),
|
|
73
203
|
});
|
|
74
204
|
}
|
|
75
205
|
}
|
|
@@ -118,9 +248,26 @@ export function createRouter(sql, opencode, serverUrl) {
|
|
|
118
248
|
.slice(-limit)
|
|
119
249
|
.map((m) => ({
|
|
120
250
|
role: m.info.role,
|
|
121
|
-
parts: m.parts
|
|
122
|
-
|
|
123
|
-
|
|
251
|
+
parts: m.parts.map((p) => {
|
|
252
|
+
const part = p;
|
|
253
|
+
if (part.type === "text") {
|
|
254
|
+
return { type: "text", text: part.text ?? "" };
|
|
255
|
+
}
|
|
256
|
+
else if (part.type === "tool") {
|
|
257
|
+
// Extract tool name, input, and output from the tool state
|
|
258
|
+
const state = part.state;
|
|
259
|
+
return {
|
|
260
|
+
type: "tool",
|
|
261
|
+
tool: part.tool,
|
|
262
|
+
input: state?.input,
|
|
263
|
+
output: state?.output,
|
|
264
|
+
data: part,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
return { type: part.type, data: part };
|
|
269
|
+
}
|
|
270
|
+
}),
|
|
124
271
|
}))
|
|
125
272
|
.filter((m) => m.parts.length > 0);
|
|
126
273
|
res.json(result);
|
|
@@ -177,6 +324,48 @@ export function createRouter(sql, opencode, serverUrl) {
|
|
|
177
324
|
res.status(502).json({ error: "Failed to inject message into OpenCode session", detail: String(err) });
|
|
178
325
|
}
|
|
179
326
|
});
|
|
327
|
+
router.post("/sessions/:name/revert-to-start", async (req, res) => {
|
|
328
|
+
const { name } = req.params;
|
|
329
|
+
const rows = await sql `
|
|
330
|
+
SELECT id, name, session_id, agent_code, mode, status, created_at FROM sessions WHERE name = ${name}
|
|
331
|
+
`;
|
|
332
|
+
if (rows.length === 0) {
|
|
333
|
+
res.status(404).json({ error: `Session "${name}" not found` });
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
const session = rows[0];
|
|
337
|
+
try {
|
|
338
|
+
const messages = await opencode.session.messages(session.session_id);
|
|
339
|
+
if (messages.length === 0) {
|
|
340
|
+
res.status(400).json({ error: "Session has no messages to revert to" });
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
const firstMessage = messages[0];
|
|
344
|
+
if (!firstMessage || !firstMessage.info || !firstMessage.info.id) {
|
|
345
|
+
res.status(500).json({ error: "Failed to get first message ID" });
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
await opencode.session.revert(session.session_id, { messageID: firstMessage.info.id });
|
|
349
|
+
const providers = await opencode.app.providers();
|
|
350
|
+
const defaultEntry = Object.entries(providers.default)[0];
|
|
351
|
+
if (!defaultEntry) {
|
|
352
|
+
res.status(502).json({ error: "No default model configured in OpenCode" });
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
const clockInToken = `${session.agent_code}@${serverUrl}`;
|
|
356
|
+
const enrollmentMessage = `You have been enrolled in the agent office.\n\nTo clock in and receive your full briefing, run:\n\n agent-office worker clock-in ${clockInToken}`;
|
|
357
|
+
await opencode.session.chat(session.session_id, {
|
|
358
|
+
modelID: defaultEntry[0],
|
|
359
|
+
providerID: defaultEntry[1],
|
|
360
|
+
parts: [{ type: "text", text: enrollmentMessage }],
|
|
361
|
+
});
|
|
362
|
+
res.json({ ok: true, messageID: firstMessage.info.id });
|
|
363
|
+
}
|
|
364
|
+
catch (err) {
|
|
365
|
+
console.error("POST /sessions/:name/revert-to-start error:", err);
|
|
366
|
+
res.status(502).json({ error: "Failed to revert session", detail: String(err) });
|
|
367
|
+
}
|
|
368
|
+
});
|
|
180
369
|
router.delete("/sessions/:name", async (req, res) => {
|
|
181
370
|
const { name } = req.params;
|
|
182
371
|
const rows = await sql `
|
|
@@ -321,23 +510,21 @@ export function createRouter(sql, opencode, serverUrl) {
|
|
|
321
510
|
`;
|
|
322
511
|
if (sessionMap.has(recipient)) {
|
|
323
512
|
const sessionId = sessionMap.get(recipient);
|
|
324
|
-
|
|
325
|
-
|
|
513
|
+
const msgId = msgRow.id;
|
|
514
|
+
injected = true;
|
|
515
|
+
opencode.app.providers().then((providers) => {
|
|
326
516
|
const defaultEntry = Object.entries(providers.default)[0];
|
|
327
|
-
if (defaultEntry)
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
catch (err) {
|
|
517
|
+
if (!defaultEntry)
|
|
518
|
+
return;
|
|
519
|
+
const injectText = `[Message from "${trimmedFrom}"]: ${trimmedBody}${MAIL_INJECTION_BLURB}`;
|
|
520
|
+
return opencode.session.chat(sessionId, {
|
|
521
|
+
modelID: defaultEntry[0],
|
|
522
|
+
providerID: defaultEntry[1],
|
|
523
|
+
parts: [{ type: "text", text: injectText }],
|
|
524
|
+
}).then(() => sql `UPDATE messages SET injected = TRUE WHERE id = ${msgId}`);
|
|
525
|
+
}).catch((err) => {
|
|
339
526
|
console.warn(`Warning: could not inject message into session ${recipient}:`, err);
|
|
340
|
-
}
|
|
527
|
+
});
|
|
341
528
|
}
|
|
342
529
|
results.push({ to: recipient, messageId: msgRow.id, injected });
|
|
343
530
|
}
|
|
@@ -365,6 +552,253 @@ export function createRouter(sql, opencode, serverUrl) {
|
|
|
365
552
|
res.status(500).json({ error: "Internal server error" });
|
|
366
553
|
}
|
|
367
554
|
});
|
|
555
|
+
// ── Cron Jobs Endpoints ────────────────────────────────────────────────────
|
|
556
|
+
router.get("/crons", async (req, res) => {
|
|
557
|
+
try {
|
|
558
|
+
const { session_name } = req.query;
|
|
559
|
+
let rows;
|
|
560
|
+
if (session_name) {
|
|
561
|
+
rows = await sql `
|
|
562
|
+
SELECT id, name, session_name, schedule, timezone, message, enabled, created_at, last_run
|
|
563
|
+
FROM cron_jobs
|
|
564
|
+
WHERE session_name = ${session_name}
|
|
565
|
+
ORDER BY name
|
|
566
|
+
`;
|
|
567
|
+
}
|
|
568
|
+
else {
|
|
569
|
+
rows = await sql `
|
|
570
|
+
SELECT id, name, session_name, schedule, timezone, message, enabled, created_at, last_run
|
|
571
|
+
FROM cron_jobs
|
|
572
|
+
ORDER BY name
|
|
573
|
+
`;
|
|
574
|
+
}
|
|
575
|
+
const jobs = rows.map((job) => {
|
|
576
|
+
let nextRun = null;
|
|
577
|
+
if (job.enabled) {
|
|
578
|
+
try {
|
|
579
|
+
const options = {};
|
|
580
|
+
if (job.timezone)
|
|
581
|
+
options.timezone = job.timezone;
|
|
582
|
+
const cronJob = new CronerInstance(job.schedule, options);
|
|
583
|
+
const next = cronJob.nextRun();
|
|
584
|
+
nextRun = next ? next.toISOString() : null;
|
|
585
|
+
}
|
|
586
|
+
catch { }
|
|
587
|
+
}
|
|
588
|
+
return { ...job, next_run: nextRun, last_run: job.last_run ? job.last_run.toISOString() : null, created_at: job.created_at.toISOString() };
|
|
589
|
+
});
|
|
590
|
+
res.json(jobs);
|
|
591
|
+
}
|
|
592
|
+
catch (err) {
|
|
593
|
+
console.error("GET /crons error:", err);
|
|
594
|
+
res.status(500).json({ error: "Internal server error" });
|
|
595
|
+
}
|
|
596
|
+
});
|
|
597
|
+
router.post("/crons", async (req, res) => {
|
|
598
|
+
const { name, session_name, schedule, message, timezone } = req.body;
|
|
599
|
+
if (!name || typeof name !== "string" || !name.trim()) {
|
|
600
|
+
res.status(400).json({ error: "name is required" });
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
if (!session_name || typeof session_name !== "string") {
|
|
604
|
+
res.status(400).json({ error: "session_name is required" });
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
if (!schedule || typeof schedule !== "string" || !schedule.trim()) {
|
|
608
|
+
res.status(400).json({ error: "schedule is required" });
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
if (!message || typeof message !== "string" || !message.trim()) {
|
|
612
|
+
res.status(400).json({ error: "message is required" });
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
const trimmedName = name.trim();
|
|
616
|
+
const trimmedSchedule = schedule.trim();
|
|
617
|
+
const trimmedMessage = message.trim();
|
|
618
|
+
try {
|
|
619
|
+
new CronerInstance(trimmedSchedule);
|
|
620
|
+
}
|
|
621
|
+
catch {
|
|
622
|
+
res.status(400).json({ error: "Invalid cron schedule expression" });
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
if (timezone) {
|
|
626
|
+
try {
|
|
627
|
+
new CronerInstance("0 0 * * *", { timezone });
|
|
628
|
+
}
|
|
629
|
+
catch {
|
|
630
|
+
res.status(400).json({ error: "Invalid timezone" });
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
const [existing] = await sql `
|
|
635
|
+
SELECT id FROM cron_jobs WHERE name = ${trimmedName} AND session_name = ${session_name}
|
|
636
|
+
`;
|
|
637
|
+
if (existing) {
|
|
638
|
+
res.status(409).json({ error: `Cron job "${trimmedName}" already exists for this session` });
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
try {
|
|
642
|
+
const [inserted] = await sql `
|
|
643
|
+
INSERT INTO cron_jobs (name, session_name, schedule, timezone, message)
|
|
644
|
+
VALUES (${trimmedName}, ${session_name}, ${trimmedSchedule}, ${timezone ?? null}, ${trimmedMessage})
|
|
645
|
+
RETURNING id, name, session_name, schedule, timezone, message, enabled, created_at, last_run
|
|
646
|
+
`;
|
|
647
|
+
const cronJobrow = inserted;
|
|
648
|
+
scheduler.addCronJob(cronJobrow);
|
|
649
|
+
const nextRun = cronJobrow.enabled ? (() => {
|
|
650
|
+
try {
|
|
651
|
+
const options = {};
|
|
652
|
+
if (cronJobrow.timezone)
|
|
653
|
+
options.timezone = cronJobrow.timezone;
|
|
654
|
+
const cronJob = new CronerInstance(cronJobrow.schedule, options);
|
|
655
|
+
const next = cronJob.nextRun();
|
|
656
|
+
return next ? next.toISOString() : null;
|
|
657
|
+
}
|
|
658
|
+
catch {
|
|
659
|
+
return null;
|
|
660
|
+
}
|
|
661
|
+
})() : null;
|
|
662
|
+
res.status(201).json({
|
|
663
|
+
...cronJobrow,
|
|
664
|
+
next_run: nextRun,
|
|
665
|
+
last_run: cronJobrow?.last_run?.toISOString() ?? null,
|
|
666
|
+
created_at: cronJobrow.created_at.toISOString(),
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
catch (err) {
|
|
670
|
+
console.error("POST /crons error:", err);
|
|
671
|
+
res.status(500).json({ error: "Internal server error" });
|
|
672
|
+
}
|
|
673
|
+
});
|
|
674
|
+
router.delete("/crons/:id", async (req, res) => {
|
|
675
|
+
const id = parseInt(req.params.id, 10);
|
|
676
|
+
if (isNaN(id)) {
|
|
677
|
+
res.status(400).json({ error: "Invalid cron job id" });
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
try {
|
|
681
|
+
const [job] = await sql `SELECT id, name FROM cron_jobs WHERE id = ${id}`;
|
|
682
|
+
if (!job) {
|
|
683
|
+
res.status(404).json({ error: "Cron job not found" });
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
scheduler.removeCronJob(id);
|
|
687
|
+
await sql `DELETE FROM cron_jobs WHERE id = ${id}`;
|
|
688
|
+
res.json({ deleted: true, id, name: job.name });
|
|
689
|
+
}
|
|
690
|
+
catch (err) {
|
|
691
|
+
console.error("DELETE /crons/:id error:", err);
|
|
692
|
+
res.status(500).json({ error: "Internal server error" });
|
|
693
|
+
}
|
|
694
|
+
});
|
|
695
|
+
router.post("/crons/:id/enable", async (req, res) => {
|
|
696
|
+
const id = parseInt(req.params.id, 10);
|
|
697
|
+
if (isNaN(id)) {
|
|
698
|
+
res.status(400).json({ error: "Invalid cron job id" });
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
try {
|
|
702
|
+
const [existing] = await sql `
|
|
703
|
+
SELECT id, name FROM cron_jobs WHERE id = ${id}
|
|
704
|
+
`;
|
|
705
|
+
if (!existing) {
|
|
706
|
+
res.status(404).json({ error: "Cron job not found" });
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
await sql `UPDATE cron_jobs SET enabled = TRUE WHERE id = ${id}`;
|
|
710
|
+
const [updated] = await sql `
|
|
711
|
+
SELECT id, name, session_name, schedule, timezone, message, enabled, created_at, last_run
|
|
712
|
+
FROM cron_jobs WHERE id = ${id}
|
|
713
|
+
`;
|
|
714
|
+
if (updated) {
|
|
715
|
+
scheduler.enableCronJob(updated);
|
|
716
|
+
const nextRun = (() => {
|
|
717
|
+
try {
|
|
718
|
+
const options = {};
|
|
719
|
+
if (updated.timezone)
|
|
720
|
+
options.timezone = updated.timezone;
|
|
721
|
+
const cronJob = new CronerInstance(updated.schedule, options);
|
|
722
|
+
const next = cronJob.nextRun();
|
|
723
|
+
return next ? next.toISOString() : null;
|
|
724
|
+
}
|
|
725
|
+
catch {
|
|
726
|
+
return null;
|
|
727
|
+
}
|
|
728
|
+
})();
|
|
729
|
+
res.json({
|
|
730
|
+
...updated,
|
|
731
|
+
next_run: nextRun,
|
|
732
|
+
last_run: updated.last_run?.toISOString() ?? null,
|
|
733
|
+
created_at: updated.created_at.toISOString(),
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
else {
|
|
737
|
+
res.status(404).json({ error: "Cron job not found" });
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
catch (err) {
|
|
741
|
+
console.error("POST /crons/:id/enable error:", err);
|
|
742
|
+
res.status(500).json({ error: "Internal server error" });
|
|
743
|
+
}
|
|
744
|
+
});
|
|
745
|
+
router.post("/crons/:id/disable", async (req, res) => {
|
|
746
|
+
const id = parseInt(req.params.id, 10);
|
|
747
|
+
if (isNaN(id)) {
|
|
748
|
+
res.status(400).json({ error: "Invalid cron job id" });
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
try {
|
|
752
|
+
const [job] = await sql `
|
|
753
|
+
SELECT id, name FROM cron_jobs WHERE id = ${id}
|
|
754
|
+
`;
|
|
755
|
+
if (!job) {
|
|
756
|
+
res.status(404).json({ error: "Cron job not found" });
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
await sql `UPDATE cron_jobs SET enabled = FALSE WHERE id = ${id}`;
|
|
760
|
+
scheduler.disableCronJob(id);
|
|
761
|
+
const [updated] = await sql `
|
|
762
|
+
SELECT id, name, session_name, schedule, timezone, message, enabled, created_at, last_run
|
|
763
|
+
FROM cron_jobs WHERE id = ${id}
|
|
764
|
+
`;
|
|
765
|
+
res.json({
|
|
766
|
+
...updated,
|
|
767
|
+
next_run: null,
|
|
768
|
+
last_run: updated?.last_run?.toISOString() ?? null,
|
|
769
|
+
created_at: updated?.created_at.toISOString() ?? null,
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
catch (err) {
|
|
773
|
+
console.error("POST /crons/:id/disable error:", err);
|
|
774
|
+
res.status(500).json({ error: "Internal server error" });
|
|
775
|
+
}
|
|
776
|
+
});
|
|
777
|
+
router.get("/crons/:id/history", async (req, res) => {
|
|
778
|
+
const id = parseInt(req.params.id, 10);
|
|
779
|
+
const limit = Math.min(parseInt(req.query.limit ?? "10", 10), 100);
|
|
780
|
+
if (isNaN(id)) {
|
|
781
|
+
res.status(400).json({ error: "Invalid cron job id" });
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
try {
|
|
785
|
+
const rows = await sql `
|
|
786
|
+
SELECT id, cron_job_id, executed_at, success, error_message
|
|
787
|
+
FROM cron_history
|
|
788
|
+
WHERE cron_job_id = ${id}
|
|
789
|
+
ORDER BY executed_at DESC
|
|
790
|
+
LIMIT ${limit}
|
|
791
|
+
`;
|
|
792
|
+
res.json(rows.map((r) => ({
|
|
793
|
+
...r,
|
|
794
|
+
executed_at: r.executed_at.toISOString(),
|
|
795
|
+
})));
|
|
796
|
+
}
|
|
797
|
+
catch (err) {
|
|
798
|
+
console.error("GET /crons/:id/history error:", err);
|
|
799
|
+
res.status(500).json({ error: "Internal server error" });
|
|
800
|
+
}
|
|
801
|
+
});
|
|
368
802
|
return router;
|
|
369
803
|
}
|
|
370
804
|
export function createWorkerRouter(sql, opencode, serverUrl) {
|
|
@@ -385,11 +819,97 @@ export function createWorkerRouter(sql, opencode, serverUrl) {
|
|
|
385
819
|
return;
|
|
386
820
|
}
|
|
387
821
|
const session = rows[0];
|
|
822
|
+
const token = `${session.agent_code}@<server-url>`;
|
|
823
|
+
const name = session.name;
|
|
824
|
+
const humanConfig = await sql `SELECT value FROM config WHERE key = 'human_name'`;
|
|
825
|
+
const humanName = humanConfig[0]?.value ?? "your human manager";
|
|
826
|
+
const humanDescConfig = await sql `SELECT value FROM config WHERE key = 'human_description'`;
|
|
827
|
+
const humanDescription = humanDescConfig[0]?.value ?? "";
|
|
828
|
+
const message = [
|
|
829
|
+
`╔══════════════════════════════════════════════════════╗`,
|
|
830
|
+
`║ WELCOME TO THE AGENT OFFICE ║`,
|
|
831
|
+
`╚══════════════════════════════════════════════════════╝`,
|
|
832
|
+
``,
|
|
833
|
+
`You are now clocked in.`,
|
|
834
|
+
` Name: ${name}`,
|
|
835
|
+
...(session.mode ? [` Mode: ${session.mode}`] : []),
|
|
836
|
+
` Human manager: ${humanName} — the human who created your`,
|
|
837
|
+
` session, assigns your work, and is your`,
|
|
838
|
+
` primary point of contact for questions,`,
|
|
839
|
+
` updates, and decisions.`,
|
|
840
|
+
...(humanDescription ? [
|
|
841
|
+
` "${humanDescription}"`,
|
|
842
|
+
] : []),
|
|
843
|
+
``,
|
|
844
|
+
`The agent-office CLI is your PRIMARY means of communicating`,
|
|
845
|
+
`with your human manager (${humanName}) and your coworkers.`,
|
|
846
|
+
`Use it to send and receive messages, and to discover who`,
|
|
847
|
+
`else is working.`,
|
|
848
|
+
``,
|
|
849
|
+
`════════════════════════════════════════════════════════`,
|
|
850
|
+
` AVAILABLE COMMANDS`,
|
|
851
|
+
`════════════════════════════════════════════════════════`,
|
|
852
|
+
``,
|
|
853
|
+
` List your coworkers`,
|
|
854
|
+
` agent-office worker list-coworkers \\`,
|
|
855
|
+
` ${token}`,
|
|
856
|
+
``,
|
|
857
|
+
` Send a message to your manager or a coworker`,
|
|
858
|
+
` agent-office worker send-message \\`,
|
|
859
|
+
` --name <recipient-name> \\`,
|
|
860
|
+
` --body "Your message here" \\`,
|
|
861
|
+
` ${token}`,
|
|
862
|
+
``,
|
|
863
|
+
` Send a message to multiple recipients at once`,
|
|
864
|
+
` agent-office worker send-message \\`,
|
|
865
|
+
` --name alice --name bob \\`,
|
|
866
|
+
` --body "Your message here" \\`,
|
|
867
|
+
` ${token}`,
|
|
868
|
+
``,
|
|
869
|
+
` Manage scheduled tasks (optional)`,
|
|
870
|
+
` agent-office worker cron \\`,
|
|
871
|
+
` ${token}`,
|
|
872
|
+
``,
|
|
873
|
+
`════════════════════════════════════════════════════════`,
|
|
874
|
+
` ⚠ IMPORTANT: YOUR SESSIONS ARE PRIVATE`,
|
|
875
|
+
`════════════════════════════════════════════════════════`,
|
|
876
|
+
``,
|
|
877
|
+
` Nobody — not ${humanName}, not your coworkers — can see`,
|
|
878
|
+
` anything you think, reason, or write inside this session.`,
|
|
879
|
+
` Your work is completely private until you explicitly send`,
|
|
880
|
+
` a message using the send-message command.`,
|
|
881
|
+
``,
|
|
882
|
+
` This means:`,
|
|
883
|
+
` • You must send a message to report progress or ask`,
|
|
884
|
+
` for help. Silence looks like inaction.`,
|
|
885
|
+
` • Finishing a task without messaging ${humanName}`,
|
|
886
|
+
` means they will never know it is done.`,
|
|
887
|
+
` • If you are blocked or uncertain, send a message —`,
|
|
888
|
+
` nobody will know otherwise.`,
|
|
889
|
+
``,
|
|
890
|
+
`════════════════════════════════════════════════════════`,
|
|
891
|
+
` TIPS`,
|
|
892
|
+
`════════════════════════════════════════════════════════`,
|
|
893
|
+
``,
|
|
894
|
+
` - Run list-coworkers to discover who is available and`,
|
|
895
|
+
` what their names are before sending messages.`,
|
|
896
|
+
` - Messages you send are delivered directly into the`,
|
|
897
|
+
` recipient's active session — they will see them`,
|
|
898
|
+
` immediately.`,
|
|
899
|
+
` - Your human manager is ${humanName}. They can send you`,
|
|
900
|
+
` messages at any time and those will appear here in`,
|
|
901
|
+
` your session just like this one. You can reach them`,
|
|
902
|
+
` by sending a message to --name ${humanName}.`,
|
|
903
|
+
` - Optional: Set up recurring scheduled tasks with cron`,
|
|
904
|
+
` jobs. Run 'agent-office worker cron list ${token}' to`,
|
|
905
|
+
` get started.`,
|
|
906
|
+
``,
|
|
907
|
+
].join("\n");
|
|
388
908
|
res.json({
|
|
389
909
|
ok: true,
|
|
390
910
|
name: session.name,
|
|
391
911
|
session_id: session.session_id,
|
|
392
|
-
message
|
|
912
|
+
message,
|
|
393
913
|
});
|
|
394
914
|
});
|
|
395
915
|
router.get("/worker/list-coworkers", async (req, res) => {
|
|
@@ -421,6 +941,42 @@ export function createWorkerRouter(sql, opencode, serverUrl) {
|
|
|
421
941
|
res.status(500).json({ error: "Internal server error" });
|
|
422
942
|
}
|
|
423
943
|
});
|
|
944
|
+
router.post("/worker/set-status", async (req, res) => {
|
|
945
|
+
const { code } = req.query;
|
|
946
|
+
const { status } = req.body;
|
|
947
|
+
if (!code || typeof code !== "string") {
|
|
948
|
+
res.status(400).json({ error: "code query parameter is required" });
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
const trimmedStatus = status === null ? null : (status === undefined ? null : status.trim());
|
|
952
|
+
if (trimmedStatus !== null && trimmedStatus.length > 140) {
|
|
953
|
+
res.status(400).json({ error: "status must be at most 140 characters" });
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
const rows = await sql `
|
|
957
|
+
SELECT id, name, session_id, agent_code, mode, status, created_at
|
|
958
|
+
FROM sessions
|
|
959
|
+
WHERE agent_code = ${code}
|
|
960
|
+
`;
|
|
961
|
+
if (rows.length === 0) {
|
|
962
|
+
res.status(401).json({ error: "Invalid agent code" });
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
const session = rows[0];
|
|
966
|
+
try {
|
|
967
|
+
await sql `
|
|
968
|
+
UPDATE sessions
|
|
969
|
+
SET status = ${trimmedStatus}
|
|
970
|
+
WHERE agent_code = ${code}
|
|
971
|
+
`;
|
|
972
|
+
}
|
|
973
|
+
catch (err) {
|
|
974
|
+
console.error("POST /worker/set-status error:", err);
|
|
975
|
+
res.status(500).json({ error: "Internal server error" });
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
res.json({ ok: true, name: session.name, status: trimmedStatus });
|
|
979
|
+
});
|
|
424
980
|
router.post("/worker/send-message", async (req, res) => {
|
|
425
981
|
const { code } = req.query;
|
|
426
982
|
const { to, body } = req.body;
|
|
@@ -474,27 +1030,345 @@ export function createWorkerRouter(sql, opencode, serverUrl) {
|
|
|
474
1030
|
`;
|
|
475
1031
|
if (sessionMap.has(recipient)) {
|
|
476
1032
|
const recipientSessionId = sessionMap.get(recipient);
|
|
477
|
-
|
|
478
|
-
|
|
1033
|
+
const msgId = msgRow.id;
|
|
1034
|
+
injected = true;
|
|
1035
|
+
opencode.app.providers().then((providers) => {
|
|
479
1036
|
const defaultEntry = Object.entries(providers.default)[0];
|
|
480
|
-
if (defaultEntry)
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
catch (err) {
|
|
1037
|
+
if (!defaultEntry)
|
|
1038
|
+
return;
|
|
1039
|
+
const injectText = `[Message from "${session.name}"]: ${trimmedBody}${MAIL_INJECTION_BLURB}`;
|
|
1040
|
+
return opencode.session.chat(recipientSessionId, {
|
|
1041
|
+
modelID: defaultEntry[0],
|
|
1042
|
+
providerID: defaultEntry[1],
|
|
1043
|
+
parts: [{ type: "text", text: injectText }],
|
|
1044
|
+
}).then(() => sql `UPDATE messages SET injected = TRUE WHERE id = ${msgId}`);
|
|
1045
|
+
}).catch((err) => {
|
|
492
1046
|
console.warn(`Warning: could not inject message into session ${recipient}:`, err);
|
|
493
|
-
}
|
|
1047
|
+
});
|
|
494
1048
|
}
|
|
495
1049
|
results.push({ to: recipient, messageId: msgRow.id, injected });
|
|
496
1050
|
}
|
|
497
1051
|
res.status(201).json({ ok: true, results });
|
|
498
1052
|
});
|
|
1053
|
+
// ── Worker Cron Endpoints (authenticated via agent_code in query) ───────────
|
|
1054
|
+
router.get("/worker/crons", async (req, res) => {
|
|
1055
|
+
const { code } = req.query;
|
|
1056
|
+
if (!code || typeof code !== "string") {
|
|
1057
|
+
res.status(400).json({ error: "code query parameter is required" });
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
const session = await sql `
|
|
1061
|
+
SELECT name FROM sessions WHERE agent_code = ${code}
|
|
1062
|
+
`;
|
|
1063
|
+
if (session.length === 0) {
|
|
1064
|
+
res.status(401).json({ error: "Invalid agent code" });
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
const sessionName = session[0].name;
|
|
1068
|
+
try {
|
|
1069
|
+
const rows = await sql `
|
|
1070
|
+
SELECT id, name, session_name, schedule, timezone, message, enabled, created_at, last_run
|
|
1071
|
+
FROM cron_jobs
|
|
1072
|
+
WHERE session_name = ${sessionName}
|
|
1073
|
+
ORDER BY name
|
|
1074
|
+
`;
|
|
1075
|
+
const jobs = rows.map((job) => {
|
|
1076
|
+
let nextRun = null;
|
|
1077
|
+
if (job.enabled) {
|
|
1078
|
+
try {
|
|
1079
|
+
const options = {};
|
|
1080
|
+
if (job.timezone)
|
|
1081
|
+
options.timezone = job.timezone;
|
|
1082
|
+
const cronJob = new CronerInstance(job.schedule, options);
|
|
1083
|
+
const next = cronJob.nextRun();
|
|
1084
|
+
nextRun = next ? next.toISOString() : null;
|
|
1085
|
+
}
|
|
1086
|
+
catch { }
|
|
1087
|
+
}
|
|
1088
|
+
return {
|
|
1089
|
+
...job,
|
|
1090
|
+
next_run: nextRun,
|
|
1091
|
+
last_run: job.last_run ? job.last_run.toISOString() : null,
|
|
1092
|
+
created_at: job.created_at.toISOString(),
|
|
1093
|
+
};
|
|
1094
|
+
});
|
|
1095
|
+
res.json(jobs);
|
|
1096
|
+
}
|
|
1097
|
+
catch (err) {
|
|
1098
|
+
console.error("GET /worker/crons error:", err);
|
|
1099
|
+
res.status(500).json({ error: "Internal server error" });
|
|
1100
|
+
}
|
|
1101
|
+
});
|
|
1102
|
+
router.post("/worker/crons", async (req, res) => {
|
|
1103
|
+
const { code } = req.query;
|
|
1104
|
+
const { name, schedule, message, timezone } = req.body;
|
|
1105
|
+
if (!code || typeof code !== "string") {
|
|
1106
|
+
res.status(400).json({ error: "code query parameter is required" });
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
1109
|
+
const session = await sql `
|
|
1110
|
+
SELECT name FROM sessions WHERE agent_code = ${code}
|
|
1111
|
+
`;
|
|
1112
|
+
if (session.length === 0) {
|
|
1113
|
+
res.status(401).json({ error: "Invalid agent code" });
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1116
|
+
if (!name || typeof name !== "string" || !name.trim()) {
|
|
1117
|
+
res.status(400).json({ error: "name is required" });
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1120
|
+
if (!schedule || typeof schedule !== "string" || !schedule.trim()) {
|
|
1121
|
+
res.status(400).json({ error: "schedule is required" });
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
if (!message || typeof message !== "string" || !message.trim()) {
|
|
1125
|
+
res.status(400).json({ error: "message is required" });
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1128
|
+
const trimmedName = name.trim();
|
|
1129
|
+
const trimmedSchedule = schedule.trim();
|
|
1130
|
+
const trimmedMessage = message.trim();
|
|
1131
|
+
const sessionName = session[0].name;
|
|
1132
|
+
try {
|
|
1133
|
+
new CronerInstance(trimmedSchedule);
|
|
1134
|
+
}
|
|
1135
|
+
catch {
|
|
1136
|
+
res.status(400).json({ error: "Invalid cron schedule expression" });
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
if (timezone) {
|
|
1140
|
+
try {
|
|
1141
|
+
new CronerInstance("0 0 * * *", { timezone });
|
|
1142
|
+
}
|
|
1143
|
+
catch {
|
|
1144
|
+
res.status(400).json({ error: "Invalid timezone" });
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
const [existing] = await sql `
|
|
1149
|
+
SELECT id FROM cron_jobs WHERE name = ${trimmedName} AND session_name = ${sessionName}
|
|
1150
|
+
`;
|
|
1151
|
+
if (existing) {
|
|
1152
|
+
res.status(409).json({ error: `Cron job "${trimmedName}" already exists` });
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
try {
|
|
1156
|
+
const [inserted] = await sql `
|
|
1157
|
+
INSERT INTO cron_jobs (name, session_name, schedule, timezone, message)
|
|
1158
|
+
VALUES (${trimmedName}, ${sessionName}, ${trimmedSchedule}, ${timezone ?? null}, ${trimmedMessage})
|
|
1159
|
+
RETURNING id, name, session_name, schedule, timezone, message, enabled, created_at, last_run
|
|
1160
|
+
`;
|
|
1161
|
+
const cronJobrow = inserted;
|
|
1162
|
+
const nextRun = cronJobrow.enabled ? (() => {
|
|
1163
|
+
try {
|
|
1164
|
+
const options = {};
|
|
1165
|
+
if (cronJobrow.timezone)
|
|
1166
|
+
options.timezone = cronJobrow.timezone;
|
|
1167
|
+
const cronJob = new CronerInstance(cronJobrow.schedule, options);
|
|
1168
|
+
const next = cronJob.nextRun();
|
|
1169
|
+
return next ? next.toISOString() : null;
|
|
1170
|
+
}
|
|
1171
|
+
catch {
|
|
1172
|
+
return null;
|
|
1173
|
+
}
|
|
1174
|
+
})() : null;
|
|
1175
|
+
res.status(201).json({
|
|
1176
|
+
...cronJobrow,
|
|
1177
|
+
next_run: nextRun,
|
|
1178
|
+
last_run: cronJobrow?.last_run?.toISOString() ?? null,
|
|
1179
|
+
created_at: cronJobrow.created_at.toISOString(),
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
catch (err) {
|
|
1183
|
+
console.error("POST /worker/crons error:", err);
|
|
1184
|
+
res.status(500).json({ error: "Internal server error" });
|
|
1185
|
+
}
|
|
1186
|
+
});
|
|
1187
|
+
router.delete("/worker/crons/:id", async (req, res) => {
|
|
1188
|
+
const { code } = req.query;
|
|
1189
|
+
const id = parseInt(req.params.id, 10);
|
|
1190
|
+
if (!code || typeof code !== "string") {
|
|
1191
|
+
res.status(400).json({ error: "code query parameter is required" });
|
|
1192
|
+
return;
|
|
1193
|
+
}
|
|
1194
|
+
if (isNaN(id)) {
|
|
1195
|
+
res.status(400).json({ error: "Invalid cron job id" });
|
|
1196
|
+
return;
|
|
1197
|
+
}
|
|
1198
|
+
const session = await sql `
|
|
1199
|
+
SELECT name FROM sessions WHERE agent_code = ${code}
|
|
1200
|
+
`;
|
|
1201
|
+
if (session.length === 0) {
|
|
1202
|
+
res.status(401).json({ error: "Invalid agent code" });
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1205
|
+
const sessionName = session[0].name;
|
|
1206
|
+
try {
|
|
1207
|
+
const [job] = await sql `
|
|
1208
|
+
SELECT id, name FROM cron_jobs WHERE id = ${id} AND session_name = ${sessionName}
|
|
1209
|
+
`;
|
|
1210
|
+
if (!job) {
|
|
1211
|
+
res.status(404).json({ error: "Cron job not found or not owned by you" });
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
await sql `DELETE FROM cron_jobs WHERE id = ${id}`;
|
|
1215
|
+
res.json({ deleted: true, id, name: job.name });
|
|
1216
|
+
}
|
|
1217
|
+
catch (err) {
|
|
1218
|
+
console.error("DELETE /worker/crons/:id error:", err);
|
|
1219
|
+
res.status(500).json({ error: "Internal server error" });
|
|
1220
|
+
}
|
|
1221
|
+
});
|
|
1222
|
+
router.post("/worker/crons/:id/enable", async (req, res) => {
|
|
1223
|
+
const { code } = req.query;
|
|
1224
|
+
const id = parseInt(req.params.id, 10);
|
|
1225
|
+
if (!code || typeof code !== "string") {
|
|
1226
|
+
res.status(400).json({ error: "code query parameter is required" });
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1229
|
+
if (isNaN(id)) {
|
|
1230
|
+
res.status(400).json({ error: "Invalid cron job id" });
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
1233
|
+
const session = await sql `
|
|
1234
|
+
SELECT name FROM sessions WHERE agent_code = ${code}
|
|
1235
|
+
`;
|
|
1236
|
+
if (session.length === 0) {
|
|
1237
|
+
res.status(401).json({ error: "Invalid agent code" });
|
|
1238
|
+
return;
|
|
1239
|
+
}
|
|
1240
|
+
const sessionName = session[0].name;
|
|
1241
|
+
try {
|
|
1242
|
+
const [existing] = await sql `
|
|
1243
|
+
SELECT id FROM cron_jobs WHERE id = ${id} AND session_name = ${sessionName}
|
|
1244
|
+
`;
|
|
1245
|
+
if (!existing) {
|
|
1246
|
+
res.status(404).json({ error: "Cron job not found or not owned by you" });
|
|
1247
|
+
return;
|
|
1248
|
+
}
|
|
1249
|
+
await sql `UPDATE cron_jobs SET enabled = TRUE WHERE id = ${id}`;
|
|
1250
|
+
const [updated] = await sql `
|
|
1251
|
+
SELECT id, name, session_name, schedule, timezone, message, enabled, created_at, last_run
|
|
1252
|
+
FROM cron_jobs WHERE id = ${id}
|
|
1253
|
+
`;
|
|
1254
|
+
if (updated) {
|
|
1255
|
+
const nextRun = (() => {
|
|
1256
|
+
try {
|
|
1257
|
+
const options = {};
|
|
1258
|
+
if (updated.timezone)
|
|
1259
|
+
options.timezone = updated.timezone;
|
|
1260
|
+
const cronJob = new CronerInstance(updated.schedule, options);
|
|
1261
|
+
const next = cronJob.nextRun();
|
|
1262
|
+
return next ? next.toISOString() : null;
|
|
1263
|
+
}
|
|
1264
|
+
catch {
|
|
1265
|
+
return null;
|
|
1266
|
+
}
|
|
1267
|
+
})();
|
|
1268
|
+
res.json({
|
|
1269
|
+
...updated,
|
|
1270
|
+
next_run: nextRun,
|
|
1271
|
+
last_run: updated.last_run?.toISOString() ?? null,
|
|
1272
|
+
created_at: updated.created_at.toISOString(),
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
1275
|
+
else {
|
|
1276
|
+
res.status(404).json({ error: "Cron job not found" });
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
catch (err) {
|
|
1280
|
+
console.error("POST /worker/crons/:id/enable error:", err);
|
|
1281
|
+
res.status(500).json({ error: "Internal server error" });
|
|
1282
|
+
}
|
|
1283
|
+
});
|
|
1284
|
+
router.post("/worker/crons/:id/disable", async (req, res) => {
|
|
1285
|
+
const { code } = req.query;
|
|
1286
|
+
const id = parseInt(req.params.id, 10);
|
|
1287
|
+
if (!code || typeof code !== "string") {
|
|
1288
|
+
res.status(400).json({ error: "code query parameter is required" });
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
1291
|
+
if (isNaN(id)) {
|
|
1292
|
+
res.status(400).json({ error: "Invalid cron job id" });
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
1295
|
+
const session = await sql `
|
|
1296
|
+
SELECT name FROM sessions WHERE agent_code = ${code}
|
|
1297
|
+
`;
|
|
1298
|
+
if (session.length === 0) {
|
|
1299
|
+
res.status(401).json({ error: "Invalid agent code" });
|
|
1300
|
+
return;
|
|
1301
|
+
}
|
|
1302
|
+
const sessionName = session[0].name;
|
|
1303
|
+
try {
|
|
1304
|
+
const [job] = await sql `
|
|
1305
|
+
SELECT id FROM cron_jobs WHERE id = ${id} AND session_name = ${sessionName}
|
|
1306
|
+
`;
|
|
1307
|
+
if (!job) {
|
|
1308
|
+
res.status(404).json({ error: "Cron job not found or not owned by you" });
|
|
1309
|
+
return;
|
|
1310
|
+
}
|
|
1311
|
+
await sql `UPDATE cron_jobs SET enabled = FALSE WHERE id = ${id}`;
|
|
1312
|
+
const [updated] = await sql `
|
|
1313
|
+
SELECT id, name, session_name, schedule, timezone, message, enabled, created_at, last_run
|
|
1314
|
+
FROM cron_jobs WHERE id = ${id}
|
|
1315
|
+
`;
|
|
1316
|
+
res.json({
|
|
1317
|
+
...updated,
|
|
1318
|
+
next_run: null,
|
|
1319
|
+
last_run: updated?.last_run?.toISOString() ?? null,
|
|
1320
|
+
created_at: updated?.created_at.toISOString() ?? null,
|
|
1321
|
+
});
|
|
1322
|
+
}
|
|
1323
|
+
catch (err) {
|
|
1324
|
+
console.error("POST /worker/crons/:id/disable error:", err);
|
|
1325
|
+
res.status(500).json({ error: "Internal server error" });
|
|
1326
|
+
}
|
|
1327
|
+
});
|
|
1328
|
+
router.get("/worker/crons/:id/history", async (req, res) => {
|
|
1329
|
+
const { code } = req.query;
|
|
1330
|
+
const id = parseInt(req.params.id, 10);
|
|
1331
|
+
const limit = Math.min(parseInt(req.query.limit ?? "10", 10), 100);
|
|
1332
|
+
if (!code || typeof code !== "string") {
|
|
1333
|
+
res.status(400).json({ error: "code query parameter is required" });
|
|
1334
|
+
return;
|
|
1335
|
+
}
|
|
1336
|
+
if (isNaN(id)) {
|
|
1337
|
+
res.status(400).json({ error: "Invalid cron job id" });
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1340
|
+
const session = await sql `
|
|
1341
|
+
SELECT name FROM sessions WHERE agent_code = ${code}
|
|
1342
|
+
`;
|
|
1343
|
+
if (session.length === 0) {
|
|
1344
|
+
res.status(401).json({ error: "Invalid agent code" });
|
|
1345
|
+
return;
|
|
1346
|
+
}
|
|
1347
|
+
const sessionName = session[0].name;
|
|
1348
|
+
try {
|
|
1349
|
+
const [job] = await sql `
|
|
1350
|
+
SELECT id FROM cron_jobs WHERE id = ${id} AND session_name = ${sessionName}
|
|
1351
|
+
`;
|
|
1352
|
+
if (!job) {
|
|
1353
|
+
res.status(404).json({ error: "Cron job not found or not owned by you" });
|
|
1354
|
+
return;
|
|
1355
|
+
}
|
|
1356
|
+
const rows = await sql `
|
|
1357
|
+
SELECT id, cron_job_id, executed_at, success, error_message
|
|
1358
|
+
FROM cron_history
|
|
1359
|
+
WHERE cron_job_id = ${id}
|
|
1360
|
+
ORDER BY executed_at DESC
|
|
1361
|
+
LIMIT ${limit}
|
|
1362
|
+
`;
|
|
1363
|
+
res.json(rows.map((r) => ({
|
|
1364
|
+
...r,
|
|
1365
|
+
executed_at: r.executed_at.toISOString(),
|
|
1366
|
+
})));
|
|
1367
|
+
}
|
|
1368
|
+
catch (err) {
|
|
1369
|
+
console.error("GET /worker/crons/:id/history error:", err);
|
|
1370
|
+
res.status(500).json({ error: "Internal server error" });
|
|
1371
|
+
}
|
|
1372
|
+
});
|
|
499
1373
|
return router;
|
|
500
1374
|
}
|