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.
- package/README.md +46 -22
- package/dist/cli.js +101 -4
- package/dist/commands/communicator.d.ts +1 -0
- package/dist/commands/communicator.js +1385 -2
- package/dist/commands/task-board.d.ts +29 -0
- package/dist/commands/task-board.js +251 -0
- package/dist/commands/worker.d.ts +2 -1
- package/dist/commands/worker.js +7 -3
- package/dist/db/index.d.ts +23 -0
- package/dist/db/postgresql-storage.d.ts +17 -1
- package/dist/db/postgresql-storage.js +175 -0
- package/dist/db/sqlite-storage.d.ts +17 -1
- package/dist/db/sqlite-storage.js +241 -0
- package/dist/db/storage-base.d.ts +17 -1
- package/dist/db/storage.d.ts +17 -1
- package/dist/manage/app.js +6 -1
- package/dist/manage/components/CronRequests.d.ts +8 -0
- package/dist/manage/components/CronRequests.js +181 -0
- package/dist/manage/hooks/useApi.d.ts +22 -0
- package/dist/manage/hooks/useApi.js +18 -0
- package/dist/server/routes.js +184 -35
- package/package.json +1 -1
package/dist/server/routes.js
CHANGED
|
@@ -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
|
-
`
|
|
88
|
-
` agent-office worker cron
|
|
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
|
-
`
|
|
101
|
-
`
|
|
102
|
-
`
|
|
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:
|
|
149
|
-
` jobs. Run 'agent-office worker cron
|
|
150
|
-
`
|
|
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
|
-
|
|
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
|
|
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
|
-
...
|
|
1078
|
-
|
|
1079
|
-
|
|
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/
|
|
1233
|
+
console.error("POST /worker/cron-requests error:", err);
|
|
1085
1234
|
res.status(500).json({ error: "Internal server error" });
|
|
1086
1235
|
}
|
|
1087
1236
|
});
|