agent-office 0.2.3 → 0.3.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.
@@ -1,10 +1,11 @@
1
1
  interface ServeOptions {
2
2
  databaseUrl?: string;
3
- opencodeUrl: string;
3
+ sqlite?: string;
4
4
  host: string;
5
5
  port: string;
6
6
  memoryPath: string;
7
7
  password?: string;
8
+ opencodeUrl: string;
8
9
  }
9
10
  export declare function serve(options: ServeOptions): Promise<void>;
10
11
  export {};
@@ -1,4 +1,4 @@
1
- import { createDb } from "../db/index.js";
1
+ import { createPostgresqlStorage, createSqliteStorage } from "../db/index.js";
2
2
  import { runMigrations } from "../db/migrate.js";
3
3
  import { OpenCodeCodingServer } from "../lib/opencode-coding-server.js";
4
4
  import { createApp } from "../server/index.js";
@@ -10,32 +10,39 @@ export async function serve(options) {
10
10
  console.error("Error: --password is required (or set AGENT_OFFICE_PASSWORD)");
11
11
  process.exit(1);
12
12
  }
13
- const databaseUrl = options.databaseUrl;
14
- if (!databaseUrl) {
15
- console.error("Error: --database-url is required (or set DATABASE_URL)");
16
- process.exit(1);
17
- }
18
13
  const port = parseInt(options.port, 10);
19
14
  if (isNaN(port) || port < 1 || port > 65535) {
20
15
  console.error(`Error: invalid port "${options.port}"`);
21
16
  process.exit(1);
22
17
  }
23
- // Connect to DB
24
- console.log("Connecting to database...");
25
- const sql = createDb(databaseUrl);
18
+ // Determine storage backend
19
+ let storage;
20
+ if (options.sqlite) {
21
+ console.log(`Connecting to SQLite database at ${options.sqlite}...`);
22
+ storage = createSqliteStorage(options.sqlite);
23
+ }
24
+ else if (options.databaseUrl) {
25
+ console.log("Connecting to PostgreSQL database...");
26
+ storage = createPostgresqlStorage(options.databaseUrl);
27
+ }
28
+ else {
29
+ console.error("Error: either --database-url (or DATABASE_URL) or --sqlite (or AGENT_OFFICE_SQLITE) is required");
30
+ process.exit(1);
31
+ }
26
32
  // Run migrations
27
33
  console.log("Running migrations...");
28
34
  try {
29
- await runMigrations(sql);
35
+ await runMigrations(storage);
30
36
  console.log("Migrations complete.");
31
37
  }
32
38
  catch (err) {
33
39
  console.error("Migration failed:", err);
34
- await sql.end();
40
+ await storage.close();
35
41
  process.exit(1);
36
42
  }
37
43
  // Init agentic coding server (OpenCode implementation)
38
44
  const agenticCodingServer = new OpenCodeCodingServer(options.opencodeUrl);
45
+ console.log(`Connecting to OpenCode server at ${options.opencodeUrl}...`);
39
46
  const serverUrl = `http://${options.host}:${port}`;
40
47
  // Create memory manager and verify embedding model
41
48
  const memoryManager = new MemoryManager(options.memoryPath);
@@ -49,15 +56,15 @@ export async function serve(options) {
49
56
  console.error("Try deleting the model cache and restarting:");
50
57
  console.error(` rm -rf ${options.memoryPath}/.model-cache`);
51
58
  memoryManager.closeAll();
52
- await sql.end();
59
+ await storage.close();
53
60
  process.exit(1);
54
61
  }
55
62
  // Create cron scheduler
56
63
  const cronScheduler = new CronScheduler();
57
64
  // Create Express app
58
- const app = createApp(sql, agenticCodingServer, password, serverUrl, cronScheduler, memoryManager);
65
+ const app = createApp(storage, agenticCodingServer, password, serverUrl, cronScheduler, memoryManager);
59
66
  // Start cron scheduler
60
- await cronScheduler.start(sql, agenticCodingServer, serverUrl);
67
+ await cronScheduler.start(storage, agenticCodingServer, serverUrl);
61
68
  // Start server
62
69
  const server = app.listen(port, options.host, () => {
63
70
  console.log(`agent-office server listening on ${serverUrl}`);
@@ -68,7 +75,7 @@ export async function serve(options) {
68
75
  server.close(async () => {
69
76
  cronScheduler.stop();
70
77
  memoryManager.closeAll();
71
- await sql.end();
78
+ await storage.close();
72
79
  console.log("Goodbye.");
73
80
  process.exit(0);
74
81
  });
@@ -1,6 +1,9 @@
1
1
  import postgres from "postgres";
2
2
  export type Sql = ReturnType<typeof postgres>;
3
3
  export declare function createDb(databaseUrl: string): Sql;
4
+ export type { AgentOfficeStorage, WatchListener, WatchState, SenderInfo } from "./storage.js";
5
+ export { AgentOfficePostgresqlStorage, createPostgresqlStorage } from "./postgresql-storage.js";
6
+ export { AgentOfficeSqliteStorage, createSqliteStorage } from "./sqlite-storage.js";
4
7
  export interface SessionRow {
5
8
  id: number;
6
9
  name: string;
package/dist/db/index.js CHANGED
@@ -7,3 +7,5 @@ export function createDb(databaseUrl) {
7
7
  onnotice: () => { },
8
8
  });
9
9
  }
10
+ export { AgentOfficePostgresqlStorage, createPostgresqlStorage } from "./postgresql-storage.js";
11
+ export { AgentOfficeSqliteStorage, createSqliteStorage } from "./sqlite-storage.js";
@@ -1,2 +1,2 @@
1
- import type { Sql } from "./index.js";
2
- export declare function runMigrations(sql: Sql): Promise<void>;
1
+ import type { AgentOfficeStorage } from "./storage.js";
2
+ export declare function runMigrations(storage: AgentOfficeStorage): Promise<void>;
@@ -1,125 +1,3 @@
1
- const MIGRATIONS = [
2
- {
3
- version: 1,
4
- name: "create_sessions",
5
- sql: `
6
- CREATE TABLE IF NOT EXISTS sessions (
7
- id SERIAL PRIMARY KEY,
8
- name VARCHAR(255) UNIQUE NOT NULL,
9
- session_id VARCHAR(255) UNIQUE NOT NULL,
10
- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
11
- );
12
- CREATE INDEX IF NOT EXISTS idx_sessions_name ON sessions(name);
13
- `,
14
- },
15
- {
16
- version: 2,
17
- name: "add_agent_code",
18
- sql: `
19
- ALTER TABLE sessions ADD COLUMN IF NOT EXISTS agent_code UUID NOT NULL DEFAULT gen_random_uuid();
20
- CREATE UNIQUE INDEX IF NOT EXISTS idx_sessions_agent_code ON sessions(agent_code);
21
- `,
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
- },
89
- {
90
- version: 7,
91
- name: "add_status_to_sessions",
92
- sql: `
93
- ALTER TABLE sessions ADD COLUMN IF NOT EXISTS status TEXT NULL;
94
- `,
95
- },
96
- {
97
- version: 8,
98
- name: "rename_mode_to_agent",
99
- sql: `
100
- ALTER TABLE sessions RENAME COLUMN mode TO agent;
101
- `,
102
- },
103
- ];
104
- export async function runMigrations(sql) {
105
- await sql `
106
- CREATE TABLE IF NOT EXISTS _migrations (
107
- version INTEGER PRIMARY KEY,
108
- name VARCHAR(255) NOT NULL,
109
- applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
110
- )
111
- `;
112
- const applied = await sql `
113
- SELECT version FROM _migrations ORDER BY version
114
- `;
115
- const appliedVersions = new Set(applied.map((r) => r.version));
116
- for (const migration of MIGRATIONS) {
117
- if (appliedVersions.has(migration.version))
118
- continue;
119
- console.log(` Applying migration ${migration.version}: ${migration.name}`);
120
- await sql.begin(async (tx) => {
121
- await tx.unsafe(migration.sql);
122
- await tx.unsafe(`INSERT INTO _migrations (version, name) VALUES ($1, $2)`, [migration.version, migration.name]);
123
- });
124
- }
1
+ export async function runMigrations(storage) {
2
+ await storage.runMigrations();
125
3
  }
@@ -0,0 +1,41 @@
1
+ import { AgentOfficeStorageBase } from "./storage-base.js";
2
+ import type { SessionRow, ConfigRow, MessageRow, CronJobRow, CronHistoryRow, Sql } from "./index.js";
3
+ export declare class AgentOfficePostgresqlStorage extends AgentOfficeStorageBase {
4
+ private sql;
5
+ constructor(sql: Sql);
6
+ close(): Promise<void>;
7
+ begin<T>(callback: (tx: import("./storage.js").AgentOfficeStorage) => Promise<T>): Promise<T>;
8
+ listSessions(): Promise<SessionRow[]>;
9
+ getSessionByName(name: string): Promise<SessionRow | null>;
10
+ getSessionByAgentCode(agentCode: string): Promise<SessionRow | null>;
11
+ getSessionIdByName(name: string): Promise<number | null>;
12
+ createSession(name: string, sessionId: string, agent: string): Promise<SessionRow>;
13
+ deleteSession(id: number): Promise<void>;
14
+ regenerateAgentCode(name: string): Promise<SessionRow>;
15
+ updateSessionAgent(name: string, agent: string): Promise<SessionRow>;
16
+ updateSessionStatus(agentCode: string, status: string | null): Promise<void>;
17
+ updateSessionId(name: string, sessionId: string): Promise<void>;
18
+ sessionExists(name: string): Promise<boolean>;
19
+ getAllConfig(): Promise<ConfigRow[]>;
20
+ getConfig(key: string): Promise<string | null>;
21
+ setConfig(key: string, value: string): Promise<void>;
22
+ listMessagesForRecipient(name: string, unreadOnly: boolean): Promise<MessageRow[]>;
23
+ listMessagesFromSender(name: string): Promise<MessageRow[]>;
24
+ createMessageImpl(from: string, to: string, body: string): Promise<MessageRow>;
25
+ markMessageAsRead(id: number): Promise<MessageRow | null>;
26
+ markMessageAsInjected(id: number): Promise<void>;
27
+ listCronJobs(): Promise<CronJobRow[]>;
28
+ listCronJobsForSession(sessionName: string): Promise<CronJobRow[]>;
29
+ getCronJobById(id: number): Promise<CronJobRow | null>;
30
+ getCronJobByNameAndSession(name: string, sessionName: string): Promise<CronJobRow | null>;
31
+ createCronJob(name: string, sessionName: string, schedule: string, timezone: string | null, message: string): Promise<CronJobRow>;
32
+ deleteCronJob(id: number): Promise<void>;
33
+ enableCronJob(id: number): Promise<void>;
34
+ disableCronJob(id: number): Promise<void>;
35
+ updateCronJobLastRun(id: number, lastRun: Date): Promise<void>;
36
+ cronJobExistsForSession(name: string, sessionName: string): Promise<boolean>;
37
+ listCronHistory(cronJobId: number, limit: number): Promise<CronHistoryRow[]>;
38
+ createCronHistory(cronJobId: number, executedAt: Date, success: boolean, errorMessage?: string): Promise<void>;
39
+ runMigrations(): Promise<void>;
40
+ }
41
+ export declare function createPostgresqlStorage(databaseUrl: string): AgentOfficePostgresqlStorage;
@@ -0,0 +1,366 @@
1
+ import postgres from "postgres";
2
+ import { AgentOfficeStorageBase } from "./storage-base.js";
3
+ export class AgentOfficePostgresqlStorage extends AgentOfficeStorageBase {
4
+ sql;
5
+ constructor(sql) {
6
+ super();
7
+ this.sql = sql;
8
+ }
9
+ async close() {
10
+ await this.sql.end();
11
+ }
12
+ async begin(callback) {
13
+ return this.sql.begin(async (tx) => {
14
+ const txStorage = new AgentOfficePostgresqlStorage(tx);
15
+ return callback(txStorage);
16
+ });
17
+ }
18
+ // Sessions
19
+ async listSessions() {
20
+ return this.sql `
21
+ SELECT id, name, session_id, agent_code, agent, status, created_at
22
+ FROM sessions
23
+ ORDER BY created_at DESC
24
+ `;
25
+ }
26
+ async getSessionByName(name) {
27
+ const [row] = await this.sql `
28
+ SELECT id, name, session_id, agent_code, agent, status, created_at
29
+ FROM sessions WHERE name = ${name}
30
+ `;
31
+ return row ?? null;
32
+ }
33
+ async getSessionByAgentCode(agentCode) {
34
+ const [row] = await this.sql `
35
+ SELECT id, name, session_id, agent_code, agent, status, created_at
36
+ FROM sessions WHERE agent_code = ${agentCode}
37
+ `;
38
+ return row ?? null;
39
+ }
40
+ async getSessionIdByName(name) {
41
+ const [row] = await this.sql `
42
+ SELECT id FROM sessions WHERE name = ${name}
43
+ `;
44
+ return row?.id ?? null;
45
+ }
46
+ async createSession(name, sessionId, agent) {
47
+ const [row] = await this.sql `
48
+ INSERT INTO sessions (name, session_id, agent)
49
+ VALUES (${name}, ${sessionId}, ${agent})
50
+ RETURNING id, name, session_id, agent_code, agent, status, created_at
51
+ `;
52
+ return row;
53
+ }
54
+ async deleteSession(id) {
55
+ await this.sql `DELETE FROM sessions WHERE id = ${id}`;
56
+ }
57
+ async regenerateAgentCode(name) {
58
+ const [row] = await this.sql `
59
+ UPDATE sessions
60
+ SET agent_code = gen_random_uuid()
61
+ WHERE name = ${name}
62
+ RETURNING id, name, session_id, agent_code, agent, status, created_at
63
+ `;
64
+ return row;
65
+ }
66
+ async updateSessionAgent(name, agent) {
67
+ const [row] = await this.sql `
68
+ UPDATE sessions
69
+ SET agent = ${agent}
70
+ WHERE name = ${name}
71
+ RETURNING id, name, session_id, agent_code, agent, status, created_at
72
+ `;
73
+ return row;
74
+ }
75
+ async updateSessionStatus(agentCode, status) {
76
+ await this.sql `
77
+ UPDATE sessions
78
+ SET status = ${status}
79
+ WHERE agent_code = ${agentCode}
80
+ `;
81
+ }
82
+ async updateSessionId(name, sessionId) {
83
+ await this.sql `
84
+ UPDATE sessions SET session_id = ${sessionId} WHERE name = ${name}
85
+ `;
86
+ }
87
+ async sessionExists(name) {
88
+ const [row] = await this.sql `
89
+ SELECT id FROM sessions WHERE name = ${name}
90
+ `;
91
+ return !!row;
92
+ }
93
+ // Config
94
+ async getAllConfig() {
95
+ return this.sql `SELECT key, value FROM config`;
96
+ }
97
+ async getConfig(key) {
98
+ const [row] = await this.sql `
99
+ SELECT value FROM config WHERE key = ${key}
100
+ `;
101
+ return row?.value ?? null;
102
+ }
103
+ async setConfig(key, value) {
104
+ await this.sql `
105
+ INSERT INTO config (key, value) VALUES (${key}, ${value})
106
+ ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value
107
+ `;
108
+ }
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
+ `;
118
+ }
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
+ `;
125
+ }
126
+ async listMessagesFromSender(name) {
127
+ return this.sql `
128
+ SELECT id, from_name, to_name, body, read, injected, created_at
129
+ FROM messages
130
+ WHERE from_name = ${name}
131
+ ORDER BY created_at DESC
132
+ `;
133
+ }
134
+ async createMessageImpl(from, to, body) {
135
+ const [row] = await this.sql `
136
+ INSERT INTO messages (from_name, to_name, body)
137
+ VALUES (${from}, ${to}, ${body})
138
+ RETURNING id, from_name, to_name, body, read, injected, created_at
139
+ `;
140
+ return row;
141
+ }
142
+ async markMessageAsRead(id) {
143
+ const [row] = await this.sql `
144
+ UPDATE messages SET read = TRUE WHERE id = ${id}
145
+ RETURNING id, from_name, to_name, body, read, injected, created_at
146
+ `;
147
+ return row ?? null;
148
+ }
149
+ async markMessageAsInjected(id) {
150
+ await this.sql `UPDATE messages SET injected = TRUE WHERE id = ${id}`;
151
+ }
152
+ // Cron Jobs
153
+ async listCronJobs() {
154
+ return this.sql `
155
+ SELECT id, name, session_name, schedule, timezone, message, enabled, created_at, last_run
156
+ FROM cron_jobs
157
+ ORDER BY name
158
+ `;
159
+ }
160
+ async listCronJobsForSession(sessionName) {
161
+ return this.sql `
162
+ SELECT id, name, session_name, schedule, timezone, message, enabled, created_at, last_run
163
+ FROM cron_jobs
164
+ WHERE session_name = ${sessionName}
165
+ ORDER BY name
166
+ `;
167
+ }
168
+ async getCronJobById(id) {
169
+ const [row] = await this.sql `
170
+ SELECT id, name, session_name, schedule, timezone, message, enabled, created_at, last_run
171
+ FROM cron_jobs WHERE id = ${id}
172
+ `;
173
+ return row ?? null;
174
+ }
175
+ async getCronJobByNameAndSession(name, sessionName) {
176
+ const [row] = await this.sql `
177
+ SELECT id, name, session_name, schedule, timezone, message, enabled, created_at, last_run
178
+ FROM cron_jobs WHERE name = ${name} AND session_name = ${sessionName}
179
+ `;
180
+ return row ?? null;
181
+ }
182
+ async createCronJob(name, sessionName, schedule, timezone, message) {
183
+ const [row] = await this.sql `
184
+ INSERT INTO cron_jobs (name, session_name, schedule, timezone, message)
185
+ VALUES (${name}, ${sessionName}, ${schedule}, ${timezone}, ${message})
186
+ RETURNING id, name, session_name, schedule, timezone, message, enabled, created_at, last_run
187
+ `;
188
+ return row;
189
+ }
190
+ async deleteCronJob(id) {
191
+ await this.sql `DELETE FROM cron_jobs WHERE id = ${id}`;
192
+ }
193
+ async enableCronJob(id) {
194
+ await this.sql `UPDATE cron_jobs SET enabled = TRUE WHERE id = ${id}`;
195
+ }
196
+ async disableCronJob(id) {
197
+ await this.sql `UPDATE cron_jobs SET enabled = FALSE WHERE id = ${id}`;
198
+ }
199
+ async updateCronJobLastRun(id, lastRun) {
200
+ await this.sql `UPDATE cron_jobs SET last_run = ${lastRun} WHERE id = ${id}`;
201
+ }
202
+ async cronJobExistsForSession(name, sessionName) {
203
+ const [row] = await this.sql `
204
+ SELECT id FROM cron_jobs WHERE name = ${name} AND session_name = ${sessionName}
205
+ `;
206
+ return !!row;
207
+ }
208
+ // Cron History
209
+ async listCronHistory(cronJobId, limit) {
210
+ return this.sql `
211
+ SELECT id, cron_job_id, executed_at, success, error_message
212
+ FROM cron_history
213
+ WHERE cron_job_id = ${cronJobId}
214
+ ORDER BY executed_at DESC
215
+ LIMIT ${limit}
216
+ `;
217
+ }
218
+ async createCronHistory(cronJobId, executedAt, success, errorMessage) {
219
+ if (success) {
220
+ await this.sql `
221
+ INSERT INTO cron_history (cron_job_id, executed_at, success)
222
+ VALUES (${cronJobId}, ${executedAt}, TRUE)
223
+ `;
224
+ }
225
+ else {
226
+ await this.sql `
227
+ INSERT INTO cron_history (cron_job_id, executed_at, success, error_message)
228
+ VALUES (${cronJobId}, ${executedAt}, FALSE, ${errorMessage ?? null})
229
+ `;
230
+ }
231
+ }
232
+ async runMigrations() {
233
+ const MIGRATIONS = [
234
+ {
235
+ version: 1,
236
+ name: "create_sessions",
237
+ sql: `
238
+ CREATE TABLE IF NOT EXISTS sessions (
239
+ id SERIAL PRIMARY KEY,
240
+ name VARCHAR(255) UNIQUE NOT NULL,
241
+ session_id VARCHAR(255) UNIQUE NOT NULL,
242
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
243
+ );
244
+ CREATE INDEX IF NOT EXISTS idx_sessions_name ON sessions(name);
245
+ `,
246
+ },
247
+ {
248
+ version: 2,
249
+ name: "add_agent_code",
250
+ sql: `
251
+ ALTER TABLE sessions ADD COLUMN IF NOT EXISTS agent_code UUID NOT NULL DEFAULT gen_random_uuid();
252
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_sessions_agent_code ON sessions(agent_code);
253
+ `,
254
+ },
255
+ {
256
+ version: 3,
257
+ name: "create_config_table",
258
+ sql: `
259
+ CREATE TABLE IF NOT EXISTS config (
260
+ key VARCHAR(255) PRIMARY KEY,
261
+ value TEXT NOT NULL
262
+ );
263
+ INSERT INTO config (key, value) VALUES ('human_name', 'Human') ON CONFLICT DO NOTHING;
264
+ INSERT INTO config (key, value) VALUES ('human_description', '') ON CONFLICT DO NOTHING;
265
+ `,
266
+ },
267
+ {
268
+ version: 5,
269
+ name: "add_mode_to_sessions",
270
+ sql: `
271
+ ALTER TABLE sessions ADD COLUMN IF NOT EXISTS mode VARCHAR(255) NULL;
272
+ `,
273
+ },
274
+ {
275
+ version: 4,
276
+ name: "create_messages_table",
277
+ sql: `
278
+ CREATE TABLE IF NOT EXISTS messages (
279
+ id SERIAL PRIMARY KEY,
280
+ from_name VARCHAR(255) NOT NULL,
281
+ to_name VARCHAR(255) NOT NULL,
282
+ body TEXT NOT NULL,
283
+ read BOOLEAN NOT NULL DEFAULT FALSE,
284
+ injected BOOLEAN NOT NULL DEFAULT FALSE,
285
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
286
+ );
287
+ CREATE INDEX IF NOT EXISTS idx_messages_to_name ON messages(to_name);
288
+ CREATE INDEX IF NOT EXISTS idx_messages_from_name ON messages(from_name);
289
+ CREATE INDEX IF NOT EXISTS idx_messages_read ON messages(read);
290
+ `,
291
+ },
292
+ {
293
+ version: 6,
294
+ name: "create_cron_tables",
295
+ sql: `
296
+ CREATE TABLE IF NOT EXISTS cron_jobs (
297
+ id SERIAL PRIMARY KEY,
298
+ name VARCHAR(255) NOT NULL,
299
+ session_name VARCHAR(255) NOT NULL REFERENCES sessions(name) ON DELETE CASCADE,
300
+ schedule TEXT NOT NULL,
301
+ timezone VARCHAR(100),
302
+ message TEXT NOT NULL,
303
+ enabled BOOLEAN NOT NULL DEFAULT TRUE,
304
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
305
+ last_run TIMESTAMPTZ
306
+ );
307
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_cron_jobs_name_session ON cron_jobs(name, session_name);
308
+ CREATE INDEX IF NOT EXISTS idx_cron_jobs_session_name ON cron_jobs(session_name);
309
+ CREATE INDEX IF NOT EXISTS idx_cron_jobs_enabled ON cron_jobs(enabled);
310
+
311
+ CREATE TABLE IF NOT EXISTS cron_history (
312
+ id SERIAL PRIMARY KEY,
313
+ cron_job_id INTEGER NOT NULL REFERENCES cron_jobs(id) ON DELETE CASCADE,
314
+ executed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
315
+ success BOOLEAN NOT NULL DEFAULT TRUE,
316
+ error_message TEXT
317
+ );
318
+ CREATE INDEX IF NOT EXISTS idx_cron_history_job_id ON cron_history(cron_job_id);
319
+ `,
320
+ },
321
+ {
322
+ version: 7,
323
+ name: "add_status_to_sessions",
324
+ sql: `
325
+ ALTER TABLE sessions ADD COLUMN IF NOT EXISTS status TEXT NULL;
326
+ `,
327
+ },
328
+ {
329
+ version: 8,
330
+ name: "rename_mode_to_agent",
331
+ sql: `
332
+ ALTER TABLE sessions RENAME COLUMN mode TO agent;
333
+ `,
334
+ },
335
+ ];
336
+ await this.sql `
337
+ CREATE TABLE IF NOT EXISTS _migrations (
338
+ version INTEGER PRIMARY KEY,
339
+ name VARCHAR(255) NOT NULL,
340
+ applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
341
+ )
342
+ `;
343
+ const applied = await this.sql `
344
+ SELECT version FROM _migrations ORDER BY version
345
+ `;
346
+ const appliedVersions = new Set(applied.map((r) => r.version));
347
+ for (const migration of MIGRATIONS) {
348
+ if (appliedVersions.has(migration.version))
349
+ continue;
350
+ console.log(` Applying migration ${migration.version}: ${migration.name}`);
351
+ await this.sql.begin(async (tx) => {
352
+ await tx.unsafe(migration.sql);
353
+ await tx.unsafe(`INSERT INTO _migrations (version, name) VALUES ($1, $2)`, [migration.version, migration.name]);
354
+ });
355
+ }
356
+ }
357
+ }
358
+ export function createPostgresqlStorage(databaseUrl) {
359
+ const sql = postgres(databaseUrl, {
360
+ max: 10,
361
+ idle_timeout: 30,
362
+ connect_timeout: 10,
363
+ onnotice: () => { },
364
+ });
365
+ return new AgentOfficePostgresqlStorage(sql);
366
+ }