agent-office 0.3.1 → 0.4.0

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.
@@ -96,7 +96,8 @@ export async function listCrons(token) {
96
96
  console.log(JSON.stringify(crons, null, 2));
97
97
  }
98
98
  export async function createCron(token, options) {
99
- const cron = await postWorker(token, "/worker/crons", options);
99
+ const finalMessage = `Action: ${options.message}\n\nWho to respond to when done: ${options.respondTo}`;
100
+ const cron = await postWorker(token, "/worker/crons", { name: options.name, schedule: options.schedule, message: finalMessage, timezone: options.timezone });
100
101
  console.log(JSON.stringify(cron, null, 2));
101
102
  }
102
103
  export async function deleteCron(token, cronId) {
@@ -138,66 +139,3 @@ export async function cronHistory(token, cronId) {
138
139
  const history = await fetchWorker(token, `/worker/crons/${cronId}/history`);
139
140
  console.log(JSON.stringify(history, null, 2));
140
141
  }
141
- // ── Memory Commands ──────────────────────────────────────────────────────────
142
- export async function memoryAdd(token, content) {
143
- const result = await postWorker(token, "/worker/memory/add", { content });
144
- console.log(JSON.stringify(result, null, 2));
145
- }
146
- export async function memorySearch(token, query, limit) {
147
- const result = await postWorker(token, "/worker/memory/search", { query, limit });
148
- console.log(JSON.stringify(result, null, 2));
149
- }
150
- export async function memoryList(token, limit) {
151
- const { agentCode, serverUrl } = parseToken(token);
152
- const url = `${serverUrl}/worker/memory/list?code=${encodeURIComponent(agentCode)}&limit=${limit}`;
153
- let res;
154
- try {
155
- res = await fetch(url);
156
- }
157
- catch (err) {
158
- console.error(`Error: could not reach ${serverUrl}`);
159
- console.error(err instanceof Error ? err.message : String(err));
160
- process.exit(1);
161
- }
162
- let body;
163
- try {
164
- body = await res.json();
165
- }
166
- catch {
167
- console.error(`Error: invalid response from server`);
168
- process.exit(1);
169
- }
170
- if (!res.ok) {
171
- const msg = body.error ?? `HTTP ${res.status}`;
172
- console.error(`Error: ${msg}`);
173
- process.exit(1);
174
- }
175
- console.log(JSON.stringify(body, null, 2));
176
- }
177
- export async function memoryForget(token, memoryId) {
178
- const { agentCode, serverUrl } = parseToken(token);
179
- const url = `${serverUrl}/worker/memory/${encodeURIComponent(memoryId)}?code=${encodeURIComponent(agentCode)}`;
180
- let res;
181
- try {
182
- res = await fetch(url, { method: "DELETE" });
183
- }
184
- catch (err) {
185
- console.error(`Error: could not reach ${serverUrl}`);
186
- console.error(err instanceof Error ? err.message : String(err));
187
- process.exit(1);
188
- }
189
- let body;
190
- try {
191
- body = await res.json();
192
- }
193
- catch {
194
- console.error(`Error: invalid response from server`);
195
- process.exit(1);
196
- }
197
- if (!res.ok) {
198
- const msg = body.error ?? `HTTP ${res.status}`;
199
- console.error(`Error: ${msg}`);
200
- process.exit(1);
201
- }
202
- console.log(JSON.stringify(body, null, 2));
203
- }
@@ -24,6 +24,7 @@ export interface MessageRow {
24
24
  body: string;
25
25
  read: boolean;
26
26
  injected: boolean;
27
+ notified?: boolean;
27
28
  created_at: Date;
28
29
  }
29
30
  export interface CronJobRow {
@@ -19,11 +19,17 @@ export declare class AgentOfficePostgresqlStorage extends AgentOfficeStorageBase
19
19
  getAllConfig(): Promise<ConfigRow[]>;
20
20
  getConfig(key: string): Promise<string | null>;
21
21
  setConfig(key: string, value: string): Promise<void>;
22
- listMessagesForRecipient(name: string, unreadOnly: boolean): Promise<MessageRow[]>;
22
+ listMessagesForRecipient(name: string, filters?: {
23
+ unread?: boolean;
24
+ olderThanHours?: number;
25
+ notified?: boolean;
26
+ }): Promise<MessageRow[]>;
23
27
  listMessagesFromSender(name: string): Promise<MessageRow[]>;
28
+ countUnreadBySender(recipientName: string): Promise<Map<string, number>>;
24
29
  createMessageImpl(from: string, to: string, body: string): Promise<MessageRow>;
25
30
  markMessageAsRead(id: number): Promise<MessageRow | null>;
26
31
  markMessageAsInjected(id: number): Promise<void>;
32
+ markMessagesAsNotified(ids: number[]): Promise<void>;
27
33
  listCronJobs(): Promise<CronJobRow[]>;
28
34
  listCronJobsForSession(sessionName: string): Promise<CronJobRow[]>;
29
35
  getCronJobById(id: number): Promise<CronJobRow | null>;
@@ -107,21 +107,19 @@ export class AgentOfficePostgresqlStorage extends AgentOfficeStorageBase {
107
107
  `;
108
108
  }
109
109
  // Messages
110
- async listMessagesForRecipient(name, unreadOnly) {
111
- if (unreadOnly) {
112
- return this.sql `
113
- SELECT id, from_name, to_name, body, read, injected, created_at
114
- FROM messages
115
- WHERE to_name = ${name} AND read = FALSE
116
- ORDER BY created_at DESC
117
- `;
110
+ async listMessagesForRecipient(name, filters) {
111
+ const whereClauses = ["to_name = $1"];
112
+ const params = [name];
113
+ if (filters?.unread)
114
+ whereClauses.push("read = FALSE");
115
+ if (filters?.notified === false)
116
+ whereClauses.push("notified = FALSE");
117
+ if (filters?.olderThanHours !== undefined) {
118
+ whereClauses.push(`created_at < NOW() - INTERVAL '${filters.olderThanHours} hours'`);
118
119
  }
119
- return this.sql `
120
- SELECT id, from_name, to_name, body, read, injected, created_at
121
- FROM messages
122
- WHERE to_name = ${name}
123
- ORDER BY created_at DESC
124
- `;
120
+ const whereSQL = whereClauses.join(" AND ");
121
+ const rows = await this.sql.unsafe(`SELECT id, from_name, to_name, body, read, injected, created_at, notified FROM messages WHERE ${whereSQL} ORDER BY created_at DESC`, params);
122
+ return rows;
125
123
  }
126
124
  async listMessagesFromSender(name) {
127
125
  return this.sql `
@@ -131,6 +129,19 @@ export class AgentOfficePostgresqlStorage extends AgentOfficeStorageBase {
131
129
  ORDER BY created_at DESC
132
130
  `;
133
131
  }
132
+ async countUnreadBySender(recipientName) {
133
+ const rows = await this.sql `
134
+ SELECT from_name, COUNT(*) as count
135
+ FROM messages
136
+ WHERE to_name = ${recipientName} AND read = FALSE
137
+ GROUP BY from_name
138
+ `;
139
+ const result = new Map();
140
+ for (const row of rows) {
141
+ result.set(row.from_name, Number(row.count));
142
+ }
143
+ return result;
144
+ }
134
145
  async createMessageImpl(from, to, body) {
135
146
  const [row] = await this.sql `
136
147
  INSERT INTO messages (from_name, to_name, body)
@@ -149,6 +160,11 @@ export class AgentOfficePostgresqlStorage extends AgentOfficeStorageBase {
149
160
  async markMessageAsInjected(id) {
150
161
  await this.sql `UPDATE messages SET injected = TRUE WHERE id = ${id}`;
151
162
  }
163
+ async markMessagesAsNotified(ids) {
164
+ if (ids.length === 0)
165
+ return;
166
+ await this.sql `UPDATE messages SET notified = TRUE WHERE id = ANY(${ids})`;
167
+ }
152
168
  // Cron Jobs
153
169
  async listCronJobs() {
154
170
  return this.sql `
@@ -330,6 +346,13 @@ export class AgentOfficePostgresqlStorage extends AgentOfficeStorageBase {
330
346
  name: "rename_mode_to_agent",
331
347
  sql: `
332
348
  ALTER TABLE sessions RENAME COLUMN mode TO agent;
349
+ `,
350
+ },
351
+ {
352
+ version: 9,
353
+ name: "add_notified_to_messages",
354
+ sql: `
355
+ ALTER TABLE messages ADD COLUMN IF NOT EXISTS notified BOOLEAN NOT NULL DEFAULT FALSE;
333
356
  `,
334
357
  },
335
358
  ];
@@ -20,11 +20,17 @@ export declare class AgentOfficeSqliteStorage extends AgentOfficeStorageBase {
20
20
  getAllConfig(): Promise<ConfigRow[]>;
21
21
  getConfig(key: string): Promise<string | null>;
22
22
  setConfig(key: string, value: string): Promise<void>;
23
- listMessagesForRecipient(name: string, unreadOnly: boolean): Promise<MessageRow[]>;
23
+ listMessagesForRecipient(name: string, filters?: {
24
+ unread?: boolean;
25
+ olderThanHours?: number;
26
+ notified?: boolean;
27
+ }): Promise<MessageRow[]>;
24
28
  listMessagesFromSender(name: string): Promise<MessageRow[]>;
29
+ countUnreadBySender(recipientName: string): Promise<Map<string, number>>;
25
30
  createMessageImpl(from: string, to: string, body: string): Promise<MessageRow>;
26
31
  markMessageAsRead(id: number): Promise<MessageRow | null>;
27
32
  markMessageAsInjected(id: number): Promise<void>;
33
+ markMessagesAsNotified(ids: number[]): Promise<void>;
28
34
  listCronJobs(): Promise<CronJobRow[]>;
29
35
  listCronJobsForSession(sessionName: string): Promise<CronJobRow[]>;
30
36
  getCronJobById(id: number): Promise<CronJobRow | null>;
@@ -142,21 +142,32 @@ export class AgentOfficeSqliteStorage extends AgentOfficeStorageBase {
142
142
  stmt.run(key, value);
143
143
  }
144
144
  // Messages
145
- async listMessagesForRecipient(name, unreadOnly) {
146
- const sql = unreadOnly
147
- ? `SELECT id, from_name, to_name, body, read, injected, created_at
148
- FROM messages
149
- WHERE to_name = ? AND read = FALSE
150
- ORDER BY created_at DESC`
151
- : `SELECT id, from_name, to_name, body, read, injected, created_at
152
- FROM messages
153
- WHERE to_name = ?
154
- ORDER BY created_at DESC`;
145
+ async listMessagesForRecipient(name, filters) {
146
+ let whereClauses = [`to_name = ?`];
147
+ let params = [name];
148
+ if (filters?.unread) {
149
+ whereClauses.push(`read = 0`);
150
+ }
151
+ if (filters?.notified === false) {
152
+ whereClauses.push(`notified != 1`);
153
+ }
154
+ if (filters?.olderThanHours !== undefined) {
155
+ const hours = filters.olderThanHours;
156
+ whereClauses.push(`created_at < datetime('now', '-${hours} hours')`);
157
+ }
158
+ const where = whereClauses.join(' AND ');
159
+ const sql = `SELECT id, from_name, to_name, body, read, injected, created_at, notified
160
+ FROM messages
161
+ WHERE ${where}
162
+ ORDER BY created_at DESC`;
155
163
  const stmt = this.db.prepare(sql);
156
- const rows = stmt.all(name);
164
+ const rows = stmt.all(...params);
157
165
  return rows.map(row => ({
158
166
  ...row,
159
- created_at: new Date(row.created_at + 'Z'), // Treat SQLite datetime as UTC
167
+ read: !!row.read,
168
+ injected: !!row.injected,
169
+ notified: !!row.notified,
170
+ created_at: new Date(row.created_at + 'Z'),
160
171
  }));
161
172
  }
162
173
  async listMessagesFromSender(name) {
@@ -172,6 +183,20 @@ export class AgentOfficeSqliteStorage extends AgentOfficeStorageBase {
172
183
  created_at: new Date(row.created_at + 'Z'), // Treat SQLite datetime as UTC
173
184
  }));
174
185
  }
186
+ async countUnreadBySender(recipientName) {
187
+ const stmt = this.db.prepare(`
188
+ SELECT from_name, COUNT(*) as count
189
+ FROM messages
190
+ WHERE to_name = ? AND read = FALSE
191
+ GROUP BY from_name
192
+ `);
193
+ const rows = stmt.all(recipientName);
194
+ const result = new Map();
195
+ for (const row of rows) {
196
+ result.set(row.from_name, row.count);
197
+ }
198
+ return result;
199
+ }
175
200
  async createMessageImpl(from, to, body) {
176
201
  const stmt = this.db.prepare(`
177
202
  INSERT INTO messages (from_name, to_name, body)
@@ -201,6 +226,13 @@ export class AgentOfficeSqliteStorage extends AgentOfficeStorageBase {
201
226
  const stmt = this.db.prepare(`UPDATE messages SET injected = TRUE WHERE id = ?`);
202
227
  stmt.run(id);
203
228
  }
229
+ async markMessagesAsNotified(ids) {
230
+ if (ids.length === 0)
231
+ return;
232
+ const placeholders = ids.map(() => '?').join(',');
233
+ const stmt = this.db.prepare(`UPDATE messages SET notified = 1 WHERE id IN (${placeholders})`);
234
+ stmt.run(...ids);
235
+ }
204
236
  // Cron Jobs
205
237
  async listCronJobs() {
206
238
  const stmt = this.db.prepare(`
@@ -433,6 +465,13 @@ export class AgentOfficeSqliteStorage extends AgentOfficeStorageBase {
433
465
  ALTER TABLE sessions_new RENAME TO sessions;
434
466
  CREATE INDEX IF NOT EXISTS idx_sessions_name ON sessions(name);
435
467
  CREATE UNIQUE INDEX IF NOT EXISTS idx_sessions_agent_code ON sessions(agent_code);
468
+ `,
469
+ },
470
+ {
471
+ version: 9,
472
+ name: "add_notified_to_messages",
473
+ sql: `
474
+ ALTER TABLE messages ADD COLUMN notified INTEGER NOT NULL DEFAULT 0;
436
475
  `,
437
476
  },
438
477
  ];
@@ -21,10 +21,16 @@ export declare abstract class AgentOfficeStorageBase implements AgentOfficeStora
21
21
  abstract getAllConfig(): Promise<ConfigRow[]>;
22
22
  abstract getConfig(key: string): Promise<string | null>;
23
23
  abstract setConfig(key: string, value: string): Promise<void>;
24
- abstract listMessagesForRecipient(name: string, unreadOnly: boolean): Promise<MessageRow[]>;
24
+ abstract listMessagesForRecipient(name: string, filters?: {
25
+ unread?: boolean;
26
+ olderThanHours?: number;
27
+ notified?: boolean;
28
+ }): Promise<MessageRow[]>;
25
29
  abstract listMessagesFromSender(name: string): Promise<MessageRow[]>;
30
+ abstract countUnreadBySender(recipientName: string): Promise<Map<string, number>>;
26
31
  abstract markMessageAsRead(id: number): Promise<MessageRow | null>;
27
32
  abstract markMessageAsInjected(id: number): Promise<void>;
33
+ abstract markMessagesAsNotified(ids: number[]): Promise<void>;
28
34
  abstract listCronJobs(): Promise<CronJobRow[]>;
29
35
  abstract listCronJobsForSession(sessionName: string): Promise<CronJobRow[]>;
30
36
  abstract getCronJobById(id: number): Promise<CronJobRow | null>;
@@ -18,7 +18,7 @@ export class AgentOfficeStorageBase {
18
18
  }
19
19
  // For each session, find all messages sent to them and track the latest per sender
20
20
  for (const session of sessions) {
21
- const messages = await this.listMessagesForRecipient(session.name, false);
21
+ const messages = await this.listMessagesForRecipient(session.name);
22
22
  const senderMap = this.coworkerMailState.get(session.name);
23
23
  for (const message of messages) {
24
24
  const existingDate = senderMap.get(message.from_name);
@@ -30,11 +30,17 @@ export interface AgentOfficeStorage {
30
30
  getAllConfig(): Promise<ConfigRow[]>;
31
31
  getConfig(key: string): Promise<string | null>;
32
32
  setConfig(key: string, value: string): Promise<void>;
33
- listMessagesForRecipient(name: string, unreadOnly: boolean): Promise<MessageRow[]>;
33
+ listMessagesForRecipient(name: string, filters?: {
34
+ unread?: boolean;
35
+ olderThanHours?: number;
36
+ notified?: boolean;
37
+ }): Promise<MessageRow[]>;
34
38
  listMessagesFromSender(name: string): Promise<MessageRow[]>;
39
+ countUnreadBySender(recipientName: string): Promise<Map<string, number>>;
35
40
  createMessage(from: string, to: string, body: string): Promise<MessageRow>;
36
41
  markMessageAsRead(id: number): Promise<MessageRow | null>;
37
42
  markMessageAsInjected(id: number): Promise<void>;
43
+ markMessagesAsNotified(ids: number[]): Promise<void>;
38
44
  listCronJobs(): Promise<CronJobRow[]>;
39
45
  listCronJobsForSession(sessionName: string): Promise<CronJobRow[]>;
40
46
  getCronJobById(id: number): Promise<CronJobRow | null>;
@@ -0,0 +1,18 @@
1
+ export interface NotifyOptions {
2
+ from: string;
3
+ to: string;
4
+ subject: string;
5
+ text: string;
6
+ }
7
+ /**
8
+ * Abstract notification interface — swap out the implementation
9
+ * (email, SMS, webhook, etc.) without changing the daemon logic.
10
+ */
11
+ export interface AgentOfficeNotifier {
12
+ send(opts: NotifyOptions): Promise<void>;
13
+ }
14
+ export declare class ResendNotifier implements AgentOfficeNotifier {
15
+ private resend;
16
+ constructor(apiKey: string);
17
+ send(opts: NotifyOptions): Promise<void>;
18
+ }
@@ -0,0 +1,15 @@
1
+ import { Resend } from "resend";
2
+ export class ResendNotifier {
3
+ resend;
4
+ constructor(apiKey) {
5
+ this.resend = new Resend(apiKey);
6
+ }
7
+ async send(opts) {
8
+ await this.resend.emails.send({
9
+ from: opts.from,
10
+ to: [opts.to],
11
+ subject: opts.subject,
12
+ text: opts.text,
13
+ });
14
+ }
15
+ }