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.
Files changed (33) hide show
  1. package/dist/cli.js +102 -3
  2. package/dist/commands/serve.js +9 -2
  3. package/dist/commands/worker.d.ts +13 -0
  4. package/dist/commands/worker.js +120 -5
  5. package/dist/db/index.d.ts +32 -0
  6. package/dist/db/migrate.js +66 -0
  7. package/dist/manage/app.js +55 -35
  8. package/dist/manage/components/CronList.d.ts +9 -0
  9. package/dist/manage/components/CronList.js +310 -0
  10. package/dist/manage/components/ItemSelector.d.ts +7 -0
  11. package/dist/manage/components/ItemSelector.js +20 -0
  12. package/dist/manage/components/MenuSelect.d.ts +13 -0
  13. package/dist/manage/components/MenuSelect.js +22 -0
  14. package/dist/manage/components/MyMail.d.ts +9 -0
  15. package/dist/manage/components/MyMail.js +143 -0
  16. package/dist/manage/components/Profile.d.ts +8 -0
  17. package/dist/manage/components/Profile.js +60 -0
  18. package/dist/manage/components/ReadMail.d.ts +8 -0
  19. package/dist/manage/components/ReadMail.js +110 -0
  20. package/dist/manage/components/SendMessage.d.ts +9 -0
  21. package/dist/manage/components/SendMessage.js +79 -0
  22. package/dist/manage/components/SessionList.js +392 -31
  23. package/dist/manage/components/SessionSidebar.d.ts +6 -0
  24. package/dist/manage/components/SessionSidebar.js +18 -0
  25. package/dist/manage/hooks/useApi.d.ts +74 -1
  26. package/dist/manage/hooks/useApi.js +69 -3
  27. package/dist/server/cron.d.ts +24 -0
  28. package/dist/server/cron.js +121 -0
  29. package/dist/server/index.d.ts +2 -1
  30. package/dist/server/index.js +3 -3
  31. package/dist/server/routes.d.ts +3 -2
  32. package/dist/server/routes.js +976 -23
  33. 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();
@@ -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 http://${options.host}:${port}`);
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>;
@@ -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
- export async function clockIn(token) {
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
- parsedUrl = new URL(serverUrl);
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
+ }
@@ -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
+ }
@@ -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 `
@@ -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 { Select, Spinner } from "@inkjs/ui";
4
+ import { Spinner } from "@inkjs/ui";
5
5
  import { useApi } from "./hooks/useApi.js";
6
6
  import { SessionList } from "./components/SessionList.js";
7
- import { CreateSession } from "./components/CreateSession.js";
8
- import { DeleteSession } from "./components/DeleteSession.js";
9
- import { TailMessages } from "./components/TailMessages.js";
10
- import { InjectText } from "./components/InjectText.js";
11
- import { AgentCode } from "./components/AgentCode.js";
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: "List sessions", value: "list" },
14
- { label: "Create session", value: "create" },
15
- { label: "Delete session", value: "delete" },
16
- { label: "Tail messages", value: "tail" },
17
- { label: "Inject text", value: "inject" },
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", "create", "delete", "tail", "inject", "agent-code"];
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/hide agent code · Esc back to menu",
27
- create: "Enter submit · Esc back to menu",
28
- delete: "↑↓ navigate · Enter select · Esc back to menu",
29
- tail: "↑↓ scroll · Esc back to menu",
30
- inject: "Enter submit · Esc back to menu",
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, gap: 1, children: [_jsx(Text, { bold: true, children: "Main Menu" }), _jsx(Select, { options: MENU_OPTIONS, onChange: handleMenuSelect })] }));
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 "create":
90
- return (_jsx(Box, { height: contentHeight, flexDirection: "column", paddingX: 2, paddingY: 1, children: _jsx(CreateSession, { serverUrl: serverUrl, password: password, onBack: goBack }) }));
91
- case "delete":
92
- return (_jsx(Box, { height: contentHeight, flexDirection: "column", paddingX: 2, paddingY: 1, children: _jsx(DeleteSession, { serverUrl: serverUrl, password: password, onBack: goBack }) }));
93
- case "tail":
94
- return (_jsx(Box, { height: contentHeight, flexDirection: "column", paddingX: 2, paddingY: 1, children: _jsx(TailMessages, { serverUrl: serverUrl, password: password, onBack: goBack, contentHeight: contentHeight - 2 }) }));
95
- case "inject":
96
- return (_jsx(Box, { height: contentHeight, flexDirection: "column", paddingX: 2, paddingY: 1, children: _jsx(InjectText, { serverUrl: serverUrl, password: password, onBack: goBack, contentHeight: contentHeight - 2 }) }));
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 {};