agent-office 0.5.0 → 0.6.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.
Files changed (189) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +259 -228
  3. package/dist/commands/cron-requests.d.ts +7 -0
  4. package/dist/commands/cron-requests.d.ts.map +1 -0
  5. package/dist/commands/cron-requests.js +31 -0
  6. package/dist/commands/cron-requests.js.map +1 -0
  7. package/dist/commands/crons.d.ts +10 -0
  8. package/dist/commands/crons.d.ts.map +1 -0
  9. package/dist/commands/crons.js +45 -0
  10. package/dist/commands/crons.js.map +1 -0
  11. package/dist/commands/hello.d.ts +5 -0
  12. package/dist/commands/hello.d.ts.map +1 -0
  13. package/dist/commands/hello.js +4 -0
  14. package/dist/commands/hello.js.map +1 -0
  15. package/dist/commands/messages.d.ts +5 -0
  16. package/dist/commands/messages.d.ts.map +1 -0
  17. package/dist/commands/messages.js +18 -0
  18. package/dist/commands/messages.js.map +1 -0
  19. package/dist/commands/sessions.d.ts +13 -0
  20. package/dist/commands/sessions.d.ts.map +1 -0
  21. package/dist/commands/sessions.js +58 -0
  22. package/dist/commands/sessions.js.map +1 -0
  23. package/dist/commands/task-columns.d.ts +2 -0
  24. package/dist/commands/task-columns.d.ts.map +1 -0
  25. package/dist/commands/task-columns.js +13 -0
  26. package/dist/commands/task-columns.js.map +1 -0
  27. package/dist/commands/tasks.d.ts +11 -0
  28. package/dist/commands/tasks.d.ts.map +1 -0
  29. package/dist/commands/tasks.js +75 -0
  30. package/dist/commands/tasks.js.map +1 -0
  31. package/dist/config.test.d.ts +2 -0
  32. package/dist/config.test.d.ts.map +1 -0
  33. package/dist/config.test.js +50 -0
  34. package/dist/config.test.js.map +1 -0
  35. package/dist/db/index.d.ts +6 -70
  36. package/dist/db/index.d.ts.map +1 -0
  37. package/dist/db/index.js +4 -11
  38. package/dist/db/index.js.map +1 -0
  39. package/dist/db/mock-storage.d.ts +79 -0
  40. package/dist/db/mock-storage.d.ts.map +1 -0
  41. package/dist/db/mock-storage.js +381 -0
  42. package/dist/db/mock-storage.js.map +1 -0
  43. package/dist/db/mock-storage.test.d.ts +2 -0
  44. package/dist/db/mock-storage.test.d.ts.map +1 -0
  45. package/dist/db/mock-storage.test.js +234 -0
  46. package/dist/db/mock-storage.test.js.map +1 -0
  47. package/dist/db/postgresql-storage.d.ts +10 -8
  48. package/dist/db/postgresql-storage.d.ts.map +1 -0
  49. package/dist/db/postgresql-storage.js +76 -42
  50. package/dist/db/postgresql-storage.js.map +1 -0
  51. package/dist/db/sqlite-storage.d.ts +9 -8
  52. package/dist/db/sqlite-storage.d.ts.map +1 -0
  53. package/dist/db/sqlite-storage.js +75 -41
  54. package/dist/db/sqlite-storage.js.map +1 -0
  55. package/dist/db/storage-base.d.ts +7 -8
  56. package/dist/db/storage-base.d.ts.map +1 -0
  57. package/dist/db/storage-base.js +3 -2
  58. package/dist/db/storage-base.js.map +1 -0
  59. package/dist/db/storage.d.ts +12 -12
  60. package/dist/db/storage.d.ts.map +1 -0
  61. package/dist/db/storage.js +1 -0
  62. package/dist/db/storage.js.map +1 -0
  63. package/dist/db/types.d.ts +67 -0
  64. package/dist/db/types.d.ts.map +1 -0
  65. package/dist/db/types.js +2 -0
  66. package/dist/db/types.js.map +1 -0
  67. package/dist/index.d.ts +2 -0
  68. package/dist/index.d.ts.map +1 -0
  69. package/dist/index.js +397 -0
  70. package/dist/index.js.map +1 -0
  71. package/dist/index.test.d.ts +2 -0
  72. package/dist/index.test.d.ts.map +1 -0
  73. package/dist/index.test.js +49 -0
  74. package/dist/index.test.js.map +1 -0
  75. package/dist/lib/output.d.ts +2 -0
  76. package/dist/lib/output.d.ts.map +1 -0
  77. package/dist/lib/output.js +8 -0
  78. package/dist/lib/output.js.map +1 -0
  79. package/dist/services/cron-service.constraints.test.d.ts +2 -0
  80. package/dist/services/cron-service.constraints.test.d.ts.map +1 -0
  81. package/dist/services/cron-service.constraints.test.js +90 -0
  82. package/dist/services/cron-service.constraints.test.js.map +1 -0
  83. package/dist/services/cron-service.d.ts +45 -0
  84. package/dist/services/cron-service.d.ts.map +1 -0
  85. package/dist/services/cron-service.js +157 -0
  86. package/dist/services/cron-service.js.map +1 -0
  87. package/dist/services/cron-service.test.d.ts +2 -0
  88. package/dist/services/cron-service.test.d.ts.map +1 -0
  89. package/dist/services/cron-service.test.js +280 -0
  90. package/dist/services/cron-service.test.js.map +1 -0
  91. package/dist/services/index.d.ts +5 -0
  92. package/dist/services/index.d.ts.map +1 -0
  93. package/dist/services/index.js +5 -0
  94. package/dist/services/index.js.map +1 -0
  95. package/dist/services/message-service.d.ts +16 -0
  96. package/dist/services/message-service.d.ts.map +1 -0
  97. package/dist/services/message-service.js +39 -0
  98. package/dist/services/message-service.js.map +1 -0
  99. package/dist/services/message-service.test.d.ts +2 -0
  100. package/dist/services/message-service.test.d.ts.map +1 -0
  101. package/dist/services/message-service.test.js +145 -0
  102. package/dist/services/message-service.test.js.map +1 -0
  103. package/dist/services/session-service.constraints.test.d.ts +2 -0
  104. package/dist/services/session-service.constraints.test.d.ts.map +1 -0
  105. package/dist/services/session-service.constraints.test.js +34 -0
  106. package/dist/services/session-service.constraints.test.js.map +1 -0
  107. package/dist/services/session-service.d.ts +27 -0
  108. package/dist/services/session-service.d.ts.map +1 -0
  109. package/dist/services/session-service.js +55 -0
  110. package/dist/services/session-service.js.map +1 -0
  111. package/dist/services/session-service.test.d.ts +2 -0
  112. package/dist/services/session-service.test.d.ts.map +1 -0
  113. package/dist/services/session-service.test.js +87 -0
  114. package/dist/services/session-service.test.js.map +1 -0
  115. package/dist/services/task-service.d.ts +25 -0
  116. package/dist/services/task-service.d.ts.map +1 -0
  117. package/dist/services/task-service.js +87 -0
  118. package/dist/services/task-service.js.map +1 -0
  119. package/dist/services/task-service.test.d.ts +2 -0
  120. package/dist/services/task-service.test.d.ts.map +1 -0
  121. package/dist/services/task-service.test.js +180 -0
  122. package/dist/services/task-service.test.js.map +1 -0
  123. package/package.json +41 -42
  124. package/dist/cli.d.ts +0 -2
  125. package/dist/cli.js +0 -317
  126. package/dist/commands/communicator.d.ts +0 -9
  127. package/dist/commands/communicator.js +0 -2232
  128. package/dist/commands/manage.d.ts +0 -5
  129. package/dist/commands/manage.js +0 -20
  130. package/dist/commands/notifier.d.ts +0 -11
  131. package/dist/commands/notifier.js +0 -100
  132. package/dist/commands/screensaver.d.ts +0 -8
  133. package/dist/commands/screensaver.js +0 -1280
  134. package/dist/commands/serve.d.ts +0 -13
  135. package/dist/commands/serve.js +0 -95
  136. package/dist/commands/task-board.d.ts +0 -29
  137. package/dist/commands/task-board.js +0 -251
  138. package/dist/commands/worker.d.ts +0 -16
  139. package/dist/commands/worker.js +0 -145
  140. package/dist/db/migrate.d.ts +0 -2
  141. package/dist/db/migrate.js +0 -3
  142. package/dist/lib/agentic-coding-server.d.ts +0 -66
  143. package/dist/lib/agentic-coding-server.js +0 -7
  144. package/dist/lib/notifier.d.ts +0 -18
  145. package/dist/lib/notifier.js +0 -15
  146. package/dist/lib/opencode-coding-server.d.ts +0 -11
  147. package/dist/lib/opencode-coding-server.js +0 -66
  148. package/dist/lib/pi-coding-server.d.ts +0 -20
  149. package/dist/lib/pi-coding-server.js +0 -162
  150. package/dist/manage/app.d.ts +0 -6
  151. package/dist/manage/app.js +0 -128
  152. package/dist/manage/components/AgentCode.d.ts +0 -8
  153. package/dist/manage/components/AgentCode.js +0 -73
  154. package/dist/manage/components/CreateSession.d.ts +0 -8
  155. package/dist/manage/components/CreateSession.js +0 -37
  156. package/dist/manage/components/CronList.d.ts +0 -9
  157. package/dist/manage/components/CronList.js +0 -321
  158. package/dist/manage/components/CronRequests.d.ts +0 -8
  159. package/dist/manage/components/CronRequests.js +0 -181
  160. package/dist/manage/components/DeleteSession.d.ts +0 -7
  161. package/dist/manage/components/DeleteSession.js +0 -55
  162. package/dist/manage/components/InjectText.d.ts +0 -8
  163. package/dist/manage/components/InjectText.js +0 -51
  164. package/dist/manage/components/ItemSelector.d.ts +0 -7
  165. package/dist/manage/components/ItemSelector.js +0 -20
  166. package/dist/manage/components/MenuSelect.d.ts +0 -13
  167. package/dist/manage/components/MenuSelect.js +0 -22
  168. package/dist/manage/components/MyMail.d.ts +0 -9
  169. package/dist/manage/components/MyMail.js +0 -143
  170. package/dist/manage/components/Profile.d.ts +0 -8
  171. package/dist/manage/components/Profile.js +0 -60
  172. package/dist/manage/components/ReadMail.d.ts +0 -8
  173. package/dist/manage/components/ReadMail.js +0 -110
  174. package/dist/manage/components/SendMessage.d.ts +0 -9
  175. package/dist/manage/components/SendMessage.js +0 -79
  176. package/dist/manage/components/SessionList.d.ts +0 -9
  177. package/dist/manage/components/SessionList.js +0 -608
  178. package/dist/manage/components/SessionSidebar.d.ts +0 -6
  179. package/dist/manage/components/SessionSidebar.js +0 -24
  180. package/dist/manage/components/TailMessages.d.ts +0 -8
  181. package/dist/manage/components/TailMessages.js +0 -126
  182. package/dist/manage/hooks/useApi.d.ts +0 -147
  183. package/dist/manage/hooks/useApi.js +0 -181
  184. package/dist/server/cron.d.ts +0 -25
  185. package/dist/server/cron.js +0 -107
  186. package/dist/server/index.d.ts +0 -4
  187. package/dist/server/index.js +0 -22
  188. package/dist/server/routes.d.ts +0 -13
  189. package/dist/server/routes.js +0 -1396
@@ -1,1396 +0,0 @@
1
- import { Router } from "express";
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
- ``,
8
- `When responding to the sender:`,
9
- `- Use the \`agent-office worker send-message\` tool so they can see your reply`,
10
- `- Avoid excessive length - keep responses concise`,
11
- `- Feel free to use markdown formatting`,
12
- `- IMPORTANT: Remember when using bash commands certain characters (like dollar signs) may need to be escaped or wrapped in quotes`,
13
- ].join("\n");
14
- /**
15
- * Build the persistent system-prompt briefing for a worker session.
16
- * This is injected as the `system` field on every `promptAsync` call so the
17
- * agent always has its identity, token, and command reference in context —
18
- * without consuming a user-message turn.
19
- */
20
- export function generateSystemPrompt(name, status, humanName, humanDescription, token) {
21
- return [
22
- `╔══════════════════════════════════════════════════════╗`,
23
- `║ WELCOME TO THE AGENT OFFICE ║`,
24
- `╚══════════════════════════════════════════════════════╝`,
25
- ``,
26
- `You are an AI worker agent enrolled in the agent office.`,
27
- ` Name: ${name}`,
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. You have a special token that you use to use the CLI, and it's important you DO NOT SHARE THIS with other coworkers because it represents you`,
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
- ` If your message contains $ characters, escape them to avoid shell`,
73
- ` variable expansion (bash expands unescaped $ before the CLI sees it):`,
74
- ` agent-office worker send-message \\`,
75
- ` --name alice \\`,
76
- ` --body "The total cost is \\$42" \\`,
77
- ` ${token}`,
78
- ``,
79
- ` ⚠ Always escape $ in --body when using double quotes in bash,`,
80
- ` or switch to single quotes. Unescaped $ will be silently`,
81
- ` expanded by the shell and your message will be garbled.`,
82
- ``,
83
- ` Manage scheduled tasks (optional)`,
84
- ` agent-office worker cron list \\`,
85
- ` ${token}`,
86
- ``,
87
- ` Request a cron job (requires human approval)`,
88
- ` agent-office worker cron request \\`,
89
- ` --name <job-name> \\`,
90
- ` --schedule "<cron-expression>" \\`,
91
- ` --message "<action-to-perform>" \\`,
92
- ` --respond-to "<who-to-respond-to-when-done>" \\`,
93
- ` ${token}`,
94
- ``,
95
- ` Example: Daily standup reminder at 9am`,
96
- ` --schedule "0 9 * * *" \\`,
97
- ` --message "Prepare your standup update" \\`,
98
- ` --respond-to "${humanName} in the standup channel"`,
99
- ``,
100
- ` NOTE: Cron requests must be approved by ${humanName}`,
101
- ` before they become active. You will be notified when`,
102
- ` your request is approved or rejected.`,
103
- ``,
104
- ` Check the status of your cron requests`,
105
- ` agent-office worker cron requests \\`,
106
- ` ${token}`,
107
- ``,
108
- `════════════════════════════════════════════════════════`,
109
- ` IMPORTANT: BASH ESCAPING IN COMMAND ARGUMENTS`,
110
- `════════════════════════════════════════════════════════`,
111
- ``,
112
- ` The shell processes --body, --message, and --respond-to`,
113
- ` before the CLI receives them. Be mindful of bash special`,
114
- ` characters in these values:`,
115
- ` $var, \${x} variable expansion → escape as \\$ or use single quotes`,
116
- ` \`cmd\`, $(x) command substitution → escape backticks or use single quotes`,
117
- ` !, \\, " history/escape chars → escape or use single quotes`,
118
- ` The safest option is always single quotes:`,
119
- ` --body 'your message here'`,
120
- ` Single quotes prevent all shell interpretation.`,
121
- ``,
122
- `════════════════════════════════════════════════════════`,
123
- ` IMPORTANT: YOUR SESSIONS ARE PRIVATE`,
124
- `════════════════════════════════════════════════════════`,
125
- ``,
126
- ` Nobody — not ${humanName}, not your coworkers — can see`,
127
- ` anything you think, reason, or write inside this session.`,
128
- ` Your work is completely private until you explicitly send`,
129
- ` a message using the send-message command.`,
130
- ``,
131
- ` This means:`,
132
- ` • You must send a message to report progress or ask`,
133
- ` for help. Silence looks like inaction.`,
134
- ` • Finishing a task without messaging ${humanName}`,
135
- ` means they will never know it is done.`,
136
- ` • If you are blocked or uncertain, send a message —`,
137
- ` nobody will know otherwise.`,
138
- ``,
139
- `════════════════════════════════════════════════════════`,
140
- ` TIPS`,
141
- `════════════════════════════════════════════════════════`,
142
- ``,
143
- ` - Run list-coworkers to discover who is available and`,
144
- ` what their names are before sending messages.`,
145
- ` - Messages you send are delivered directly into the`,
146
- ` recipient's active session — they will see them`,
147
- ` immediately.`,
148
- ` - Your human manager is ${humanName}. They can send you`,
149
- ` messages at any time and those will appear here in`,
150
- ` your session just like this one. You can reach them`,
151
- ` by sending a message to --name ${humanName}.`,
152
- ` - Optional: Request recurring scheduled tasks with cron`,
153
- ` jobs. Run 'agent-office worker cron request' to submit`,
154
- ` a request. Your human manager must approve it before`,
155
- ` it becomes active.`,
156
- ``,
157
- `════════════════════════════════════════════════════════`,
158
- ` IMPORTANT: WAIT FOR A MESSAGE BEFORE DOING ANYTHING`,
159
- `════════════════════════════════════════════════════════`,
160
- ``,
161
- ` When you first start up, do nothing. Do not send messages,`,
162
- ` do not begin tasks, do not check in. Simply wait until`,
163
- ` you receive a message assigning you work.`,
164
- ``,
165
- `════════════════════════════════════════════════════════`,
166
- ` IMPORTANT: AVOID MESSAGE SPIRALS WITH COWORKERS`,
167
- `════════════════════════════════════════════════════════`,
168
- ``,
169
- ` Be careful not to get into back-and-forth messaging loops`,
170
- ` with coworkers. Each message you send triggers their session`,
171
- ` which may trigger another message back to you, and so on.`,
172
- ` To avoid spiraling out of control:`,
173
- ` • Consolidate your thoughts — send one clear, complete`,
174
- ` message rather than multiple short ones.`,
175
- ` • Do not send a message just to acknowledge receipt.`,
176
- ` • If a coworker's reply doesn't require action from you,`,
177
- ` do not respond.`,
178
- ` • When delegating, give full context upfront so the`,
179
- ` coworker doesn't need to ask follow-up questions.`,
180
- ``,
181
- ].join("\n");
182
- }
183
- /** Load human_name and human_description from the config table. */
184
- async function loadHumanConfig(storage) {
185
- const humanName = await storage.getConfig('human_name');
186
- const humanDescription = await storage.getConfig('human_description');
187
- return {
188
- humanName: humanName ?? "your human manager",
189
- humanDescription: humanDescription ?? "",
190
- };
191
- }
192
- export function createRouter(storage, agenticCodingServer, serverUrl, scheduler) {
193
- const router = Router();
194
- router.get("/health", (_req, res) => {
195
- res.json({ ok: true });
196
- });
197
- // ── Watch Endpoint (Server-Sent Events for real-time coworker mail state) ──
198
- router.get("/watch", async (_req, res) => {
199
- // Set up SSE headers
200
- res.setHeader("Content-Type", "text/event-stream");
201
- res.setHeader("Cache-Control", "no-cache");
202
- res.setHeader("Connection", "keep-alive");
203
- let isConnected = true;
204
- // Subscribe to storage changes
205
- const unsubscribe = storage.watch((state) => {
206
- if (!isConnected || res.writableEnded)
207
- return;
208
- // Send the state as an SSE event
209
- res.write(`event: state\n`);
210
- res.write(`data: ${JSON.stringify(state)}\n\n`);
211
- });
212
- // Handle client disconnect
213
- res.on("close", () => {
214
- isConnected = false;
215
- unsubscribe();
216
- });
217
- // Handle connection errors
218
- res.on("error", () => {
219
- isConnected = false;
220
- unsubscribe();
221
- });
222
- });
223
- router.get("/modes", async (_req, res) => {
224
- try {
225
- const modes = await agenticCodingServer.getAgentModes();
226
- res.json(modes);
227
- }
228
- catch (err) {
229
- console.error("GET /modes error:", err);
230
- res.json([]);
231
- }
232
- });
233
- router.get("/sessions", async (_req, res) => {
234
- try {
235
- const rows = await storage.listSessions();
236
- res.json(rows);
237
- }
238
- catch (err) {
239
- console.error("GET /sessions error:", err);
240
- res.status(500).json({ error: "Internal server error" });
241
- }
242
- });
243
- router.get("/coworkers", async (_req, res) => {
244
- try {
245
- const sessions = await storage.listSessions();
246
- const humanName = await storage.getConfig('human_name') ?? "Human";
247
- const [unreadCounts, lastMessageAt] = await Promise.all([
248
- storage.countUnreadBySender(humanName),
249
- storage.lastMessageAtByCoworker(humanName),
250
- ]);
251
- const coworkers = [
252
- { name: humanName, status: null, isHuman: true, unreadMessages: 0, lastMessageAt: null },
253
- ...sessions.map(s => ({
254
- name: s.name,
255
- status: s.status,
256
- isHuman: false,
257
- unreadMessages: unreadCounts.get(s.name) ?? 0,
258
- lastMessageAt: lastMessageAt.get(s.name)?.toISOString() ?? null,
259
- }))
260
- ];
261
- // Sort non-human coworkers by most recent message (most recent first),
262
- // coworkers with no messages appear last (sorted by name as tiebreaker)
263
- const [human, ...agents] = coworkers;
264
- agents.sort((a, b) => {
265
- if (a.lastMessageAt && b.lastMessageAt) {
266
- return new Date(b.lastMessageAt).getTime() - new Date(a.lastMessageAt).getTime();
267
- }
268
- if (a.lastMessageAt)
269
- return -1;
270
- if (b.lastMessageAt)
271
- return 1;
272
- return a.name.localeCompare(b.name);
273
- });
274
- res.json([human, ...agents]);
275
- }
276
- catch (err) {
277
- console.error("GET /coworkers error:", err);
278
- res.status(500).json({ error: "Internal server error" });
279
- }
280
- });
281
- router.post("/sessions", async (req, res) => {
282
- const { name, agent: agentArg } = req.body;
283
- if (!name || typeof name !== "string" || !name.trim()) {
284
- res.status(400).json({ error: "name is required" });
285
- return;
286
- }
287
- if (!agentArg || typeof agentArg !== "string" || !agentArg.trim()) {
288
- res.status(400).json({ error: "agent is required" });
289
- return;
290
- }
291
- const trimmedName = name.trim();
292
- const trimmedAgent = agentArg.trim();
293
- const existing = await storage.sessionExists(trimmedName);
294
- if (existing) {
295
- res.status(409).json({ error: `Session name "${trimmedName}" already exists` });
296
- return;
297
- }
298
- let opencodeSessionId;
299
- try {
300
- opencodeSessionId = await agenticCodingServer.createSession(`agent-office: ${trimmedName} ${new Date().toISOString()}`);
301
- }
302
- catch (err) {
303
- console.error("OpenCode session.create error:", err);
304
- res.status(502).json({ error: "Failed to create OpenCode session", detail: String(err) });
305
- return;
306
- }
307
- let row;
308
- try {
309
- row = await storage.createSession(trimmedName, opencodeSessionId, trimmedAgent);
310
- }
311
- catch (err) {
312
- console.error("DB insert error:", err);
313
- try {
314
- await agenticCodingServer.deleteSession(opencodeSessionId);
315
- }
316
- catch { }
317
- res.status(500).json({ error: "Internal server error" });
318
- return;
319
- }
320
- res.status(201).json(row);
321
- });
322
- router.post("/sessions/:name/regenerate-code", async (req, res) => {
323
- const { name } = req.params;
324
- const session = await storage.getSessionByName(name);
325
- if (!session) {
326
- res.status(404).json({ error: `Session "${name}" not found` });
327
- return;
328
- }
329
- try {
330
- const updated = await storage.regenerateAgentCode(name);
331
- res.json(updated);
332
- }
333
- catch (err) {
334
- console.error("regenerate-code error:", err);
335
- res.status(500).json({ error: "Internal server error" });
336
- }
337
- });
338
- router.patch("/sessions/:name/agent", async (req, res) => {
339
- const { name } = req.params;
340
- const { agent } = req.body;
341
- if (!agent || typeof agent !== "string" || !agent.trim()) {
342
- res.status(400).json({ error: "agent is required" });
343
- return;
344
- }
345
- const trimmedAgent = agent.trim();
346
- const session = await storage.getSessionByName(name);
347
- if (!session) {
348
- res.status(404).json({ error: `Session "${name}" not found` });
349
- return;
350
- }
351
- try {
352
- const updated = await storage.updateSessionAgent(name, trimmedAgent);
353
- res.json(updated);
354
- }
355
- catch (err) {
356
- console.error("PATCH /sessions/:name/agent error:", err);
357
- res.status(500).json({ error: "Internal server error" });
358
- }
359
- });
360
- router.get("/sessions/:name/messages", async (req, res) => {
361
- const { name } = req.params;
362
- const limit = Math.min(parseInt(req.query.limit ?? "20", 10), 100);
363
- const row = await storage.getSessionByName(name);
364
- if (!row) {
365
- res.status(404).json({ error: `Session "${name}" not found` });
366
- return;
367
- }
368
- try {
369
- const messages = await agenticCodingServer.getMessages(row.session_id, limit);
370
- const result = messages
371
- .slice(-limit)
372
- .map((m) => ({
373
- role: m.role,
374
- parts: m.parts.map((p) => {
375
- if (p.type === "text") {
376
- return { type: "text", text: p.text ?? "" };
377
- }
378
- else if (p.type === "tool") {
379
- // Extract tool name, input, and output from the tool state
380
- const state = p.state;
381
- return {
382
- type: "tool",
383
- tool: p.tool,
384
- input: state?.input,
385
- output: state?.output,
386
- data: p,
387
- };
388
- }
389
- else {
390
- return { type: p.type, data: p };
391
- }
392
- }),
393
- }))
394
- .filter((m) => m.parts.length > 0);
395
- res.json(result);
396
- }
397
- catch (err) {
398
- console.error("OpenCode session.messages error:", err);
399
- res.status(502).json({ error: "Failed to fetch messages from OpenCode", detail: String(err) });
400
- }
401
- });
402
- router.post("/sessions/:name/inject", async (req, res) => {
403
- const { name } = req.params;
404
- const { text, modelID, providerID } = req.body;
405
- if (!text || typeof text !== "string" || !text.trim()) {
406
- res.status(400).json({ error: "text is required" });
407
- return;
408
- }
409
- const row = await storage.getSessionByName(name);
410
- if (!row) {
411
- res.status(404).json({ error: `Session "${name}" not found` });
412
- return;
413
- }
414
- try {
415
- const { humanName, humanDescription } = await loadHumanConfig(storage);
416
- const token = `${row.agent_code}@${serverUrl}`;
417
- const system = generateSystemPrompt(row.name, row.status ?? null, humanName, humanDescription, token);
418
- await agenticCodingServer.sendMessage(row.session_id, text.trim(), row.agent, system);
419
- res.json({ ok: true });
420
- }
421
- catch (err) {
422
- console.error("OpenCode session.prompt error:", err);
423
- res.status(502).json({ error: "Failed to inject message into OpenCode session", detail: String(err) });
424
- }
425
- });
426
- router.post("/sessions/:name/revert-to-start", async (req, res) => {
427
- const { name } = req.params;
428
- const session = await storage.getSessionByName(name);
429
- if (!session) {
430
- res.status(404).json({ error: `Session "${name}" not found` });
431
- return;
432
- }
433
- try {
434
- // Delete the old OpenCode session and create a fresh one
435
- await agenticCodingServer.deleteSession(session.session_id);
436
- const newSessionId = await agenticCodingServer.createSession(`agent-office: ${name} ${new Date().toISOString()}`);
437
- await storage.updateSessionId(name, newSessionId);
438
- res.json({ ok: true });
439
- }
440
- catch (err) {
441
- console.error("POST /sessions/:name/revert-to-start error:", err);
442
- res.status(502).json({ error: "Failed to revert session", detail: String(err) });
443
- }
444
- });
445
- router.post("/sessions/revert-all", async (_req, res) => {
446
- const allSessions = await storage.listSessions();
447
- const results = [];
448
- for (const session of allSessions) {
449
- try {
450
- // Delete the old OpenCode session and create a fresh one
451
- await agenticCodingServer.deleteSession(session.session_id);
452
- const newSessionId = await agenticCodingServer.createSession(`agent-office: ${session.name} ${new Date().toISOString()}`);
453
- await storage.updateSessionId(session.name, newSessionId);
454
- results.push({ name: session.name, ok: true });
455
- }
456
- catch (err) {
457
- console.error(`POST /sessions/revert-all error for "${session.name}":`, err);
458
- results.push({ name: session.name, ok: false, error: String(err) });
459
- }
460
- }
461
- const failed = results.filter((r) => !r.ok);
462
- res.json({ ok: failed.length === 0, total: allSessions.length, results });
463
- });
464
- router.delete("/sessions/:name", async (req, res) => {
465
- const { name } = req.params;
466
- const row = await storage.getSessionByName(name);
467
- if (!row) {
468
- res.status(404).json({ error: `Session "${name}" not found` });
469
- return;
470
- }
471
- try {
472
- await agenticCodingServer.deleteSession(row.session_id);
473
- }
474
- catch (err) {
475
- console.error("OpenCode session.delete error:", err);
476
- res.status(502).json({ error: "Failed to delete OpenCode session", detail: String(err) });
477
- return;
478
- }
479
- try {
480
- await storage.deleteSession(row.id);
481
- res.json({ deleted: true, name: row.name, session_id: row.session_id });
482
- }
483
- catch (err) {
484
- console.error("DB delete error:", err);
485
- res.status(500).json({ error: "Internal server error" });
486
- }
487
- });
488
- router.get("/config", async (_req, res) => {
489
- try {
490
- const rows = await storage.getAllConfig();
491
- const config = {};
492
- for (const row of rows) {
493
- config[row.key] = row.value;
494
- }
495
- res.json(config);
496
- }
497
- catch (err) {
498
- console.error("GET /config error:", err);
499
- res.status(500).json({ error: "Internal server error" });
500
- }
501
- });
502
- router.put("/config", async (req, res) => {
503
- const { key, value } = req.body;
504
- if (!key || typeof key !== "string" || !key.trim()) {
505
- res.status(400).json({ error: "key is required" });
506
- return;
507
- }
508
- if (typeof value !== "string") {
509
- res.status(400).json({ error: "value must be a string" });
510
- return;
511
- }
512
- try {
513
- await storage.setConfig(key, value);
514
- res.json({ ok: true, key, value });
515
- }
516
- catch (err) {
517
- console.error("PUT /config error:", err);
518
- res.status(500).json({ error: "Internal server error" });
519
- }
520
- });
521
- router.get("/messages/:name", async (req, res) => {
522
- const { name } = req.params;
523
- const { sent, unread_only } = req.query;
524
- try {
525
- let rows;
526
- if (sent === "true") {
527
- rows = await storage.listMessagesFromSender(name);
528
- }
529
- else {
530
- rows = await storage.listMessagesForRecipient(name, unread_only === "true" ? { unread: true } : undefined);
531
- }
532
- res.json(rows);
533
- }
534
- catch (err) {
535
- console.error("GET /messages/:name error:", err);
536
- res.status(500).json({ error: "Internal server error" });
537
- }
538
- });
539
- router.post("/messages", async (req, res) => {
540
- const { from, to, body } = req.body;
541
- if (!from || typeof from !== "string" || !from.trim()) {
542
- res.status(400).json({ error: "from is required" });
543
- return;
544
- }
545
- if (!to || !Array.isArray(to) || to.length === 0) {
546
- res.status(400).json({ error: "to must be a non-empty array of recipient names" });
547
- return;
548
- }
549
- if (!body || typeof body !== "string" || !body.trim()) {
550
- res.status(400).json({ error: "body is required" });
551
- return;
552
- }
553
- const trimmedFrom = from.trim();
554
- const trimmedBody = body.trim();
555
- const sessions = await storage.listSessions();
556
- const sessionMap = new Map(sessions.map((s) => [s.name, { sessionId: s.session_id, agent: s.agent, agentCode: s.agent_code, status: s.status }]));
557
- const { humanName, humanDescription } = await loadHumanConfig(storage);
558
- const validRecipients = [];
559
- for (const recipient of to) {
560
- if (typeof recipient !== "string" || !recipient.trim())
561
- continue;
562
- const r = recipient.trim();
563
- if (sessionMap.has(r) || r === humanName) {
564
- validRecipients.push(r);
565
- }
566
- }
567
- if (validRecipients.length === 0) {
568
- res.status(400).json({ error: "No valid recipients found" });
569
- return;
570
- }
571
- const results = [];
572
- for (const recipient of validRecipients) {
573
- const msgRow = await storage.createMessage(trimmedFrom, recipient, trimmedBody);
574
- const msgId = msgRow.id;
575
- let injected = false;
576
- if (sessionMap.has(recipient)) {
577
- const { sessionId, agent, agentCode, status } = sessionMap.get(recipient);
578
- const injectText = `[Message from "${trimmedFrom}"]: ${trimmedBody}${MAIL_INJECTION_BLURB}`;
579
- const token = `${agentCode}@${serverUrl}`;
580
- const system = generateSystemPrompt(recipient, status ?? null, humanName, humanDescription, token);
581
- try {
582
- await agenticCodingServer.sendMessage(sessionId, injectText, agent ?? undefined, system);
583
- await storage.markMessageAsInjected(msgId);
584
- injected = true;
585
- }
586
- catch (err) {
587
- console.warn(`Warning: could not inject message into session "${recipient}":`, err);
588
- }
589
- }
590
- results.push({ to: recipient, messageId: msgId, injected });
591
- }
592
- res.status(201).json({ ok: true, results });
593
- });
594
- router.post("/messages/:id/read", async (req, res) => {
595
- const id = parseInt(String(req.params.id), 10);
596
- if (isNaN(id)) {
597
- res.status(400).json({ error: "Invalid message id" });
598
- return;
599
- }
600
- try {
601
- const updated = await storage.markMessageAsRead(id);
602
- if (!updated) {
603
- res.status(404).json({ error: "Message not found" });
604
- return;
605
- }
606
- res.json(updated);
607
- }
608
- catch (err) {
609
- console.error("POST /messages/:id/read error:", err);
610
- res.status(500).json({ error: "Internal server error" });
611
- }
612
- });
613
- // ── Human Notification API ─────────────────────────────────────────────────
614
- router.get("/human/unread-old", async (req, res) => {
615
- try {
616
- const hoursStr = req.query.hours ?? "";
617
- const hours = hoursStr !== "" ? parseFloat(hoursStr) : 1;
618
- const humanName = (await storage.getConfig("human_name")) ?? "Human";
619
- const msgs = await storage.listMessagesForRecipient(humanName, {
620
- unread: true,
621
- olderThanHours: hours,
622
- notified: false,
623
- });
624
- res.json(msgs);
625
- }
626
- catch (err) {
627
- console.error("GET /human/unread-old error:", err);
628
- res.status(500).json({ error: "Internal server error" });
629
- }
630
- });
631
- router.post("/human/mark-notified", async (req, res) => {
632
- try {
633
- const ids = req.body.ids;
634
- if (!Array.isArray(ids) || !ids.every((id) => typeof id === "number")) {
635
- res.status(400).json({ error: "ids must be an array of numbers" });
636
- return;
637
- }
638
- await storage.markMessagesAsNotified(ids);
639
- res.json({ success: true, count: ids.length });
640
- }
641
- catch (err) {
642
- console.error("POST /human/mark-notified error:", err);
643
- res.status(500).json({ error: "Internal server error" });
644
- }
645
- });
646
- // ── Cron Jobs Endpoints ────────────────────────────────────────────────────
647
- router.get("/crons", async (req, res) => {
648
- try {
649
- const { session_name } = req.query;
650
- let rows;
651
- if (session_name) {
652
- rows = await storage.listCronJobsForSession(session_name);
653
- }
654
- else {
655
- rows = await storage.listCronJobs();
656
- }
657
- const jobs = rows.map((job) => {
658
- let nextRun = null;
659
- if (job.enabled) {
660
- try {
661
- const options = {};
662
- if (job.timezone)
663
- options.timezone = job.timezone;
664
- const cronJob = new CronerInstance(job.schedule, options);
665
- const next = cronJob.nextRun();
666
- nextRun = next ? next.toISOString() : null;
667
- }
668
- catch { }
669
- }
670
- return { ...job, next_run: nextRun, last_run: job.last_run ? job.last_run.toISOString() : null, created_at: job.created_at.toISOString() };
671
- });
672
- res.json(jobs);
673
- }
674
- catch (err) {
675
- console.error("GET /crons error:", err);
676
- res.status(500).json({ error: "Internal server error" });
677
- }
678
- });
679
- router.post("/crons", async (req, res) => {
680
- const { name, session_name, schedule, message, timezone } = req.body;
681
- if (!name || typeof name !== "string" || !name.trim()) {
682
- res.status(400).json({ error: "name is required" });
683
- return;
684
- }
685
- if (!session_name || typeof session_name !== "string") {
686
- res.status(400).json({ error: "session_name is required" });
687
- return;
688
- }
689
- if (!schedule || typeof schedule !== "string" || !schedule.trim()) {
690
- res.status(400).json({ error: "schedule is required" });
691
- return;
692
- }
693
- if (!message || typeof message !== "string" || !message.trim()) {
694
- res.status(400).json({ error: "message is required" });
695
- return;
696
- }
697
- const trimmedName = name.trim();
698
- const trimmedSchedule = schedule.trim();
699
- const trimmedMessage = message.trim();
700
- try {
701
- new CronerInstance(trimmedSchedule);
702
- }
703
- catch {
704
- res.status(400).json({ error: "Invalid cron schedule expression" });
705
- return;
706
- }
707
- if (timezone) {
708
- try {
709
- new CronerInstance("0 0 * * *", { timezone });
710
- }
711
- catch {
712
- res.status(400).json({ error: "Invalid timezone" });
713
- return;
714
- }
715
- }
716
- const existing = await storage.cronJobExistsForSession(trimmedName, session_name);
717
- if (existing) {
718
- res.status(409).json({ error: `Cron job "${trimmedName}" already exists for this session` });
719
- return;
720
- }
721
- try {
722
- const cronJobrow = await storage.createCronJob(trimmedName, session_name, trimmedSchedule, timezone ?? null, trimmedMessage);
723
- scheduler.addCronJob(cronJobrow);
724
- const nextRun = cronJobrow.enabled ? (() => {
725
- try {
726
- const options = {};
727
- if (cronJobrow.timezone)
728
- options.timezone = cronJobrow.timezone;
729
- const cronJob = new CronerInstance(cronJobrow.schedule, options);
730
- const next = cronJob.nextRun();
731
- return next ? next.toISOString() : null;
732
- }
733
- catch {
734
- return null;
735
- }
736
- })() : null;
737
- res.status(201).json({
738
- ...cronJobrow,
739
- next_run: nextRun,
740
- last_run: cronJobrow?.last_run?.toISOString() ?? null,
741
- created_at: cronJobrow.created_at.toISOString(),
742
- });
743
- }
744
- catch (err) {
745
- console.error("POST /crons error:", err);
746
- res.status(500).json({ error: "Internal server error" });
747
- }
748
- });
749
- router.delete("/crons/:id", async (req, res) => {
750
- const id = parseInt(String(req.params.id), 10);
751
- if (isNaN(id)) {
752
- res.status(400).json({ error: "Invalid cron job id" });
753
- return;
754
- }
755
- try {
756
- const job = await storage.getCronJobById(id);
757
- if (!job) {
758
- res.status(404).json({ error: "Cron job not found" });
759
- return;
760
- }
761
- scheduler.removeCronJob(id);
762
- await storage.deleteCronJob(id);
763
- res.json({ deleted: true, id, name: job.name });
764
- }
765
- catch (err) {
766
- console.error("DELETE /crons/:id error:", err);
767
- res.status(500).json({ error: "Internal server error" });
768
- }
769
- });
770
- router.post("/crons/:id/enable", async (req, res) => {
771
- const id = parseInt(String(req.params.id), 10);
772
- if (isNaN(id)) {
773
- res.status(400).json({ error: "Invalid cron job id" });
774
- return;
775
- }
776
- try {
777
- const existing = await storage.getCronJobById(id);
778
- if (!existing) {
779
- res.status(404).json({ error: "Cron job not found" });
780
- return;
781
- }
782
- await storage.enableCronJob(id);
783
- const updated = await storage.getCronJobById(id);
784
- if (updated) {
785
- scheduler.enableCronJob(updated);
786
- const nextRun = (() => {
787
- try {
788
- const options = {};
789
- if (updated.timezone)
790
- options.timezone = updated.timezone;
791
- const cronJob = new CronerInstance(updated.schedule, options);
792
- const next = cronJob.nextRun();
793
- return next ? next.toISOString() : null;
794
- }
795
- catch {
796
- return null;
797
- }
798
- })();
799
- res.json({
800
- ...updated,
801
- next_run: nextRun,
802
- last_run: updated.last_run?.toISOString() ?? null,
803
- created_at: updated.created_at.toISOString(),
804
- });
805
- }
806
- else {
807
- res.status(404).json({ error: "Cron job not found" });
808
- }
809
- }
810
- catch (err) {
811
- console.error("POST /crons/:id/enable error:", err);
812
- res.status(500).json({ error: "Internal server error" });
813
- }
814
- });
815
- router.post("/crons/:id/disable", async (req, res) => {
816
- const id = parseInt(String(req.params.id), 10);
817
- if (isNaN(id)) {
818
- res.status(400).json({ error: "Invalid cron job id" });
819
- return;
820
- }
821
- try {
822
- const job = await storage.getCronJobById(id);
823
- if (!job) {
824
- res.status(404).json({ error: "Cron job not found" });
825
- return;
826
- }
827
- await storage.disableCronJob(id);
828
- scheduler.disableCronJob(id);
829
- const updated = await storage.getCronJobById(id);
830
- res.json({
831
- ...updated,
832
- next_run: null,
833
- last_run: updated?.last_run?.toISOString() ?? null,
834
- created_at: updated?.created_at.toISOString() ?? null,
835
- });
836
- }
837
- catch (err) {
838
- console.error("POST /crons/:id/disable error:", err);
839
- res.status(500).json({ error: "Internal server error" });
840
- }
841
- });
842
- router.get("/crons/:id/history", async (req, res) => {
843
- const id = parseInt(String(req.params.id), 10);
844
- const limit = Math.min(parseInt(req.query.limit ?? "10", 10), 100);
845
- if (isNaN(id)) {
846
- res.status(400).json({ error: "Invalid cron job id" });
847
- return;
848
- }
849
- try {
850
- const rows = await storage.listCronHistory(id, limit);
851
- res.json(rows.map((r) => ({
852
- ...r,
853
- executed_at: r.executed_at.toISOString(),
854
- })));
855
- }
856
- catch (err) {
857
- console.error("GET /crons/:id/history error:", err);
858
- res.status(500).json({ error: "Internal server error" });
859
- }
860
- });
861
- // ── Cron Requests (manage-side: view, approve, reject) ──
862
- router.get("/cron-requests", async (req, res) => {
863
- try {
864
- const { status, session_name } = req.query;
865
- const filters = {};
866
- if (status)
867
- filters.status = status;
868
- if (session_name)
869
- filters.sessionName = session_name;
870
- const rows = await storage.listCronRequests(filters);
871
- res.json(rows.map((r) => ({
872
- ...r,
873
- requested_at: r.requested_at.toISOString(),
874
- reviewed_at: r.reviewed_at?.toISOString() ?? null,
875
- })));
876
- }
877
- catch (err) {
878
- console.error("GET /cron-requests error:", err);
879
- res.status(500).json({ error: "Internal server error" });
880
- }
881
- });
882
- router.post("/cron-requests/:id/approve", async (req, res) => {
883
- const id = parseInt(String(req.params.id), 10);
884
- if (isNaN(id)) {
885
- res.status(400).json({ error: "Invalid cron request id" });
886
- return;
887
- }
888
- const { notes } = req.body;
889
- try {
890
- const request = await storage.getCronRequestById(id);
891
- if (!request) {
892
- res.status(404).json({ error: "Cron request not found" });
893
- return;
894
- }
895
- if (request.status !== "pending") {
896
- res.status(409).json({ error: `Cron request already ${request.status}` });
897
- return;
898
- }
899
- const { humanName } = await loadHumanConfig(storage);
900
- // Update request status
901
- const updated = await storage.updateCronRequestStatus(id, "approved", humanName, notes);
902
- // Create the actual cron job
903
- const cronJob = await storage.createCronJob(request.name, request.session_name, request.schedule, request.timezone, request.message);
904
- scheduler.addCronJob(cronJob);
905
- // Notify the worker that their request was approved
906
- const notificationBody = `Your cron job request "${request.name}" (schedule: ${request.schedule}) has been approved and is now active.${notes ? `\n\nNote from manager: ${notes}` : ""}`;
907
- await storage.createMessage(humanName, request.session_name, notificationBody);
908
- // Inject notification into the worker's session
909
- const session = await storage.getSessionByName(request.session_name);
910
- if (session) {
911
- const token = `${session.agent_code}@${serverUrl}`;
912
- const system = generateSystemPrompt(session.name, session.status, humanName, (await storage.getConfig('human_description')) ?? '', token);
913
- const injectText = `[Cron Request Approved] Your cron job request "${request.name}" has been approved and scheduled.${notes ? ` Manager note: ${notes}` : ""}`;
914
- try {
915
- await agenticCodingServer.sendMessage(session.session_id, injectText, session.agent, system);
916
- }
917
- catch {
918
- // If injection fails, the message is still in their mailbox
919
- }
920
- }
921
- const nextRun = cronJob.enabled ? (() => {
922
- try {
923
- const options = {};
924
- if (cronJob.timezone)
925
- options.timezone = cronJob.timezone;
926
- const cron = new CronerInstance(cronJob.schedule, options);
927
- const next = cron.nextRun();
928
- return next ? next.toISOString() : null;
929
- }
930
- catch {
931
- return null;
932
- }
933
- })() : null;
934
- res.json({
935
- request: {
936
- ...updated,
937
- requested_at: updated?.requested_at.toISOString(),
938
- reviewed_at: updated?.reviewed_at?.toISOString() ?? null,
939
- },
940
- cron_job: {
941
- ...cronJob,
942
- next_run: nextRun,
943
- last_run: cronJob.last_run?.toISOString() ?? null,
944
- created_at: cronJob.created_at.toISOString(),
945
- },
946
- });
947
- }
948
- catch (err) {
949
- console.error("POST /cron-requests/:id/approve error:", err);
950
- res.status(500).json({ error: "Internal server error" });
951
- }
952
- });
953
- router.post("/cron-requests/:id/reject", async (req, res) => {
954
- const id = parseInt(String(req.params.id), 10);
955
- if (isNaN(id)) {
956
- res.status(400).json({ error: "Invalid cron request id" });
957
- return;
958
- }
959
- const { notes } = req.body;
960
- try {
961
- const request = await storage.getCronRequestById(id);
962
- if (!request) {
963
- res.status(404).json({ error: "Cron request not found" });
964
- return;
965
- }
966
- if (request.status !== "pending") {
967
- res.status(409).json({ error: `Cron request already ${request.status}` });
968
- return;
969
- }
970
- const { humanName } = await loadHumanConfig(storage);
971
- // Update request status
972
- const updated = await storage.updateCronRequestStatus(id, "rejected", humanName, notes);
973
- // Notify the worker that their request was rejected
974
- const notificationBody = `Your cron job request "${request.name}" (schedule: ${request.schedule}) has been rejected.${notes ? `\n\nReason: ${notes}` : ""}`;
975
- await storage.createMessage(humanName, request.session_name, notificationBody);
976
- // Inject notification into the worker's session
977
- const session = await storage.getSessionByName(request.session_name);
978
- if (session) {
979
- const token = `${session.agent_code}@${serverUrl}`;
980
- const system = generateSystemPrompt(session.name, session.status, humanName, (await storage.getConfig('human_description')) ?? '', token);
981
- const injectText = `[Cron Request Rejected] Your cron job request "${request.name}" has been rejected.${notes ? ` Reason: ${notes}` : ""}`;
982
- try {
983
- await agenticCodingServer.sendMessage(session.session_id, injectText, session.agent, system);
984
- }
985
- catch {
986
- // If injection fails, the message is still in their mailbox
987
- }
988
- }
989
- res.json({
990
- ...updated,
991
- requested_at: updated?.requested_at.toISOString(),
992
- reviewed_at: updated?.reviewed_at?.toISOString() ?? null,
993
- });
994
- }
995
- catch (err) {
996
- console.error("POST /cron-requests/:id/reject error:", err);
997
- res.status(500).json({ error: "Internal server error" });
998
- }
999
- });
1000
- return router;
1001
- }
1002
- export function createWorkerRouter(storage, agenticCodingServer, serverUrl) {
1003
- const router = Router();
1004
- router.get("/worker/list-coworkers", async (req, res) => {
1005
- const { code } = req.query;
1006
- if (!code || typeof code !== "string") {
1007
- res.status(400).json({ error: "code query parameter is required" });
1008
- return;
1009
- }
1010
- const session = await storage.getSessionByAgentCode(code);
1011
- if (!session) {
1012
- res.status(401).json({ error: "Invalid agent code" });
1013
- return;
1014
- }
1015
- try {
1016
- const sessions = await storage.listSessions();
1017
- const humanName = await storage.getConfig('human_name') ?? "Human";
1018
- const workers = sessions.filter((s) => s.name !== session.name).map((s) => s.name);
1019
- workers.push(humanName);
1020
- res.json(workers);
1021
- }
1022
- catch (err) {
1023
- console.error("GET /worker/list-coworkers error:", err);
1024
- res.status(500).json({ error: "Internal server error" });
1025
- }
1026
- });
1027
- router.post("/worker/set-status", async (req, res) => {
1028
- const { code } = req.query;
1029
- const { status } = req.body;
1030
- if (!code || typeof code !== "string") {
1031
- res.status(400).json({ error: "code query parameter is required" });
1032
- return;
1033
- }
1034
- const trimmedStatus = status === null ? null : (status === undefined ? null : status.trim());
1035
- if (trimmedStatus !== null && trimmedStatus.length > 140) {
1036
- res.status(400).json({ error: "status must be at most 140 characters" });
1037
- return;
1038
- }
1039
- const session = await storage.getSessionByAgentCode(code);
1040
- if (!session) {
1041
- res.status(401).json({ error: "Invalid agent code" });
1042
- return;
1043
- }
1044
- try {
1045
- await storage.updateSessionStatus(code, trimmedStatus);
1046
- }
1047
- catch (err) {
1048
- console.error("POST /worker/set-status error:", err);
1049
- res.status(500).json({ error: "Internal server error" });
1050
- return;
1051
- }
1052
- res.json({ ok: true, name: session.name, status: trimmedStatus });
1053
- });
1054
- router.post("/worker/send-message", async (req, res) => {
1055
- const { code } = req.query;
1056
- const { to, body } = req.body;
1057
- if (!code || typeof code !== "string") {
1058
- res.status(400).json({ error: "code query parameter is required" });
1059
- return;
1060
- }
1061
- const session = await storage.getSessionByAgentCode(code);
1062
- if (!session) {
1063
- res.status(401).json({ error: "Invalid agent code" });
1064
- return;
1065
- }
1066
- if (!to || !Array.isArray(to) || to.length === 0) {
1067
- res.status(400).json({ error: "to must be a non-empty array of recipient names" });
1068
- return;
1069
- }
1070
- if (!body || typeof body !== "string" || !body.trim()) {
1071
- res.status(400).json({ error: "body is required" });
1072
- return;
1073
- }
1074
- const trimmedBody = body.trim();
1075
- const sessions = await storage.listSessions();
1076
- const sessionMap = new Map(sessions.map((s) => [s.name, { sessionId: s.session_id, agent: s.agent, agentCode: s.agent_code, status: s.status }]));
1077
- const { humanName, humanDescription } = await loadHumanConfig(storage);
1078
- const validRecipients = [];
1079
- for (const recipient of to) {
1080
- if (typeof recipient !== "string" || !recipient.trim())
1081
- continue;
1082
- const r = recipient.trim();
1083
- if (sessionMap.has(r) || r === humanName) {
1084
- validRecipients.push(r);
1085
- }
1086
- }
1087
- if (validRecipients.length === 0) {
1088
- res.status(400).json({ error: "No valid recipients found" });
1089
- return;
1090
- }
1091
- const results = [];
1092
- for (const recipient of validRecipients) {
1093
- const msgRow = await storage.createMessage(session.name, recipient, trimmedBody);
1094
- const msgId = msgRow.id;
1095
- let injected = false;
1096
- if (sessionMap.has(recipient)) {
1097
- const { sessionId: recipientSessionId, agent: recipientAgent, agentCode, status } = sessionMap.get(recipient);
1098
- const injectText = `[Message from "${session.name}"]: ${trimmedBody}${MAIL_INJECTION_BLURB}`;
1099
- const token = `${agentCode}@${serverUrl}`;
1100
- const system = generateSystemPrompt(recipient, status ?? null, humanName, humanDescription, token);
1101
- try {
1102
- await agenticCodingServer.sendMessage(recipientSessionId, injectText, recipientAgent ?? undefined, system);
1103
- await storage.markMessageAsInjected(msgId);
1104
- injected = true;
1105
- }
1106
- catch (err) {
1107
- console.warn(`Warning: could not inject message into session "${recipient}":`, err);
1108
- }
1109
- }
1110
- results.push({ to: recipient, messageId: msgId, injected });
1111
- }
1112
- res.status(201).json({ ok: true, results });
1113
- });
1114
- // ── Worker Cron Endpoints (authenticated via agent_code in query) ───────────
1115
- router.get("/worker/crons", async (req, res) => {
1116
- const { code } = req.query;
1117
- if (!code || typeof code !== "string") {
1118
- res.status(400).json({ error: "code query parameter is required" });
1119
- return;
1120
- }
1121
- const session = await storage.getSessionByAgentCode(code);
1122
- if (!session) {
1123
- res.status(401).json({ error: "Invalid agent code" });
1124
- return;
1125
- }
1126
- const sessionName = session.name;
1127
- try {
1128
- const rows = await storage.listCronJobsForSession(sessionName);
1129
- const jobs = rows.map((job) => {
1130
- let nextRun = null;
1131
- if (job.enabled) {
1132
- try {
1133
- const options = {};
1134
- if (job.timezone)
1135
- options.timezone = job.timezone;
1136
- const cronJob = new CronerInstance(job.schedule, options);
1137
- const next = cronJob.nextRun();
1138
- nextRun = next ? next.toISOString() : null;
1139
- }
1140
- catch { }
1141
- }
1142
- return {
1143
- ...job,
1144
- next_run: nextRun,
1145
- last_run: job.last_run ? job.last_run.toISOString() : null,
1146
- created_at: job.created_at.toISOString(),
1147
- };
1148
- });
1149
- res.json(jobs);
1150
- }
1151
- catch (err) {
1152
- console.error("GET /worker/crons error:", err);
1153
- res.status(500).json({ error: "Internal server error" });
1154
- }
1155
- });
1156
- // ── Cron Requests (worker can request, human must approve) ──
1157
- router.get("/worker/cron-requests", async (req, res) => {
1158
- const { code } = req.query;
1159
- if (!code || typeof code !== "string") {
1160
- res.status(400).json({ error: "code query parameter is required" });
1161
- return;
1162
- }
1163
- const session = await storage.getSessionByAgentCode(code);
1164
- if (!session) {
1165
- res.status(401).json({ error: "Invalid agent code" });
1166
- return;
1167
- }
1168
- try {
1169
- const rows = await storage.listCronRequests({ sessionName: session.name });
1170
- res.json(rows.map((r) => ({
1171
- ...r,
1172
- requested_at: r.requested_at.toISOString(),
1173
- reviewed_at: r.reviewed_at?.toISOString() ?? null,
1174
- })));
1175
- }
1176
- catch (err) {
1177
- console.error("GET /worker/cron-requests error:", err);
1178
- res.status(500).json({ error: "Internal server error" });
1179
- }
1180
- });
1181
- router.post("/worker/cron-requests", async (req, res) => {
1182
- const { code } = req.query;
1183
- const { name, schedule, message, timezone } = req.body;
1184
- if (!code || typeof code !== "string") {
1185
- res.status(400).json({ error: "code query parameter is required" });
1186
- return;
1187
- }
1188
- const session = await storage.getSessionByAgentCode(code);
1189
- if (!session) {
1190
- res.status(401).json({ error: "Invalid agent code" });
1191
- return;
1192
- }
1193
- if (!name || typeof name !== "string" || !name.trim()) {
1194
- res.status(400).json({ error: "name is required" });
1195
- return;
1196
- }
1197
- if (!schedule || typeof schedule !== "string" || !schedule.trim()) {
1198
- res.status(400).json({ error: "schedule is required" });
1199
- return;
1200
- }
1201
- if (!message || typeof message !== "string" || !message.trim()) {
1202
- res.status(400).json({ error: "message is required" });
1203
- return;
1204
- }
1205
- const trimmedName = name.trim();
1206
- const trimmedSchedule = schedule.trim();
1207
- const trimmedMessage = message.trim();
1208
- try {
1209
- new CronerInstance(trimmedSchedule);
1210
- }
1211
- catch {
1212
- res.status(400).json({ error: "Invalid cron schedule expression" });
1213
- return;
1214
- }
1215
- if (timezone) {
1216
- try {
1217
- new CronerInstance("0 0 * * *", { timezone });
1218
- }
1219
- catch {
1220
- res.status(400).json({ error: "Invalid timezone" });
1221
- return;
1222
- }
1223
- }
1224
- try {
1225
- const request = await storage.createCronRequest(trimmedName, session.name, trimmedSchedule, timezone ?? null, trimmedMessage);
1226
- res.status(201).json({
1227
- ...request,
1228
- requested_at: request.requested_at.toISOString(),
1229
- reviewed_at: request.reviewed_at?.toISOString() ?? null,
1230
- });
1231
- }
1232
- catch (err) {
1233
- console.error("POST /worker/cron-requests error:", err);
1234
- res.status(500).json({ error: "Internal server error" });
1235
- }
1236
- });
1237
- router.delete("/worker/crons/:id", async (req, res) => {
1238
- const { code } = req.query;
1239
- const id = parseInt(String(req.params.id), 10);
1240
- if (!code || typeof code !== "string") {
1241
- res.status(400).json({ error: "code query parameter is required" });
1242
- return;
1243
- }
1244
- if (isNaN(id)) {
1245
- res.status(400).json({ error: "Invalid cron job id" });
1246
- return;
1247
- }
1248
- const session = await storage.getSessionByAgentCode(code);
1249
- if (!session) {
1250
- res.status(401).json({ error: "Invalid agent code" });
1251
- return;
1252
- }
1253
- const sessionName = session.name;
1254
- try {
1255
- const job = await storage.getCronJobById(id);
1256
- if (!job || job.session_name !== sessionName) {
1257
- res.status(404).json({ error: "Cron job not found or not owned by you" });
1258
- return;
1259
- }
1260
- await storage.deleteCronJob(id);
1261
- res.json({ deleted: true, id, name: job.name });
1262
- }
1263
- catch (err) {
1264
- console.error("DELETE /worker/crons/:id error:", err);
1265
- res.status(500).json({ error: "Internal server error" });
1266
- }
1267
- });
1268
- router.post("/worker/crons/:id/enable", async (req, res) => {
1269
- const { code } = req.query;
1270
- const id = parseInt(String(req.params.id), 10);
1271
- if (!code || typeof code !== "string") {
1272
- res.status(400).json({ error: "code query parameter is required" });
1273
- return;
1274
- }
1275
- if (isNaN(id)) {
1276
- res.status(400).json({ error: "Invalid cron job id" });
1277
- return;
1278
- }
1279
- const session = await storage.getSessionByAgentCode(code);
1280
- if (!session) {
1281
- res.status(401).json({ error: "Invalid agent code" });
1282
- return;
1283
- }
1284
- const sessionName = session.name;
1285
- try {
1286
- const existing = await storage.getCronJobById(id);
1287
- if (!existing || existing.session_name !== sessionName) {
1288
- res.status(404).json({ error: "Cron job not found or not owned by you" });
1289
- return;
1290
- }
1291
- await storage.enableCronJob(id);
1292
- const updated = await storage.getCronJobById(id);
1293
- if (updated) {
1294
- const nextRun = (() => {
1295
- try {
1296
- const options = {};
1297
- if (updated.timezone)
1298
- options.timezone = updated.timezone;
1299
- const cronJob = new CronerInstance(updated.schedule, options);
1300
- const next = cronJob.nextRun();
1301
- return next ? next.toISOString() : null;
1302
- }
1303
- catch {
1304
- return null;
1305
- }
1306
- })();
1307
- res.json({
1308
- ...updated,
1309
- next_run: nextRun,
1310
- last_run: updated.last_run?.toISOString() ?? null,
1311
- created_at: updated.created_at.toISOString(),
1312
- });
1313
- }
1314
- else {
1315
- res.status(404).json({ error: "Cron job not found" });
1316
- }
1317
- }
1318
- catch (err) {
1319
- console.error("POST /worker/crons/:id/enable error:", err);
1320
- res.status(500).json({ error: "Internal server error" });
1321
- }
1322
- });
1323
- router.post("/worker/crons/:id/disable", async (req, res) => {
1324
- const { code } = req.query;
1325
- const id = parseInt(String(req.params.id), 10);
1326
- if (!code || typeof code !== "string") {
1327
- res.status(400).json({ error: "code query parameter is required" });
1328
- return;
1329
- }
1330
- if (isNaN(id)) {
1331
- res.status(400).json({ error: "Invalid cron job id" });
1332
- return;
1333
- }
1334
- const session = await storage.getSessionByAgentCode(code);
1335
- if (!session) {
1336
- res.status(401).json({ error: "Invalid agent code" });
1337
- return;
1338
- }
1339
- const sessionName = session.name;
1340
- try {
1341
- const job = await storage.getCronJobById(id);
1342
- if (!job || job.session_name !== sessionName) {
1343
- res.status(404).json({ error: "Cron job not found or not owned by you" });
1344
- return;
1345
- }
1346
- await storage.disableCronJob(id);
1347
- const updated = await storage.getCronJobById(id);
1348
- res.json({
1349
- ...updated,
1350
- next_run: null,
1351
- last_run: updated?.last_run?.toISOString() ?? null,
1352
- created_at: updated?.created_at.toISOString() ?? null,
1353
- });
1354
- }
1355
- catch (err) {
1356
- console.error("POST /worker/crons/:id/disable error:", err);
1357
- res.status(500).json({ error: "Internal server error" });
1358
- }
1359
- });
1360
- router.get("/worker/crons/:id/history", async (req, res) => {
1361
- const { code } = req.query;
1362
- const id = parseInt(String(req.params.id), 10);
1363
- const limit = Math.min(parseInt(req.query.limit ?? "10", 10), 100);
1364
- if (!code || typeof code !== "string") {
1365
- res.status(400).json({ error: "code query parameter is required" });
1366
- return;
1367
- }
1368
- if (isNaN(id)) {
1369
- res.status(400).json({ error: "Invalid cron job id" });
1370
- return;
1371
- }
1372
- const session = await storage.getSessionByAgentCode(code);
1373
- if (!session) {
1374
- res.status(401).json({ error: "Invalid agent code" });
1375
- return;
1376
- }
1377
- const sessionName = session.name;
1378
- try {
1379
- const job = await storage.getCronJobById(id);
1380
- if (!job || job.session_name !== sessionName) {
1381
- res.status(404).json({ error: "Cron job not found or not owned by you" });
1382
- return;
1383
- }
1384
- const rows = await storage.listCronHistory(id, limit);
1385
- res.json(rows.map((r) => ({
1386
- ...r,
1387
- executed_at: r.executed_at.toISOString(),
1388
- })));
1389
- }
1390
- catch (err) {
1391
- console.error("GET /worker/crons/:id/history error:", err);
1392
- res.status(500).json({ error: "Internal server error" });
1393
- }
1394
- });
1395
- return router;
1396
- }