agent-office 0.0.0 → 0.0.2
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 +102 -3
- package/dist/commands/serve.js +9 -2
- package/dist/commands/worker.d.ts +13 -0
- package/dist/commands/worker.js +120 -5
- package/dist/db/index.d.ts +32 -0
- package/dist/db/migrate.js +66 -0
- package/dist/manage/app.js +55 -35
- 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 +9 -0
- package/dist/manage/components/MyMail.js +143 -0
- package/dist/manage/components/Profile.d.ts +8 -0
- package/dist/manage/components/Profile.js +60 -0
- package/dist/manage/components/ReadMail.d.ts +8 -0
- package/dist/manage/components/ReadMail.js +110 -0
- package/dist/manage/components/SendMessage.d.ts +9 -0
- package/dist/manage/components/SendMessage.js +79 -0
- package/dist/manage/components/SessionList.js +392 -31
- package/dist/manage/components/SessionSidebar.d.ts +6 -0
- package/dist/manage/components/SessionSidebar.js +18 -0
- package/dist/manage/hooks/useApi.d.ts +74 -1
- package/dist/manage/hooks/useApi.js +69 -3
- 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 +3 -3
- package/dist/server/routes.d.ts +3 -2
- package/dist/server/routes.js +976 -23
- package/package.json +3 -2
package/dist/cli.js
CHANGED
|
@@ -4,11 +4,11 @@ import { Command } from "commander";
|
|
|
4
4
|
const program = new Command();
|
|
5
5
|
program
|
|
6
6
|
.name("agent-office")
|
|
7
|
-
.description("Manage OpenCode sessions with named aliases")
|
|
7
|
+
.description("Manage OpenCode sessions with named aliases.\n\n HUMAN OPERATORS: use 'serve' and 'manage'.\n AGENTS: use 'worker' subcommands only.")
|
|
8
8
|
.version("0.1.0");
|
|
9
9
|
program
|
|
10
10
|
.command("serve")
|
|
11
|
-
.description("Start the agent-office HTTP server")
|
|
11
|
+
.description("[HUMAN ONLY] Start the agent-office HTTP server")
|
|
12
12
|
.option("--database-url <url>", "PostgreSQL connection string", process.env.DATABASE_URL)
|
|
13
13
|
.option("--opencode-url <url>", "OpenCode server URL", process.env.OPENCODE_URL ?? "http://localhost:4096")
|
|
14
14
|
.option("--host <host>", "Host to bind to", "127.0.0.1")
|
|
@@ -20,7 +20,7 @@ program
|
|
|
20
20
|
});
|
|
21
21
|
program
|
|
22
22
|
.command("manage")
|
|
23
|
-
.description("Launch the interactive TUI to manage sessions")
|
|
23
|
+
.description("[HUMAN ONLY] Launch the interactive TUI to manage sessions")
|
|
24
24
|
.argument("<url>", "URL of the agent-office server (e.g. http://localhost:7654)")
|
|
25
25
|
.option("--password <password>", "REQUIRED. API password", process.env.AGENT_OFFICE_PASSWORD)
|
|
26
26
|
.action(async (url, options) => {
|
|
@@ -38,4 +38,103 @@ workerCmd
|
|
|
38
38
|
const { clockIn } = await import("./commands/worker.js");
|
|
39
39
|
await clockIn(token);
|
|
40
40
|
});
|
|
41
|
+
workerCmd
|
|
42
|
+
.command("list-coworkers")
|
|
43
|
+
.description("List all other workers (coworkers)")
|
|
44
|
+
.argument("<token>", "Agent token in the format <agent_code>@<server-url>")
|
|
45
|
+
.action(async (token) => {
|
|
46
|
+
const { listCoworkers } = await import("./commands/worker.js");
|
|
47
|
+
await listCoworkers(token);
|
|
48
|
+
});
|
|
49
|
+
workerCmd
|
|
50
|
+
.command("send-message")
|
|
51
|
+
.description("Send a message to one or more coworkers")
|
|
52
|
+
.argument("<token>", "Agent token in the format <agent_code>@<server-url>")
|
|
53
|
+
.requiredOption("--name <name>", "Recipient name (can be specified multiple times)", (val, prev) => [...prev, val], [])
|
|
54
|
+
.requiredOption("--body <body>", "Message body")
|
|
55
|
+
.action(async (token, options) => {
|
|
56
|
+
const { sendMessage } = await import("./commands/worker.js");
|
|
57
|
+
await sendMessage(token, options.name, options.body);
|
|
58
|
+
});
|
|
59
|
+
// ── Worker Cron Commands (nested) ────────────────────────────────────────────────
|
|
60
|
+
const cronCmd = workerCmd
|
|
61
|
+
.command("cron")
|
|
62
|
+
.description("Manage cron jobs");
|
|
63
|
+
cronCmd
|
|
64
|
+
.command("list")
|
|
65
|
+
.argument("<token>", "Agent token in the format <agent_code>@<server-url>")
|
|
66
|
+
.description("List all your cron jobs")
|
|
67
|
+
.action(async (token) => {
|
|
68
|
+
const { listCrons } = await import("./commands/worker.js");
|
|
69
|
+
await listCrons(token);
|
|
70
|
+
});
|
|
71
|
+
cronCmd
|
|
72
|
+
.command("create")
|
|
73
|
+
.argument("<token>", "Agent token in the format <agent_code>@<server-url>")
|
|
74
|
+
.description("Create a new cron job")
|
|
75
|
+
.requiredOption("--name <name>", "Cron job name")
|
|
76
|
+
.requiredOption("--schedule <schedule>", "Cron expression (e.g., '0 9 * * *' for daily at 9am)")
|
|
77
|
+
.requiredOption("--message <message>", "Message to inject when job fires")
|
|
78
|
+
.option("--timezone <timezone>", "IANA timezone (e.g., 'America/New_York')")
|
|
79
|
+
.action(async (token, options) => {
|
|
80
|
+
const { createCron } = await import("./commands/worker.js");
|
|
81
|
+
await createCron(token, options);
|
|
82
|
+
});
|
|
83
|
+
cronCmd
|
|
84
|
+
.command("delete")
|
|
85
|
+
.argument("<token>", "Agent token in the format <agent_code>@<server-url>")
|
|
86
|
+
.argument("<id>", "Cron job ID")
|
|
87
|
+
.description("Delete a cron job")
|
|
88
|
+
.action(async (token, id) => {
|
|
89
|
+
const cronId = parseInt(id, 10);
|
|
90
|
+
if (isNaN(cronId)) {
|
|
91
|
+
console.error("Error: Invalid cron job ID");
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
const { deleteCron } = await import("./commands/worker.js");
|
|
95
|
+
await deleteCron(token, cronId);
|
|
96
|
+
});
|
|
97
|
+
cronCmd
|
|
98
|
+
.command("enable")
|
|
99
|
+
.argument("<token>", "Agent token in the format <agent_code>@<server-url>")
|
|
100
|
+
.argument("<id>", "Cron job ID")
|
|
101
|
+
.description("Enable a cron job")
|
|
102
|
+
.action(async (token, id) => {
|
|
103
|
+
const cronId = parseInt(id, 10);
|
|
104
|
+
if (isNaN(cronId)) {
|
|
105
|
+
console.error("Error: Invalid cron job ID");
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
const { enableCron } = await import("./commands/worker.js");
|
|
109
|
+
await enableCron(token, cronId);
|
|
110
|
+
});
|
|
111
|
+
cronCmd
|
|
112
|
+
.command("disable")
|
|
113
|
+
.argument("<token>", "Agent token in the format <agent_code>@<server-url>")
|
|
114
|
+
.argument("<id>", "Cron job ID")
|
|
115
|
+
.description("Disable a cron job")
|
|
116
|
+
.action(async (token, id) => {
|
|
117
|
+
const cronId = parseInt(id, 10);
|
|
118
|
+
if (isNaN(cronId)) {
|
|
119
|
+
console.error("Error: Invalid cron job ID");
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
const { disableCron } = await import("./commands/worker.js");
|
|
123
|
+
await disableCron(token, cronId);
|
|
124
|
+
});
|
|
125
|
+
cronCmd
|
|
126
|
+
.command("history")
|
|
127
|
+
.argument("<token>", "Agent token in the format <agent_code>@<server-url>")
|
|
128
|
+
.argument("<id>", "Cron job ID")
|
|
129
|
+
.description("View execution history for a cron job")
|
|
130
|
+
.option("--limit <limit>", "Maximum history entries (default 10)", "10")
|
|
131
|
+
.action(async (token, id, options) => {
|
|
132
|
+
const cronId = parseInt(id, 10);
|
|
133
|
+
if (isNaN(cronId)) {
|
|
134
|
+
console.error("Error: Invalid cron job ID");
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
const { cronHistory } = await import("./commands/worker.js");
|
|
138
|
+
await cronHistory(token, cronId);
|
|
139
|
+
});
|
|
41
140
|
program.parse();
|
package/dist/commands/serve.js
CHANGED
|
@@ -2,6 +2,7 @@ import { createDb } from "../db/index.js";
|
|
|
2
2
|
import { runMigrations } from "../db/migrate.js";
|
|
3
3
|
import { createOpencodeClient } from "../lib/opencode.js";
|
|
4
4
|
import { createApp } from "../server/index.js";
|
|
5
|
+
import { CronScheduler } from "../server/cron.js";
|
|
5
6
|
export async function serve(options) {
|
|
6
7
|
const password = options.password;
|
|
7
8
|
if (!password) {
|
|
@@ -34,16 +35,22 @@ export async function serve(options) {
|
|
|
34
35
|
}
|
|
35
36
|
// Init OpenCode client
|
|
36
37
|
const opencode = createOpencodeClient(options.opencodeUrl);
|
|
38
|
+
const serverUrl = `http://${options.host}:${port}`;
|
|
39
|
+
// Create cron scheduler
|
|
40
|
+
const cronScheduler = new CronScheduler();
|
|
37
41
|
// Create Express app
|
|
38
|
-
const app = createApp(sql, opencode, password);
|
|
42
|
+
const app = createApp(sql, opencode, password, serverUrl, cronScheduler);
|
|
43
|
+
// Start cron scheduler
|
|
44
|
+
await cronScheduler.start(sql, opencode);
|
|
39
45
|
// Start server
|
|
40
46
|
const server = app.listen(port, options.host, () => {
|
|
41
|
-
console.log(`agent-office server listening on
|
|
47
|
+
console.log(`agent-office server listening on ${serverUrl}`);
|
|
42
48
|
});
|
|
43
49
|
// Graceful shutdown
|
|
44
50
|
const shutdown = async () => {
|
|
45
51
|
console.log("\nShutting down...");
|
|
46
52
|
server.close(async () => {
|
|
53
|
+
cronScheduler.stop();
|
|
47
54
|
await sql.end();
|
|
48
55
|
console.log("Goodbye.");
|
|
49
56
|
process.exit(0);
|
|
@@ -1 +1,14 @@
|
|
|
1
1
|
export declare function clockIn(token: string): Promise<void>;
|
|
2
|
+
export declare function listCoworkers(token: string): Promise<void>;
|
|
3
|
+
export declare function sendMessage(token: string, recipients: string[], body: string): Promise<void>;
|
|
4
|
+
export declare function listCrons(token: string): Promise<void>;
|
|
5
|
+
export declare function createCron(token: string, options: {
|
|
6
|
+
name: string;
|
|
7
|
+
schedule: string;
|
|
8
|
+
message: string;
|
|
9
|
+
timezone?: string;
|
|
10
|
+
}): Promise<void>;
|
|
11
|
+
export declare function deleteCron(token: string, cronId: number): Promise<void>;
|
|
12
|
+
export declare function enableCron(token: string, cronId: number): Promise<void>;
|
|
13
|
+
export declare function disableCron(token: string, cronId: number): Promise<void>;
|
|
14
|
+
export declare function cronHistory(token: string, cronId: number): Promise<void>;
|
package/dist/commands/worker.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
// UUID v4 pattern
|
|
2
1
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
3
|
-
|
|
4
|
-
// Parse <agent_code>@<url> — URL may contain @ itself (unlikely but safe with lastIndexOf)
|
|
2
|
+
function parseToken(token) {
|
|
5
3
|
const atIndex = token.indexOf("@");
|
|
6
4
|
if (atIndex === -1) {
|
|
7
5
|
console.error("Error: token must be in the format <agent_code>@<server-url>");
|
|
@@ -14,14 +12,76 @@ export async function clockIn(token) {
|
|
|
14
12
|
console.error(`Error: "${agentCode}" is not a valid UUID agent code`);
|
|
15
13
|
process.exit(1);
|
|
16
14
|
}
|
|
17
|
-
let parsedUrl;
|
|
18
15
|
try {
|
|
19
|
-
|
|
16
|
+
new URL(serverUrl);
|
|
20
17
|
}
|
|
21
18
|
catch {
|
|
22
19
|
console.error(`Error: "${serverUrl}" is not a valid URL`);
|
|
23
20
|
process.exit(1);
|
|
24
21
|
}
|
|
22
|
+
return { agentCode, serverUrl };
|
|
23
|
+
}
|
|
24
|
+
async function fetchWorker(token, endpoint) {
|
|
25
|
+
const { agentCode, serverUrl } = parseToken(token);
|
|
26
|
+
const url = `${serverUrl}${endpoint}?code=${encodeURIComponent(agentCode)}`;
|
|
27
|
+
let res;
|
|
28
|
+
try {
|
|
29
|
+
res = await fetch(url);
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
console.error(`Error: could not reach ${serverUrl}`);
|
|
33
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
let body;
|
|
37
|
+
try {
|
|
38
|
+
body = await res.json();
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
console.error(`Error: invalid response from server`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
if (!res.ok) {
|
|
45
|
+
const msg = body.error ?? `HTTP ${res.status}`;
|
|
46
|
+
console.error(`Error: ${msg}`);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
return body;
|
|
50
|
+
}
|
|
51
|
+
async function postWorker(token, endpoint, payload) {
|
|
52
|
+
const { agentCode, serverUrl } = parseToken(token);
|
|
53
|
+
const url = `${serverUrl}${endpoint}?code=${encodeURIComponent(agentCode)}`;
|
|
54
|
+
let res;
|
|
55
|
+
try {
|
|
56
|
+
res = await fetch(url, {
|
|
57
|
+
method: "POST",
|
|
58
|
+
headers: { "Content-Type": "application/json" },
|
|
59
|
+
body: JSON.stringify(payload),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
console.error(`Error: could not reach ${serverUrl}`);
|
|
64
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
let body;
|
|
68
|
+
try {
|
|
69
|
+
body = await res.json();
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
console.error(`Error: invalid response from server`);
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
if (!res.ok) {
|
|
76
|
+
const msg = body.error ?? `HTTP ${res.status}`;
|
|
77
|
+
console.error(`Error: ${msg}`);
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
return body;
|
|
81
|
+
}
|
|
82
|
+
export async function clockIn(token) {
|
|
83
|
+
const { agentCode, serverUrl } = parseToken(token);
|
|
84
|
+
const parsedUrl = new URL(serverUrl);
|
|
25
85
|
const clockInUrl = `${parsedUrl.origin}/worker/clock-in?code=${encodeURIComponent(agentCode)}`;
|
|
26
86
|
let res;
|
|
27
87
|
try {
|
|
@@ -48,3 +108,58 @@ export async function clockIn(token) {
|
|
|
48
108
|
const { message } = body;
|
|
49
109
|
console.log(message);
|
|
50
110
|
}
|
|
111
|
+
export async function listCoworkers(token) {
|
|
112
|
+
const workers = await fetchWorker(token, "/worker/list-coworkers");
|
|
113
|
+
console.log(JSON.stringify(workers, null, 2));
|
|
114
|
+
}
|
|
115
|
+
export async function sendMessage(token, recipients, body) {
|
|
116
|
+
const result = await postWorker(token, "/worker/send-message", { to: recipients, body });
|
|
117
|
+
console.log(JSON.stringify(result, null, 2));
|
|
118
|
+
}
|
|
119
|
+
export async function listCrons(token) {
|
|
120
|
+
const crons = await fetchWorker(token, "/worker/crons");
|
|
121
|
+
console.log(JSON.stringify(crons, null, 2));
|
|
122
|
+
}
|
|
123
|
+
export async function createCron(token, options) {
|
|
124
|
+
const cron = await postWorker(token, "/worker/crons", options);
|
|
125
|
+
console.log(JSON.stringify(cron, null, 2));
|
|
126
|
+
}
|
|
127
|
+
export async function deleteCron(token, cronId) {
|
|
128
|
+
const { agentCode, serverUrl } = parseToken(token);
|
|
129
|
+
const url = `${serverUrl}/worker/crons/${cronId}?code=${encodeURIComponent(agentCode)}`;
|
|
130
|
+
let res;
|
|
131
|
+
try {
|
|
132
|
+
res = await fetch(url, { method: "DELETE" });
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
console.error(`Error: could not reach ${serverUrl}`);
|
|
136
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
let body;
|
|
140
|
+
try {
|
|
141
|
+
body = await res.json();
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
console.error(`Error: invalid response from server`);
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
if (!res.ok) {
|
|
148
|
+
const msg = body.error ?? `HTTP ${res.status}`;
|
|
149
|
+
console.error(`Error: ${msg}`);
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
console.log(JSON.stringify(body, null, 2));
|
|
153
|
+
}
|
|
154
|
+
export async function enableCron(token, cronId) {
|
|
155
|
+
const result = await fetchWorker(token, `/worker/crons/${cronId}/enable`);
|
|
156
|
+
console.log(JSON.stringify(result, null, 2));
|
|
157
|
+
}
|
|
158
|
+
export async function disableCron(token, cronId) {
|
|
159
|
+
const result = await fetchWorker(token, `/worker/crons/${cronId}/disable`);
|
|
160
|
+
console.log(JSON.stringify(result, null, 2));
|
|
161
|
+
}
|
|
162
|
+
export async function cronHistory(token, cronId) {
|
|
163
|
+
const history = await fetchWorker(token, `/worker/crons/${cronId}/history`);
|
|
164
|
+
console.log(JSON.stringify(history, null, 2));
|
|
165
|
+
}
|
package/dist/db/index.d.ts
CHANGED
|
@@ -6,5 +6,37 @@ export interface SessionRow {
|
|
|
6
6
|
name: string;
|
|
7
7
|
session_id: string;
|
|
8
8
|
agent_code: string;
|
|
9
|
+
mode: string | null;
|
|
9
10
|
created_at: Date;
|
|
10
11
|
}
|
|
12
|
+
export interface ConfigRow {
|
|
13
|
+
key: string;
|
|
14
|
+
value: string;
|
|
15
|
+
}
|
|
16
|
+
export interface MessageRow {
|
|
17
|
+
id: number;
|
|
18
|
+
from_name: string;
|
|
19
|
+
to_name: string;
|
|
20
|
+
body: string;
|
|
21
|
+
read: boolean;
|
|
22
|
+
injected: boolean;
|
|
23
|
+
created_at: Date;
|
|
24
|
+
}
|
|
25
|
+
export interface CronJobRow {
|
|
26
|
+
id: number;
|
|
27
|
+
name: string;
|
|
28
|
+
session_name: string;
|
|
29
|
+
schedule: string;
|
|
30
|
+
timezone: string | null;
|
|
31
|
+
message: string;
|
|
32
|
+
enabled: boolean;
|
|
33
|
+
created_at: Date;
|
|
34
|
+
last_run: Date | null;
|
|
35
|
+
}
|
|
36
|
+
export interface CronHistoryRow {
|
|
37
|
+
id: number;
|
|
38
|
+
cron_job_id: number;
|
|
39
|
+
executed_at: Date;
|
|
40
|
+
success: boolean;
|
|
41
|
+
error_message: string | null;
|
|
42
|
+
}
|
package/dist/db/migrate.js
CHANGED
|
@@ -20,6 +20,72 @@ const MIGRATIONS = [
|
|
|
20
20
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_sessions_agent_code ON sessions(agent_code);
|
|
21
21
|
`,
|
|
22
22
|
},
|
|
23
|
+
{
|
|
24
|
+
version: 3,
|
|
25
|
+
name: "create_config_table",
|
|
26
|
+
sql: `
|
|
27
|
+
CREATE TABLE IF NOT EXISTS config (
|
|
28
|
+
key VARCHAR(255) PRIMARY KEY,
|
|
29
|
+
value TEXT NOT NULL
|
|
30
|
+
);
|
|
31
|
+
INSERT INTO config (key, value) VALUES ('human_name', 'Human') ON CONFLICT DO NOTHING;
|
|
32
|
+
INSERT INTO config (key, value) VALUES ('human_description', '') ON CONFLICT DO NOTHING;
|
|
33
|
+
`,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
version: 5,
|
|
37
|
+
name: "add_mode_to_sessions",
|
|
38
|
+
sql: `
|
|
39
|
+
ALTER TABLE sessions ADD COLUMN IF NOT EXISTS mode VARCHAR(255) NULL;
|
|
40
|
+
`,
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
version: 4,
|
|
44
|
+
name: "create_messages_table",
|
|
45
|
+
sql: `
|
|
46
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
47
|
+
id SERIAL PRIMARY KEY,
|
|
48
|
+
from_name VARCHAR(255) NOT NULL,
|
|
49
|
+
to_name VARCHAR(255) NOT NULL,
|
|
50
|
+
body TEXT NOT NULL,
|
|
51
|
+
read BOOLEAN NOT NULL DEFAULT FALSE,
|
|
52
|
+
injected BOOLEAN NOT NULL DEFAULT FALSE,
|
|
53
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
54
|
+
);
|
|
55
|
+
CREATE INDEX IF NOT EXISTS idx_messages_to_name ON messages(to_name);
|
|
56
|
+
CREATE INDEX IF NOT EXISTS idx_messages_from_name ON messages(from_name);
|
|
57
|
+
CREATE INDEX IF NOT EXISTS idx_messages_read ON messages(read);
|
|
58
|
+
`,
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
version: 6,
|
|
62
|
+
name: "create_cron_tables",
|
|
63
|
+
sql: `
|
|
64
|
+
CREATE TABLE IF NOT EXISTS cron_jobs (
|
|
65
|
+
id SERIAL PRIMARY KEY,
|
|
66
|
+
name VARCHAR(255) NOT NULL,
|
|
67
|
+
session_name VARCHAR(255) NOT NULL REFERENCES sessions(name) ON DELETE CASCADE,
|
|
68
|
+
schedule TEXT NOT NULL,
|
|
69
|
+
timezone VARCHAR(100),
|
|
70
|
+
message TEXT NOT NULL,
|
|
71
|
+
enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
|
72
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
73
|
+
last_run TIMESTAMPTZ
|
|
74
|
+
);
|
|
75
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_cron_jobs_name_session ON cron_jobs(name, session_name);
|
|
76
|
+
CREATE INDEX IF NOT EXISTS idx_cron_jobs_session_name ON cron_jobs(session_name);
|
|
77
|
+
CREATE INDEX IF NOT EXISTS idx_cron_jobs_enabled ON cron_jobs(enabled);
|
|
78
|
+
|
|
79
|
+
CREATE TABLE IF NOT EXISTS cron_history (
|
|
80
|
+
id SERIAL PRIMARY KEY,
|
|
81
|
+
cron_job_id INTEGER NOT NULL REFERENCES cron_jobs(id) ON DELETE CASCADE,
|
|
82
|
+
executed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
83
|
+
success BOOLEAN NOT NULL DEFAULT TRUE,
|
|
84
|
+
error_message TEXT
|
|
85
|
+
);
|
|
86
|
+
CREATE INDEX IF NOT EXISTS idx_cron_history_job_id ON cron_history(cron_job_id);
|
|
87
|
+
`,
|
|
88
|
+
},
|
|
23
89
|
];
|
|
24
90
|
export async function runMigrations(sql) {
|
|
25
91
|
await sql `
|
package/dist/manage/app.js
CHANGED
|
@@ -1,37 +1,36 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useState, useEffect } from "react";
|
|
2
|
+
import { useState, useEffect, useRef } from "react";
|
|
3
3
|
import { Box, Text, useApp, useStdout, useInput } from "ink";
|
|
4
|
-
import {
|
|
4
|
+
import { Spinner } from "@inkjs/ui";
|
|
5
5
|
import { useApi } from "./hooks/useApi.js";
|
|
6
6
|
import { SessionList } from "./components/SessionList.js";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
7
|
+
import { SendMessage } from "./components/SendMessage.js";
|
|
8
|
+
import { Profile } from "./components/Profile.js";
|
|
9
|
+
import { MyMail } from "./components/MyMail.js";
|
|
10
|
+
import { SessionSidebar } from "./components/SessionSidebar.js";
|
|
11
|
+
import { MenuSelect } from "./components/MenuSelect.js";
|
|
12
|
+
import { CronList } from "./components/CronList.js";
|
|
12
13
|
const MENU_OPTIONS = [
|
|
13
|
-
{ label: "
|
|
14
|
-
{ label: "
|
|
15
|
-
{ label: "
|
|
16
|
-
{ label: "
|
|
17
|
-
{ label: "
|
|
18
|
-
{ label: "Agent code", value: "agent-code" },
|
|
14
|
+
{ label: "Send message", value: "send-message" },
|
|
15
|
+
{ label: "My mail", value: "my-mail" },
|
|
16
|
+
{ label: "Coworkers", value: "list" },
|
|
17
|
+
{ label: "Cron jobs", value: "cron" },
|
|
18
|
+
{ label: "My profile", value: "profile" },
|
|
19
19
|
{ label: "Quit", value: "quit" },
|
|
20
20
|
];
|
|
21
|
-
const SUB_SCREENS = ["list", "
|
|
21
|
+
const SUB_SCREENS = ["list", "send-message", "my-mail", "profile", "cron"];
|
|
22
22
|
const FOOTER_HINTS = {
|
|
23
23
|
connecting: "",
|
|
24
24
|
"auth-error": "",
|
|
25
25
|
menu: "↑↓ navigate · Enter select · q quit",
|
|
26
|
-
list: "↑↓ navigate · r reveal
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
"agent-code": "r reveal/hide · g regenerate · Esc back to menu",
|
|
26
|
+
list: "↑↓ navigate · c create · d delete · r reveal code · g regen · t tail · i inject · m mail · Esc back",
|
|
27
|
+
"send-message": "Enter submit · Esc back to menu",
|
|
28
|
+
"my-mail": "↑↓ select message · r reply · m mark read · a mark all read · s sent tab · Esc back",
|
|
29
|
+
"profile": "Enter submit · Esc back to menu",
|
|
30
|
+
cron: "↑↓ navigate · c create · d delete · e enable/disable · h history · Esc back",
|
|
32
31
|
};
|
|
33
|
-
function Header({ serverUrl }) {
|
|
34
|
-
return (_jsxs(Box, { borderStyle: "single", borderColor: "cyan", paddingX: 1, justifyContent: "space-between", children: [_jsx(Text, { bold: true, color: "cyan", children: "agent-office" }), _jsx(Text, { dimColor: true, children: serverUrl })] }));
|
|
32
|
+
function Header({ serverUrl, unreadCount }) {
|
|
33
|
+
return (_jsxs(Box, { borderStyle: "single", borderColor: "cyan", paddingX: 1, justifyContent: "space-between", children: [_jsx(Text, { bold: true, color: "cyan", children: "agent-office" }), _jsxs(Box, { gap: 2, children: [unreadCount > 0 && (_jsxs(Text, { color: "yellow", bold: true, children: ["\u2709 ", unreadCount, " unread"] })), _jsx(Text, { dimColor: true, children: serverUrl })] })] }));
|
|
35
34
|
}
|
|
36
35
|
function Footer({ hint }) {
|
|
37
36
|
return (_jsx(Box, { borderStyle: "single", borderColor: "gray", paddingX: 1, children: _jsx(Text, { dimColor: true, children: hint }) }));
|
|
@@ -39,10 +38,13 @@ function Footer({ hint }) {
|
|
|
39
38
|
export function App({ serverUrl, password }) {
|
|
40
39
|
const { exit } = useApp();
|
|
41
40
|
const { stdout } = useStdout();
|
|
42
|
-
const { checkHealth } = useApi(serverUrl, password);
|
|
41
|
+
const { checkHealth, getConfig, getMailMessages } = useApi(serverUrl, password);
|
|
43
42
|
const [screen, setScreen] = useState("connecting");
|
|
44
43
|
const [termHeight, setTermHeight] = useState(stdout?.rows ?? 24);
|
|
45
44
|
const [termWidth, setTermWidth] = useState(stdout?.columns ?? 80);
|
|
45
|
+
const [unreadCount, setUnreadCount] = useState(0);
|
|
46
|
+
const [replyTo, setReplyTo] = useState(null);
|
|
47
|
+
const humanNameRef = useRef("Human");
|
|
46
48
|
// Track terminal size
|
|
47
49
|
useEffect(() => {
|
|
48
50
|
const update = () => {
|
|
@@ -58,6 +60,24 @@ export function App({ serverUrl, password }) {
|
|
|
58
60
|
setScreen(ok ? "menu" : "auth-error");
|
|
59
61
|
});
|
|
60
62
|
}, []);
|
|
63
|
+
// Resolve human name once, then poll unread mail every 15s
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
const fetchUnread = async () => {
|
|
66
|
+
try {
|
|
67
|
+
const msgs = await getMailMessages(humanNameRef.current, { unreadOnly: true });
|
|
68
|
+
setUnreadCount(msgs.length);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// ignore poll errors silently
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
getConfig().then((cfg) => {
|
|
75
|
+
humanNameRef.current = cfg.human_name ?? "Human";
|
|
76
|
+
fetchUnread();
|
|
77
|
+
}).catch(() => { });
|
|
78
|
+
const timer = setInterval(fetchUnread, 15_000);
|
|
79
|
+
return () => clearInterval(timer);
|
|
80
|
+
}, []);
|
|
61
81
|
useInput((input, key) => {
|
|
62
82
|
if (key.escape && SUB_SCREENS.includes(screen)) {
|
|
63
83
|
setScreen("menu");
|
|
@@ -72,6 +92,8 @@ export function App({ serverUrl, password }) {
|
|
|
72
92
|
exit();
|
|
73
93
|
return;
|
|
74
94
|
}
|
|
95
|
+
if (value === "my-mail")
|
|
96
|
+
setUnreadCount(0);
|
|
75
97
|
setScreen(value);
|
|
76
98
|
};
|
|
77
99
|
// content area = terminal height minus header (3 rows) and footer (3 rows)
|
|
@@ -83,20 +105,18 @@ export function App({ serverUrl, password }) {
|
|
|
83
105
|
case "auth-error":
|
|
84
106
|
return (_jsxs(Box, { height: contentHeight, flexDirection: "column", alignItems: "center", justifyContent: "center", gap: 1, children: [_jsx(Text, { color: "red", bold: true, children: "Connection failed" }), _jsxs(Text, { children: ["Could not reach ", _jsx(Text, { color: "cyan", children: serverUrl })] }), _jsx(Text, { dimColor: true, children: "Check that agent-office serve is running and your --password is correct." })] }));
|
|
85
107
|
case "menu":
|
|
86
|
-
return (_jsxs(Box, { height: contentHeight, flexDirection: "column", paddingX: 2, paddingY: 1,
|
|
108
|
+
return (_jsxs(Box, { height: contentHeight, flexDirection: "row", flexGrow: 1, children: [_jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, flexGrow: 1, children: [_jsx(Text, { bold: true, children: "Main Menu" }), _jsx(MenuSelect, { options: MENU_OPTIONS, onChange: handleMenuSelect, badges: unreadCount > 0 ? { "my-mail": `(${unreadCount})` } : {} })] }), _jsx(SessionSidebar, { serverUrl: serverUrl, password: password })] }));
|
|
87
109
|
case "list":
|
|
88
110
|
return (_jsx(Box, { height: contentHeight, flexDirection: "column", paddingX: 2, paddingY: 1, children: _jsx(SessionList, { serverUrl: serverUrl, password: password, onBack: goBack, contentHeight: contentHeight - 2 }) }));
|
|
89
|
-
case "
|
|
90
|
-
return (_jsx(Box, { height: contentHeight, flexDirection: "column", paddingX: 2, paddingY: 1, children: _jsx(
|
|
91
|
-
case "
|
|
92
|
-
return (_jsx(Box, { height: contentHeight, flexDirection: "column", paddingX: 2, paddingY: 1, children: _jsx(
|
|
93
|
-
case "
|
|
94
|
-
return (_jsx(Box, { height: contentHeight, flexDirection: "column", paddingX: 2, paddingY: 1, children: _jsx(
|
|
95
|
-
case "
|
|
96
|
-
return (_jsx(Box, { height: contentHeight, flexDirection: "column", paddingX: 2, paddingY: 1, children: _jsx(
|
|
97
|
-
case "agent-code":
|
|
98
|
-
return (_jsx(Box, { height: contentHeight, flexDirection: "column", paddingX: 2, paddingY: 1, children: _jsx(AgentCode, { serverUrl: serverUrl, password: password, onBack: goBack, contentHeight: contentHeight - 2 }) }));
|
|
111
|
+
case "send-message":
|
|
112
|
+
return (_jsx(Box, { height: contentHeight, flexDirection: "column", paddingX: 2, paddingY: 1, children: _jsx(SendMessage, { serverUrl: serverUrl, password: password, onBack: () => { setReplyTo(null); goBack(); }, contentHeight: contentHeight - 2, initialRecipient: replyTo ?? undefined }) }));
|
|
113
|
+
case "profile":
|
|
114
|
+
return (_jsx(Box, { height: contentHeight, flexDirection: "column", paddingX: 2, paddingY: 1, children: _jsx(Profile, { serverUrl: serverUrl, password: password, onBack: goBack, contentHeight: contentHeight - 2 }) }));
|
|
115
|
+
case "my-mail":
|
|
116
|
+
return (_jsx(Box, { height: contentHeight, flexDirection: "column", paddingX: 2, paddingY: 1, children: _jsx(MyMail, { serverUrl: serverUrl, password: password, onBack: goBack, contentHeight: contentHeight - 2, onReply: (name) => { setReplyTo(name); setScreen("send-message"); } }) }));
|
|
117
|
+
case "cron":
|
|
118
|
+
return (_jsx(Box, { height: contentHeight, flexDirection: "column", paddingX: 2, paddingY: 1, children: _jsx(CronList, { serverUrl: serverUrl, password: password, onBack: goBack, contentHeight: contentHeight - 2 }) }));
|
|
99
119
|
}
|
|
100
120
|
};
|
|
101
|
-
return (_jsxs(Box, { flexDirection: "column", width: termWidth, children: [_jsx(Header, { serverUrl: serverUrl }), renderContent(), screen !== "connecting" && (_jsx(Footer, { hint: FOOTER_HINTS[screen] }))] }));
|
|
121
|
+
return (_jsxs(Box, { flexDirection: "column", width: termWidth, children: [_jsx(Header, { serverUrl: serverUrl, unreadCount: unreadCount }), renderContent(), screen !== "connecting" && (_jsx(Footer, { hint: FOOTER_HINTS[screen] }))] }));
|
|
102
122
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
interface CronListProps {
|
|
2
|
+
serverUrl: string;
|
|
3
|
+
password: string;
|
|
4
|
+
onBack: () => void;
|
|
5
|
+
contentHeight: number;
|
|
6
|
+
sessionName?: string | null;
|
|
7
|
+
}
|
|
8
|
+
export declare function CronList({ serverUrl, password, onBack, contentHeight, sessionName: initialSessionName }: CronListProps): import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
export {};
|