agent-office 0.4.6 → 0.4.8

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.
@@ -81,11 +81,11 @@ export function generateSystemPrompt(name, status, humanName, humanDescription,
81
81
  ` expanded by the shell and your message will be garbled.`,
82
82
  ``,
83
83
  ` Manage scheduled tasks (optional)`,
84
- ` agent-office worker cron \\`,
84
+ ` agent-office worker cron list \\`,
85
85
  ` ${token}`,
86
86
  ``,
87
- ` Create a cron job (scheduled task)`,
88
- ` agent-office worker cron create \\`,
87
+ ` Request a cron job (requires human approval)`,
88
+ ` agent-office worker cron request \\`,
89
89
  ` --name <job-name> \\`,
90
90
  ` --schedule "<cron-expression>" \\`,
91
91
  ` --message "<action-to-perform>" \\`,
@@ -97,9 +97,13 @@ export function generateSystemPrompt(name, status, humanName, humanDescription,
97
97
  ` --message "Prepare your standup update" \\`,
98
98
  ` --respond-to "${humanName} in the standup channel"`,
99
99
  ``,
100
- ` The final injected message will be formatted as:`,
101
- ` Action: <message>`,
102
- ` Who to respond to when done: <respond-to>`,
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}`,
103
107
  ``,
104
108
  `════════════════════════════════════════════════════════`,
105
109
  ` IMPORTANT: BASH ESCAPING IN COMMAND ARGUMENTS`,
@@ -145,9 +149,10 @@ export function generateSystemPrompt(name, status, humanName, humanDescription,
145
149
  ` messages at any time and those will appear here in`,
146
150
  ` your session just like this one. You can reach them`,
147
151
  ` by sending a message to --name ${humanName}.`,
148
- ` - Optional: Set up recurring scheduled tasks with cron`,
149
- ` jobs. Run 'agent-office worker cron list ${token}' to`,
150
- ` get started.`,
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.`,
151
156
  ``,
152
157
  `════════════════════════════════════════════════════════`,
153
158
  ` IMPORTANT: WAIT FOR A MESSAGE BEFORE DOING ANYTHING`,
@@ -853,6 +858,145 @@ export function createRouter(storage, agenticCodingServer, serverUrl, scheduler)
853
858
  res.status(500).json({ error: "Internal server error" });
854
859
  }
855
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
+ });
856
1000
  return router;
857
1001
  }
858
1002
  export function createWorkerRouter(storage, agenticCodingServer, serverUrl) {
@@ -1009,7 +1153,32 @@ export function createWorkerRouter(storage, agenticCodingServer, serverUrl) {
1009
1153
  res.status(500).json({ error: "Internal server error" });
1010
1154
  }
1011
1155
  });
1012
- router.post("/worker/crons", async (req, res) => {
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) => {
1013
1182
  const { code } = req.query;
1014
1183
  const { name, schedule, message, timezone } = req.body;
1015
1184
  if (!code || typeof code !== "string") {
@@ -1036,7 +1205,6 @@ export function createWorkerRouter(storage, agenticCodingServer, serverUrl) {
1036
1205
  const trimmedName = name.trim();
1037
1206
  const trimmedSchedule = schedule.trim();
1038
1207
  const trimmedMessage = message.trim();
1039
- const sessionName = session.name;
1040
1208
  try {
1041
1209
  new CronerInstance(trimmedSchedule);
1042
1210
  }
@@ -1053,35 +1221,16 @@ export function createWorkerRouter(storage, agenticCodingServer, serverUrl) {
1053
1221
  return;
1054
1222
  }
1055
1223
  }
1056
- const existing = await storage.cronJobExistsForSession(trimmedName, sessionName);
1057
- if (existing) {
1058
- res.status(409).json({ error: `Cron job "${trimmedName}" already exists` });
1059
- return;
1060
- }
1061
1224
  try {
1062
- const cronJobrow = await storage.createCronJob(trimmedName, sessionName, trimmedSchedule, timezone ?? null, trimmedMessage);
1063
- const nextRun = cronJobrow.enabled ? (() => {
1064
- try {
1065
- const options = {};
1066
- if (cronJobrow.timezone)
1067
- options.timezone = cronJobrow.timezone;
1068
- const cronJob = new CronerInstance(cronJobrow.schedule, options);
1069
- const next = cronJob.nextRun();
1070
- return next ? next.toISOString() : null;
1071
- }
1072
- catch {
1073
- return null;
1074
- }
1075
- })() : null;
1225
+ const request = await storage.createCronRequest(trimmedName, session.name, trimmedSchedule, timezone ?? null, trimmedMessage);
1076
1226
  res.status(201).json({
1077
- ...cronJobrow,
1078
- next_run: nextRun,
1079
- last_run: cronJobrow?.last_run?.toISOString() ?? null,
1080
- created_at: cronJobrow.created_at.toISOString(),
1227
+ ...request,
1228
+ requested_at: request.requested_at.toISOString(),
1229
+ reviewed_at: request.reviewed_at?.toISOString() ?? null,
1081
1230
  });
1082
1231
  }
1083
1232
  catch (err) {
1084
- console.error("POST /worker/crons error:", err);
1233
+ console.error("POST /worker/cron-requests error:", err);
1085
1234
  res.status(500).json({ error: "Internal server error" });
1086
1235
  }
1087
1236
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-office",
3
- "version": "0.4.6",
3
+ "version": "0.4.8",
4
4
  "description": "An office for your AI agents",
5
5
  "type": "module",
6
6
  "license": "MIT",