agent-office 0.0.12 → 0.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,14 +1,44 @@
1
1
  # agent-office
2
2
 
3
- Manage [OpenCode](https://opencode.ai) sessions with named aliases, agent codes, and a full-screen terminal UI.
3
+ An office for your AI agents. Manage multiple [OpenCode](https://opencode.ai) coding sessions as named coworkers with inter-agent messaging, scheduled tasks, persistent memory, and a terminal UI for human oversight.
4
4
 
5
5
  ## Overview
6
6
 
7
- `agent-office` is a CLI tool with two main roles:
7
+ `agent-office` sits between a human operator and one or more AI coding agents running on an OpenCode server. It wraps raw OpenCode sessions with named identities, a messaging system, cron-based scheduling, and per-agent memory -- creating an "office" where AI agents are coworkers that communicate, take direction, and operate autonomously.
8
8
 
9
- - **`serve`** runs an HTTP server that sits in front of an OpenCode server and a PostgreSQL database. It manages named sessions, agent codes, and exposes a REST API.
10
- - **`manage`** — a full-screen React Ink TUI that connects to a running `serve` instance and lets you create, delete, inspect, and interact with sessions.
11
- - **`worker clock-in`** lets an agent identify itself using its unique agent code and receive a welcome message with its session details.
9
+ **For humans**, there are three interfaces:
10
+
11
+ - **`serve`** -- an HTTP server that manages sessions, messages, cron jobs, and memory, backed by PostgreSQL and a running OpenCode server
12
+ - **`manage`** -- a full-screen terminal UI (React Ink) for creating coworkers, sending messages, browsing mail, managing cron jobs, and observing agent activity
13
+ - **`communicator web`** -- a browser-based chat interface for conversing with a specific agent in real time
14
+
15
+ **For AI agents**, there is a CLI:
16
+
17
+ - **`worker`** -- subcommands that agents invoke from within their OpenCode sessions to clock in, message coworkers, set status, manage cron jobs, and store memories
18
+
19
+ ```
20
+ +---------------------+
21
+ | OpenCode Server |
22
+ | :4096 |
23
+ +---------+-----------+
24
+ |
25
+ +--------------+ +---------+-----------+ +--------------+
26
+ | TUI (manage) |<-->| agent-office serve |<-->| PostgreSQL |
27
+ | Ink/React | | :7654 | | |
28
+ +--------------+ | | +--------------+
29
+ | CronScheduler |
30
+ +--------------+ | MemoryManager | +--------------+
31
+ | Communicator |<-->| |<-->| .memory/ |
32
+ | Web (HTMX) | | | | SQLite DBs |
33
+ | :7655 | +---------+-----------+ +--------------+
34
+ +--------------+ |
35
+ | /worker/* endpoints
36
+ +---------+-----------+
37
+ | AI Agent Workers |
38
+ | (running inside |
39
+ | OpenCode sessions) |
40
+ +-----------------------+
41
+ ```
12
42
 
13
43
  ## Installation
14
44
 
@@ -19,7 +49,7 @@ npm install -g agent-office
19
49
  Or run without installing:
20
50
 
21
51
  ```bash
22
- npx agent-office serve --help
52
+ npx agent-office --help
23
53
  ```
24
54
 
25
55
  ## Requirements
@@ -36,31 +66,60 @@ npx agent-office serve --help
36
66
  agent-office serve \
37
67
  --database-url "postgresql://user:pass@localhost:5432/mydb" \
38
68
  --opencode-url "http://localhost:4096" \
39
- --host 127.0.0.1 \
40
- --port 7654 \
41
69
  --password mysecret
42
70
  ```
43
71
 
44
- Or use environment variables:
45
-
46
- ```bash
47
- export DATABASE_URL="postgresql://user:pass@localhost:5432/mydb"
48
- export AGENT_OFFICE_PASSWORD="mysecret"
49
- agent-office serve
50
- ```
72
+ The server runs migrations automatically on startup, initializes the cron scheduler, and warms up the embedding model for agent memory.
51
73
 
52
74
  ### 2. Open the manager
53
75
 
76
+ In another terminal:
77
+
54
78
  ```bash
55
- agent-office manage http://localhost:7654 --password mysecret
79
+ agent-office manage --password mysecret
56
80
  ```
57
81
 
58
- ### 3. Clock in as a worker
82
+ From the TUI you can create coworkers, send them messages, browse mail, manage cron jobs, and observe their sessions.
83
+
84
+ ### 3. Chat with a coworker in the browser
59
85
 
60
86
  ```bash
61
- agent-office worker clock-in <agent_code>@http://localhost:7654
87
+ agent-office communicator web "Alice" --secret mysecret
62
88
  ```
63
89
 
90
+ Opens a web-based chat interface at `http://127.0.0.1:7655` for real-time conversation with the named coworker.
91
+
92
+ ## How It Works
93
+
94
+ ### Session Lifecycle
95
+
96
+ 1. **Create** -- A human creates a named coworker via the TUI or API. This creates an OpenCode session, stores the mapping in PostgreSQL, and sends an enrollment message containing a clock-in command.
97
+
98
+ 2. **Clock In** -- The AI agent sees the enrollment message in its session, runs `agent-office worker clock-in <token>`, and receives a welcome briefing with its name, the human manager's identity, all available CLI commands, and a privacy notice.
99
+
100
+ 3. **Work** -- The agent operates in its OpenCode session. It communicates with the human and other agents by running `agent-office worker send-message`. It can set its status, create cron jobs, and store persistent memories.
101
+
102
+ 4. **Message Delivery** -- Messages are stored in PostgreSQL and simultaneously injected as prompts into the recipient's OpenCode session. This means agents see messages immediately in their active session context.
103
+
104
+ 5. **Reset** -- A session can be reverted to its initial state (clearing all conversation history) and re-enrolled. Memories persist across resets since they are stored separately.
105
+
106
+ 6. **Delete** -- Deleting a coworker removes both the OpenCode session and the database record.
107
+
108
+ ### Privacy Model
109
+
110
+ Agent sessions are private by design. The welcome message tells each agent:
111
+
112
+ > Nobody -- not your human manager, not your coworkers -- can see anything you think, reason, or write inside this session. Your work is completely private until you explicitly send a message.
113
+
114
+ This means agents must actively communicate to report progress, ask questions, or share results.
115
+
116
+ ### Authentication
117
+
118
+ Two authentication schemes run in parallel:
119
+
120
+ - **Bearer token** -- Human operators authenticate with `Authorization: Bearer <password>` for all management endpoints.
121
+ - **Agent code** -- AI agents authenticate with a UUID passed as `?code=<uuid>` for all `/worker/*` endpoints. Each agent gets a unique code generated at session creation time.
122
+
64
123
  ## Environment Variables
65
124
 
66
125
  Copy `.env.example` to `.env` for local development:
@@ -69,81 +128,191 @@ Copy `.env.example` to `.env` for local development:
69
128
  cp .env.example .env
70
129
  ```
71
130
 
72
- | Variable | Description |
73
- |---|---|
74
- | `DATABASE_URL` | PostgreSQL connection string |
75
- | `AGENT_OFFICE_PASSWORD` | Password for the server API (required) |
76
- | `OPENCODE_URL` | OpenCode server URL (default: `http://localhost:4096`) |
131
+ | Variable | Description | Default |
132
+ |---|---|---|
133
+ | `DATABASE_URL` | PostgreSQL connection string | (required) |
134
+ | `AGENT_OFFICE_PASSWORD` | API password for human access | (required) |
135
+ | `OPENCODE_URL` | OpenCode server URL | `http://localhost:4096` |
136
+ | `AGENT_OFFICE_URL` | Server URL for manage/communicator clients | `http://127.0.0.1:7654` |
77
137
 
78
138
  ## Commands
79
139
 
80
140
  ### `agent-office serve`
81
141
 
82
- Starts the HTTP server. Runs database migrations automatically on startup.
142
+ Starts the HTTP server. Connects to PostgreSQL, runs migrations, initializes the cron scheduler, and warms up the embedding model.
83
143
 
84
144
  ```
85
145
  Options:
86
- --database-url <url> PostgreSQL connection string (env: DATABASE_URL)
87
- --opencode-url <url> OpenCode server URL (env: OPENCODE_URL)
88
- --host <host> Host to bind to (default: 127.0.0.1)
89
- --port <port> Port to serve on (default: 7654)
90
- --password <password> REQUIRED. API password (env: AGENT_OFFICE_PASSWORD)
146
+ --database-url <url> PostgreSQL connection string (env: DATABASE_URL)
147
+ --opencode-url <url> OpenCode server URL (default: http://localhost:4096)
148
+ --host <host> Bind host (default: 127.0.0.1)
149
+ --port <port> Bind port (default: 7654)
150
+ --memory-path <path> Directory for memory storage (default: ./.memory)
151
+ --password <password> REQUIRED. API password (env: AGENT_OFFICE_PASSWORD)
91
152
  ```
92
153
 
93
- ### `agent-office manage <url>`
154
+ ### `agent-office manage`
94
155
 
95
- Opens the full-screen terminal UI. All operations go through the server `manage` never touches the database or OpenCode directly.
156
+ Opens the full-screen terminal UI. All operations go through the HTTP server -- the TUI never touches PostgreSQL or OpenCode directly.
96
157
 
97
158
  ```
98
- Arguments:
99
- url URL of the agent-office server
100
-
101
159
  Options:
102
- --password <password> REQUIRED. API password (env: AGENT_OFFICE_PASSWORD)
160
+ --url <url> Server URL (default: http://127.0.0.1:7654)
161
+ --password <password> REQUIRED. API password (env: AGENT_OFFICE_PASSWORD)
103
162
  ```
104
163
 
105
- #### TUI screens
164
+ #### TUI Features
106
165
 
107
166
  | Screen | Description |
108
167
  |---|---|
109
- | List sessions | View all named sessions with masked agent codes. `↑↓` to navigate, `r` to reveal/hide the selected session's agent code |
110
- | Create session | Create a new named session. Automatically creates an OpenCode session and generates an agent code |
111
- | Delete session | Select and delete a session (also deletes the OpenCode session) |
112
- | Tail messages | View the most recent messages in a session |
113
- | Inject text | Send a message into a session |
114
- | Agent code | Reveal, hide, or regenerate the agent code for a session |
168
+ | **Coworkers** | Table of all agents showing name, status, mode, session ID, and masked agent code. Keyboard: `c` create, `d` delete, `r` reveal code, `g` regenerate code, `x` revert session, `X` revert all, `t` tail messages, `i` inject text, `m` coworker mail, `M` memories |
169
+ | **Send message** | Select a recipient and compose a message |
170
+ | **My mail** | View received and sent messages. `r` reply, `m` mark read, `a` mark all read. Tab between received/sent |
171
+ | **Cron jobs** | Table of scheduled tasks with name, coworker, schedule, next run, and status. Create, delete, enable/disable, view history |
172
+ | **My profile** | Set your display name and description (visible to agents in their welcome message) |
115
173
 
116
- ### `agent-office worker clock-in <token>`
174
+ A sidebar shows all coworkers with live status indicators, refreshed every 5 seconds. An unread mail badge appears in the header.
117
175
 
118
- Clock in as a named agent. No password required — authentication is via the agent code.
176
+ ### `agent-office communicator web <coworker>`
119
177
 
120
- ```bash
121
- agent-office worker clock-in <agent_code>@<server-url>
178
+ Launches a browser-based chat interface for real-time conversation with a specific agent.
122
179
 
123
- # Example
124
- agent-office worker clock-in 550e8400-e29b-41d4-a716-446655440000@http://localhost:7654
125
180
  ```
181
+ Arguments:
182
+ <coworker> Name of the coworker to chat with
126
183
 
127
- On success, prints:
128
-
184
+ Options:
185
+ --url <url> Server URL (default: http://127.0.0.1:7654)
186
+ --secret <secret> API password (env: AGENT_OFFICE_PASSWORD)
187
+ --host <host> Communicator bind host (default: 127.0.0.1)
188
+ --port <port> Communicator bind port (default: 7655)
129
189
  ```
130
- Welcome to the agent office, your name is john. Your OpenCode session ID is <id>. You are now clocked in and ready to work.
190
+
191
+ Features: dark theme, iMessage-style chat bubbles, auto-scroll, Enter to send (Shift+Enter for newline), live message polling (5s), unread indicators, status display, and a reset button to revert the agent's session.
192
+
193
+ ### `agent-office worker` (for AI agents)
194
+
195
+ All worker commands authenticate via a token in the format `<agent_code>@<server_url>`.
196
+
197
+ ```bash
198
+ # Clock in and receive your briefing
199
+ agent-office worker clock-in <token>
200
+
201
+ # List coworkers and the human manager
202
+ agent-office worker list-coworkers <token>
203
+
204
+ # Set or clear your public status (max 140 chars)
205
+ agent-office worker set-status --status "Working on auth module" <token>
206
+ agent-office worker set-status --clear <token>
207
+
208
+ # Send a message to one or more recipients
209
+ agent-office worker send-message --name Alice --name Bob --body "Status update: PR is ready" <token>
210
+
211
+ # Cron job management
212
+ agent-office worker cron list <token>
213
+ agent-office worker cron create --name "daily-report" --schedule "0 9 * * *" --message "Send daily status" <token>
214
+ agent-office worker cron create --name "weekly" --schedule "0 9 * * 1" --message "Weekly sync" --timezone "America/New_York" <token>
215
+ agent-office worker cron delete <token> <id>
216
+ agent-office worker cron enable <token> <id>
217
+ agent-office worker cron disable <token> <id>
218
+ agent-office worker cron history <token> <id>
219
+
220
+ # Persistent memory (survives session resets)
221
+ agent-office worker memory add --content "The auth module uses JWT with RS256" <token>
222
+ agent-office worker memory search --query "authentication" <token>
223
+ agent-office worker memory list <token>
224
+ agent-office worker memory forget <token> <memory-id>
131
225
  ```
132
226
 
133
227
  ## REST API
134
228
 
135
- All endpoints except `/worker/clock-in` require `Authorization: Bearer <password>`.
229
+ ### Authenticated Endpoints (Bearer token required)
136
230
 
137
231
  | Method | Path | Description |
138
232
  |---|---|---|
139
233
  | `GET` | `/health` | Health check |
234
+ | `GET` | `/modes` | List available agent modes from the OpenCode server |
140
235
  | `GET` | `/sessions` | List all sessions |
141
- | `POST` | `/sessions` | Create session `{ name }` |
142
- | `DELETE` | `/sessions/:name` | Delete session by name |
143
- | `GET` | `/sessions/:name/messages` | Fetch recent messages (`?limit=N`, max 100) |
144
- | `POST` | `/sessions/:name/inject` | Inject text `{ text, modelID?, providerID? }` |
145
- | `POST` | `/sessions/:name/regenerate-code` | Regenerate agent code |
146
- | `GET` | `/worker/clock-in?code=<uuid>` | Validate agent code (no auth) |
236
+ | `POST` | `/sessions` | Create a session. Body: `{ name, agent? }` |
237
+ | `DELETE` | `/sessions/:name` | Delete a session |
238
+ | `POST` | `/sessions/:name/regenerate-code` | Regenerate the agent code UUID |
239
+ | `GET` | `/sessions/:name/messages` | Fetch messages. Query: `?limit=N` (max 100) |
240
+ | `POST` | `/sessions/:name/inject` | Inject text. Body: `{ text }` |
241
+ | `POST` | `/sessions/:name/revert-to-start` | Revert session and re-enroll |
242
+ | `POST` | `/sessions/revert-all` | Revert all sessions |
243
+ | `GET` | `/config` | Get all config values |
244
+ | `PUT` | `/config` | Set a config value. Body: `{ key, value }` |
245
+ | `GET` | `/messages/:name` | Get messages for a person. Query: `?sent=true`, `?unread_only=true` |
246
+ | `POST` | `/messages` | Send a message. Body: `{ from, to: string[], body }` |
247
+ | `POST` | `/messages/:id/read` | Mark a message as read |
248
+ | `GET` | `/crons` | List cron jobs. Query: `?session_name=<name>` |
249
+ | `POST` | `/crons` | Create a cron job. Body: `{ name, session_name, schedule, message, timezone? }` |
250
+ | `DELETE` | `/crons/:id` | Delete a cron job |
251
+ | `POST` | `/crons/:id/enable` | Enable a cron job |
252
+ | `POST` | `/crons/:id/disable` | Disable a cron job |
253
+ | `GET` | `/crons/:id/history` | Cron execution history. Query: `?limit=N` |
254
+ | `GET` | `/sessions/:name/memories` | List memories. Query: `?limit=N` |
255
+ | `POST` | `/sessions/:name/memories` | Add a memory. Body: `{ content, metadata? }` |
256
+ | `GET` | `/sessions/:name/memories/:id` | Get a memory |
257
+ | `PUT` | `/sessions/:name/memories/:id` | Update a memory. Body: `{ content, metadata? }` |
258
+ | `DELETE` | `/sessions/:name/memories/:id` | Delete a memory |
259
+ | `POST` | `/sessions/:name/memories/search` | Search memories. Body: `{ query, limit? }` |
260
+
261
+ ### Worker Endpoints (agent code auth via `?code=<uuid>`)
262
+
263
+ | Method | Path | Description |
264
+ |---|---|---|
265
+ | `GET` | `/worker/clock-in` | Clock in and receive welcome briefing |
266
+ | `GET` | `/worker/list-coworkers` | List all other agents and the human manager |
267
+ | `POST` | `/worker/set-status` | Set or clear status. Body: `{ status }` |
268
+ | `POST` | `/worker/send-message` | Send a message. Body: `{ to: string[], body }` |
269
+ | `GET` | `/worker/crons` | List own cron jobs |
270
+ | `POST` | `/worker/crons` | Create a cron job |
271
+ | `DELETE` | `/worker/crons/:id` | Delete own cron job |
272
+ | `POST` | `/worker/crons/:id/enable` | Enable own cron job |
273
+ | `POST` | `/worker/crons/:id/disable` | Disable own cron job |
274
+ | `GET` | `/worker/crons/:id/history` | View own cron job history |
275
+ | `POST` | `/worker/memory/add` | Add a memory |
276
+ | `POST` | `/worker/memory/search` | Search memories |
277
+ | `GET` | `/worker/memory/list` | List memories |
278
+ | `DELETE` | `/worker/memory/:memoryId` | Delete a memory |
279
+
280
+ ## Architecture
281
+
282
+ ### Database Schema
283
+
284
+ The server uses PostgreSQL with automatic migrations. Tables:
285
+
286
+ - **`sessions`** -- Maps coworker names to OpenCode session IDs with agent codes, agent modes, and status
287
+ - **`config`** -- Key-value store for application settings (`human_name`, `human_description`)
288
+ - **`messages`** -- Inter-agent and human-agent mail with read/injected tracking
289
+ - **`cron_jobs`** -- Scheduled tasks tied to sessions with cron expressions and timezone support
290
+ - **`cron_history`** -- Execution log for cron jobs with success/failure tracking
291
+
292
+ ### Memory System
293
+
294
+ Each agent gets a private SQLite database (via [fastmemory](https://github.com/nichochar/fastmemory)) stored in the `--memory-path` directory. Memories support:
295
+
296
+ - **Hybrid search** -- BM25 full-text search combined with vector semantic search using Reciprocal Rank Fusion
297
+ - **Persistence across resets** -- Memories are stored outside the OpenCode session, so they survive session reverts
298
+ - **Quantized embeddings** -- Uses `q4` dtype for efficient storage
299
+
300
+ ### Agentic Coding Server Abstraction
301
+
302
+ The application does not depend on the OpenCode SDK directly. Instead, all OpenCode interactions go through an `AgenticCodingServer` interface (`src/lib/agentic-coding-server.ts`) with six methods:
303
+
304
+ ```typescript
305
+ interface AgenticCodingServer {
306
+ createSession(): Promise<string>
307
+ deleteSession(sessionID: string): Promise<void>
308
+ sendMessage(sessionID: string, text: string, agent?: string): Promise<void>
309
+ getMessages(sessionID: string, limit?: number): Promise<SessionMessage[]>
310
+ revertSession(sessionID: string, messageID: string): Promise<void>
311
+ getAgentModes(): Promise<AgentMode[]>
312
+ }
313
+ ```
314
+
315
+ The concrete `OpenCodeCodingServer` implementation (`src/lib/opencode-coding-server.ts`) encapsulates all SDK interactions. Only the server entrypoint (`src/commands/serve.ts`) knows the concrete class -- everything else depends on the interface.
147
316
 
148
317
  ## Development
149
318
 
@@ -151,15 +320,17 @@ All endpoints except `/worker/clock-in` require `Authorization: Bearer <password
151
320
  # Install dependencies
152
321
  npm install
153
322
 
154
- # Copy env
323
+ # Copy and edit environment
155
324
  cp .env.example .env
156
- # Edit .env with your values
157
325
 
158
- # Run server
326
+ # Run server with hot reload
159
327
  npm run dev:serve -- --password secret
160
328
 
161
- # Run manager (in another terminal)
162
- npm run dev:manage -- http://localhost:7654 --password secret
329
+ # Run TUI (in another terminal)
330
+ npm run dev:manage -- --password secret
331
+
332
+ # Run communicator (in another terminal)
333
+ npm run dev:communicator -- "Alice" --secret secret
163
334
 
164
335
  # Build
165
336
  npm run build
@@ -167,4 +338,4 @@ npm run build
167
338
 
168
339
  ## License
169
340
 
170
- MIT © Richard Anaya
341
+ MIT
@@ -59,11 +59,12 @@ function renderMessage(msg, humanName, spacingClass) {
59
59
  const isMine = msg.from_name === humanName;
60
60
  const bubbleClass = isMine ? "bubble bubble-mine" : "bubble bubble-theirs";
61
61
  const wrapClass = `msg-wrap ${isMine ? "msg-wrap-mine" : "msg-wrap-theirs"} ${spacingClass}`;
62
- const bodyHtml = escapeHtml(msg.body).replace(/\n/g, "<br>");
62
+ // Base64 encode to preserve newlines and special chars reliably
63
+ const bodyEncoded = Buffer.from(msg.body).toString('base64');
63
64
  const unreadDot = !isMine && !msg.read ? `<span class="unread-dot"></span>` : "";
64
65
  return `<div class="${wrapClass}" data-id="${msg.id}">
65
66
  <div class="${bubbleClass}">
66
- <div class="bubble-body">${bodyHtml}</div>
67
+ <div class="bubble-body markdown-body" data-markdown-b64="${bodyEncoded}"></div>
67
68
  <div class="bubble-time">${unreadDot}${formatTime(msg.created_at)}</div>
68
69
  </div>
69
70
  </div>`;
@@ -91,6 +92,9 @@ function renderPage(coworker, msgs, humanName) {
91
92
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
92
93
  <title>${escapeHtml(coworker)} — agent-office</title>
93
94
  <script src="https://unpkg.com/htmx.org@2.0.4/dist/htmx.min.js"></script>
95
+ <script src="https://cdn.jsdelivr.net/npm/marked@11.1.1/marked.min.js"></script>
96
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/github-markdown-css@5.5.1/github-markdown-light.min.css" media="(prefers-color-scheme: light)">
97
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/github-markdown-css@5.5.1/github-markdown-dark.min.css" media="(prefers-color-scheme: dark)">
94
98
  <style>
95
99
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
96
100
 
@@ -264,6 +268,66 @@ function renderPage(coworker, msgs, humanName) {
264
268
  }
265
269
 
266
270
  .bubble-body { font-size: 14.5px; }
271
+ .bubble-body.markdown-body {
272
+ background: transparent;
273
+ color: inherit;
274
+ font-size: 14px;
275
+ line-height: 1.5;
276
+ }
277
+ .bubble-body.markdown-body p { margin: 0 0 8px 0; }
278
+ .bubble-body.markdown-body p:last-child { margin-bottom: 0; }
279
+ .bubble-body.markdown-body pre {
280
+ background: rgba(0,0,0,0.3);
281
+ border-radius: 6px;
282
+ padding: 8px 12px;
283
+ overflow-x: auto;
284
+ margin: 8px 0;
285
+ }
286
+ .bubble-body.markdown-body code {
287
+ background: rgba(0,0,0,0.2);
288
+ padding: 2px 5px;
289
+ border-radius: 3px;
290
+ font-size: 13px;
291
+ font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
292
+ }
293
+ .bubble-body.markdown-body pre code {
294
+ background: transparent;
295
+ padding: 0;
296
+ }
297
+ .bubble-body.markdown-body ul, .bubble-body.markdown-body ol {
298
+ margin: 8px 0;
299
+ padding-left: 20px;
300
+ }
301
+ .bubble-body.markdown-body li { margin: 4px 0; }
302
+ .bubble-body.markdown-body blockquote {
303
+ border-left: 3px solid var(--accent);
304
+ margin: 8px 0;
305
+ padding-left: 12px;
306
+ color: var(--text-dim);
307
+ }
308
+ .bubble-body.markdown-body h1, .bubble-body.markdown-body h2,
309
+ .bubble-body.markdown-body h3, .bubble-body.markdown-body h4 {
310
+ margin: 12px 0 8px 0;
311
+ font-size: 15px;
312
+ font-weight: 600;
313
+ }
314
+ .bubble-body.markdown-body table {
315
+ border-collapse: collapse;
316
+ margin: 8px 0;
317
+ font-size: 13px;
318
+ }
319
+ .bubble-body.markdown-body th, .bubble-body.markdown-body td {
320
+ border: 1px solid var(--border);
321
+ padding: 6px 10px;
322
+ }
323
+ .bubble-body.markdown-body th {
324
+ background: var(--surface2);
325
+ }
326
+ .bubble-body.markdown-body strong,
327
+ .bubble-body.markdown-body b {
328
+ color: #ffffff;
329
+ font-weight: 600;
330
+ }
267
331
  .bubble-time {
268
332
  font-size: 10px;
269
333
  color: var(--text-dim);
@@ -498,6 +562,28 @@ function renderPage(coworker, msgs, humanName) {
498
562
  clearTimeout(el._hideTimer)
499
563
  el._hideTimer = setTimeout(() => el.classList.remove('visible'), 3000)
500
564
  }
565
+
566
+ // Render markdown in chat bubbles
567
+ function renderMarkdown() {
568
+ if (typeof marked === 'undefined') return
569
+ document.querySelectorAll('.markdown-body[data-markdown-b64]').forEach(el => {
570
+ const b64 = el.getAttribute('data-markdown-b64')
571
+ if (b64 && !el.hasAttribute('data-rendered')) {
572
+ // Decode base64 to get original text with preserved newlines
573
+ const text = atob(b64)
574
+ el.innerHTML = marked.parse(text)
575
+ el.setAttribute('data-rendered', 'true')
576
+ }
577
+ })
578
+ }
579
+
580
+ // Initial render
581
+ renderMarkdown()
582
+
583
+ // Re-render after HTMX swaps new content
584
+ document.addEventListener('htmx:afterSwap', () => {
585
+ renderMarkdown()
586
+ })
501
587
  </script>
502
588
  </body>
503
589
  </html>`;
@@ -609,10 +695,19 @@ export async function communicatorWeb(coworker, options) {
609
695
  app.get("/ping", (_req, res) => {
610
696
  res.status(204).end();
611
697
  });
612
- app.listen(port, host, () => {
698
+ const server = app.listen(port, host, () => {
613
699
  console.log(`Communicator running at http://${host}:${port}`);
614
700
  console.log(`Press Ctrl+C to stop.`);
615
701
  });
702
+ server.on('error', (err) => {
703
+ if (err.code === 'EADDRINUSE') {
704
+ console.error(`Error: Port ${port} is already in use. Is another instance running?`);
705
+ }
706
+ else {
707
+ console.error(`Error: ${err.message}`);
708
+ }
709
+ process.exit(1);
710
+ });
616
711
  // Keep process alive
617
712
  await new Promise(() => { });
618
713
  }
@@ -1,6 +1,6 @@
1
1
  import { createDb } from "../db/index.js";
2
2
  import { runMigrations } from "../db/migrate.js";
3
- import { createOpencodeClient } from "../lib/opencode.js";
3
+ import { OpenCodeCodingServer } from "../lib/opencode-coding-server.js";
4
4
  import { createApp } from "../server/index.js";
5
5
  import { CronScheduler } from "../server/cron.js";
6
6
  import { MemoryManager } from "../server/memory.js";
@@ -34,8 +34,8 @@ export async function serve(options) {
34
34
  await sql.end();
35
35
  process.exit(1);
36
36
  }
37
- // Init OpenCode client
38
- const opencode = createOpencodeClient(options.opencodeUrl);
37
+ // Init agentic coding server (OpenCode implementation)
38
+ const agenticCodingServer = new OpenCodeCodingServer(options.opencodeUrl);
39
39
  const serverUrl = `http://${options.host}:${port}`;
40
40
  // Create memory manager and verify embedding model
41
41
  const memoryManager = new MemoryManager(options.memoryPath);
@@ -55,9 +55,9 @@ export async function serve(options) {
55
55
  // Create cron scheduler
56
56
  const cronScheduler = new CronScheduler();
57
57
  // Create Express app
58
- const app = createApp(sql, opencode, password, serverUrl, cronScheduler, memoryManager);
58
+ const app = createApp(sql, agenticCodingServer, password, serverUrl, cronScheduler, memoryManager);
59
59
  // Start cron scheduler
60
- await cronScheduler.start(sql, opencode);
60
+ await cronScheduler.start(sql, agenticCodingServer);
61
61
  // Start server
62
62
  const server = app.listen(port, options.host, () => {
63
63
  console.log(`agent-office server listening on ${serverUrl}`);
@@ -0,0 +1,58 @@
1
+ /**
2
+ * AgenticCodingServer — abstract interface for interacting with an agentic
3
+ * coding backend. The application code depends only on this interface;
4
+ * concrete implementations (e.g. OpenCodeCodingServer) supply the wiring
5
+ * to a specific server.
6
+ */
7
+ /** Minimal representation of a message part. */
8
+ export interface MessagePart {
9
+ type: string;
10
+ [key: string]: unknown;
11
+ }
12
+ /** A single message inside a session. */
13
+ export interface SessionMessage {
14
+ id: string;
15
+ role: string;
16
+ parts: MessagePart[];
17
+ }
18
+ /** An agent mode advertised by the server. */
19
+ export interface AgentMode {
20
+ name: string;
21
+ description: string;
22
+ model: string;
23
+ }
24
+ export interface AgenticCodingServer {
25
+ /**
26
+ * Create a new session.
27
+ * @returns the server-assigned session ID.
28
+ */
29
+ createSession(): Promise<string>;
30
+ /**
31
+ * Permanently delete a session.
32
+ */
33
+ deleteSession(sessionID: string): Promise<void>;
34
+ /**
35
+ * Inject a text message into a session (fire-and-forget).
36
+ * The server processes the message asynchronously.
37
+ *
38
+ * @param agent optional agent-mode identifier to route the prompt.
39
+ */
40
+ sendMessage(sessionID: string, text: string, agent?: string): Promise<void>;
41
+ /**
42
+ * Retrieve the message history for a session.
43
+ *
44
+ * @param limit maximum number of messages to return (server may cap this).
45
+ */
46
+ getMessages(sessionID: string, limit?: number): Promise<SessionMessage[]>;
47
+ /**
48
+ * Reset a session to a specific message.
49
+ *
50
+ * Implementations should abort any in-progress generation before reverting
51
+ * so callers don't need to worry about "session is busy" errors.
52
+ */
53
+ revertSession(sessionID: string, messageID: string): Promise<void>;
54
+ /**
55
+ * Return the list of agent modes the server knows about.
56
+ */
57
+ getAgentModes(): Promise<AgentMode[]>;
58
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * AgenticCodingServer — abstract interface for interacting with an agentic
3
+ * coding backend. The application code depends only on this interface;
4
+ * concrete implementations (e.g. OpenCodeCodingServer) supply the wiring
5
+ * to a specific server.
6
+ */
7
+ export {};
@@ -0,0 +1,11 @@
1
+ import type { AgenticCodingServer, SessionMessage, AgentMode } from "./agentic-coding-server.js";
2
+ export declare class OpenCodeCodingServer implements AgenticCodingServer {
3
+ private client;
4
+ constructor(baseURL: string);
5
+ createSession(): Promise<string>;
6
+ deleteSession(sessionID: string): Promise<void>;
7
+ sendMessage(sessionID: string, text: string, agent?: string): Promise<void>;
8
+ getMessages(sessionID: string, limit?: number): Promise<SessionMessage[]>;
9
+ revertSession(sessionID: string, messageID: string): Promise<void>;
10
+ getAgentModes(): Promise<AgentMode[]>;
11
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Concrete AgenticCodingServer backed by an OpenCode server
3
+ * via the @opencode-ai/sdk.
4
+ */
5
+ import { createOpencodeClient } from "@opencode-ai/sdk/v2/client";
6
+ import { cwd } from "process";
7
+ export class OpenCodeCodingServer {
8
+ client;
9
+ constructor(baseURL) {
10
+ this.client = createOpencodeClient({ baseUrl: baseURL, directory: cwd() });
11
+ }
12
+ async createSession() {
13
+ const { data: session } = await this.client.session.create();
14
+ if (!session)
15
+ throw new Error("OpenCode returned no session");
16
+ return session.id;
17
+ }
18
+ async deleteSession(sessionID) {
19
+ await this.client.session.delete({ sessionID });
20
+ }
21
+ async sendMessage(sessionID, text, agent) {
22
+ await this.client.session.promptAsync({
23
+ sessionID,
24
+ parts: [{ type: "text", text }],
25
+ ...(agent ? { agent } : {}),
26
+ });
27
+ }
28
+ async getMessages(sessionID, limit) {
29
+ const { data: messages } = await this.client.session.messages({
30
+ sessionID,
31
+ ...(limit != null ? { limit } : {}),
32
+ });
33
+ return (messages ?? []).map((m) => ({
34
+ id: m.info.id,
35
+ role: m.info.role,
36
+ parts: m.parts,
37
+ }));
38
+ }
39
+ async revertSession(sessionID, messageID) {
40
+ // Abort any in-progress generation first — swallow errors because the
41
+ // session may not be busy.
42
+ await this.client.session.abort({ sessionID }).catch(() => { });
43
+ await this.client.session.revert({ sessionID, messageID });
44
+ }
45
+ async getAgentModes() {
46
+ const { data: config } = await this.client.config.get();
47
+ const agent = config?.agent ?? {};
48
+ return Object.entries(agent)
49
+ .filter(([, val]) => val != null)
50
+ .map(([name, val]) => ({
51
+ name,
52
+ description: val.description ?? "",
53
+ model: val.model ?? "",
54
+ }));
55
+ }
56
+ }
@@ -1,5 +1,5 @@
1
1
  import type { Sql } from "../db/index.js";
2
- import type { OpencodeClient } from "../lib/opencode.js";
2
+ import type { AgenticCodingServer } from "../lib/agentic-coding-server.js";
3
3
  import type { CronJobRow } from "../db/index.js";
4
4
  interface CronSchedulerOptions {
5
5
  onJobExecuted?: (jobId: number, success: boolean, error?: string) => void;
@@ -8,10 +8,10 @@ export declare class CronScheduler {
8
8
  private options;
9
9
  private activeJobs;
10
10
  private sql;
11
- private opencode;
11
+ private agenticCodingServer;
12
12
  private started;
13
13
  constructor(options?: CronSchedulerOptions);
14
- start(sql: Sql, opencode: OpencodeClient): Promise<void>;
14
+ start(sql: Sql, agenticCodingServer: AgenticCodingServer): Promise<void>;
15
15
  stop(): void;
16
16
  private addJob;
17
17
  private executeJob;
@@ -9,16 +9,16 @@ export class CronScheduler {
9
9
  options;
10
10
  activeJobs = new Map();
11
11
  sql = null;
12
- opencode = null;
12
+ agenticCodingServer = null;
13
13
  started = false;
14
14
  constructor(options = {}) {
15
15
  this.options = options;
16
16
  }
17
- async start(sql, opencode) {
17
+ async start(sql, agenticCodingServer) {
18
18
  if (this.started)
19
19
  return;
20
20
  this.sql = sql;
21
- this.opencode = opencode;
21
+ this.agenticCodingServer = agenticCodingServer;
22
22
  const rows = await sql `
23
23
  SELECT id, name, schedule, timezone, message, session_name
24
24
  FROM cron_jobs
@@ -39,7 +39,7 @@ export class CronScheduler {
39
39
  console.log("Cron scheduler stopped");
40
40
  }
41
41
  addJob(job) {
42
- if (!this.opencode || !this.sql)
42
+ if (!this.agenticCodingServer || !this.sql)
43
43
  return;
44
44
  const options = {
45
45
  protect: true,
@@ -54,7 +54,7 @@ export class CronScheduler {
54
54
  this.activeJobs.set(job.id, { cron, job });
55
55
  }
56
56
  async executeJob(job) {
57
- if (!this.sql || !this.opencode)
57
+ if (!this.sql || !this.agenticCodingServer)
58
58
  return;
59
59
  const executedAt = new Date();
60
60
  try {
@@ -65,11 +65,7 @@ export class CronScheduler {
65
65
  throw new Error(`Session "${job.session_name}" not found`);
66
66
  }
67
67
  const injectText = `[Cron Job "${job.name}" — ${executedAt.toISOString()}]\n${job.message}${CRON_INJECTION_BLURB}`;
68
- await this.opencode.session.promptAsync({
69
- sessionID: session.session_id,
70
- parts: [{ type: "text", text: injectText }],
71
- ...(session.agent ? { agent: session.agent } : {}),
72
- });
68
+ await this.agenticCodingServer.sendMessage(session.session_id, injectText, session.agent ?? undefined);
73
69
  await this.sql `
74
70
  UPDATE cron_jobs SET last_run = ${executedAt} WHERE id = ${job.id}
75
71
  `;
@@ -1,5 +1,5 @@
1
1
  import type { Sql } from "../db/index.js";
2
- import type { OpencodeClient } from "../lib/opencode.js";
2
+ import type { AgenticCodingServer } from "../lib/agentic-coding-server.js";
3
3
  import { CronScheduler } from "./cron.js";
4
4
  import type { MemoryManager } from "./memory.js";
5
- export declare function createApp(sql: Sql, opencode: OpencodeClient, password: string, serverUrl: string, cronScheduler: CronScheduler, memoryManager: MemoryManager): import("express-serve-static-core").Express;
5
+ export declare function createApp(sql: Sql, agenticCodingServer: AgenticCodingServer, password: string, serverUrl: string, cronScheduler: CronScheduler, memoryManager: MemoryManager): import("express-serve-static-core").Express;
@@ -10,13 +10,13 @@ function authMiddleware(password) {
10
10
  next();
11
11
  };
12
12
  }
13
- export function createApp(sql, opencode, password, serverUrl, cronScheduler, memoryManager) {
13
+ export function createApp(sql, agenticCodingServer, password, serverUrl, cronScheduler, memoryManager) {
14
14
  const app = express();
15
15
  app.use(express.json());
16
16
  // Worker routes are unauthenticated — mounted before auth middleware
17
- app.use("/", createWorkerRouter(sql, opencode, serverUrl, memoryManager));
17
+ app.use("/", createWorkerRouter(sql, agenticCodingServer, serverUrl, memoryManager));
18
18
  // Everything else requires Bearer auth
19
19
  app.use(authMiddleware(password));
20
- app.use("/", createRouter(sql, opencode, serverUrl, cronScheduler, memoryManager));
20
+ app.use("/", createRouter(sql, agenticCodingServer, serverUrl, cronScheduler, memoryManager));
21
21
  return app;
22
22
  }
@@ -1,7 +1,7 @@
1
1
  import { Router } from "express";
2
2
  import type { Sql } from "../db/index.js";
3
- import type { OpencodeClient } from "../lib/opencode.js";
3
+ import type { AgenticCodingServer } from "../lib/agentic-coding-server.js";
4
4
  import { CronScheduler } from "./cron.js";
5
5
  import type { MemoryManager } from "./memory.js";
6
- export declare function createRouter(sql: Sql, opencode: OpencodeClient, serverUrl: string, scheduler: CronScheduler, memoryManager: MemoryManager): Router;
7
- export declare function createWorkerRouter(sql: Sql, opencode: OpencodeClient, serverUrl: string, memoryManager: MemoryManager): Router;
6
+ export declare function createRouter(sql: Sql, agenticCodingServer: AgenticCodingServer, serverUrl: string, scheduler: CronScheduler, memoryManager: MemoryManager): Router;
7
+ export declare function createWorkerRouter(sql: Sql, agenticCodingServer: AgenticCodingServer, serverUrl: string, memoryManager: MemoryManager): Router;
@@ -129,22 +129,14 @@ function generateWelcomeMessage(name, agent, status, humanName, humanDescription
129
129
  ``,
130
130
  ].join("\n");
131
131
  }
132
- export function createRouter(sql, opencode, serverUrl, scheduler, memoryManager) {
132
+ export function createRouter(sql, agenticCodingServer, serverUrl, scheduler, memoryManager) {
133
133
  const router = Router();
134
134
  router.get("/health", (_req, res) => {
135
135
  res.json({ ok: true });
136
136
  });
137
137
  router.get("/modes", async (_req, res) => {
138
138
  try {
139
- const { data: config } = await opencode.config.get();
140
- const agent = config?.agent ?? {};
141
- const modes = Object.entries(agent)
142
- .filter(([, val]) => val != null)
143
- .map(([name, val]) => ({
144
- name,
145
- description: val.description ?? "",
146
- model: val.model ?? "",
147
- }));
139
+ const modes = await agenticCodingServer.getAgentModes();
148
140
  res.json(modes);
149
141
  }
150
142
  catch (err) {
@@ -183,10 +175,7 @@ export function createRouter(sql, opencode, serverUrl, scheduler, memoryManager)
183
175
  }
184
176
  let opencodeSessionId;
185
177
  try {
186
- const { data: session } = await opencode.session.create();
187
- if (!session)
188
- throw new Error("No session returned");
189
- opencodeSessionId = session.id;
178
+ opencodeSessionId = await agenticCodingServer.createSession();
190
179
  }
191
180
  catch (err) {
192
181
  console.error("OpenCode session.create error:", err);
@@ -205,7 +194,7 @@ export function createRouter(sql, opencode, serverUrl, scheduler, memoryManager)
205
194
  catch (err) {
206
195
  console.error("DB insert error:", err);
207
196
  try {
208
- await opencode.session.delete({ sessionID: opencodeSessionId });
197
+ await agenticCodingServer.deleteSession(opencodeSessionId);
209
198
  }
210
199
  catch { }
211
200
  res.status(500).json({ error: "Internal server error" });
@@ -214,11 +203,7 @@ export function createRouter(sql, opencode, serverUrl, scheduler, memoryManager)
214
203
  try {
215
204
  const clockInToken = `${row.agent_code}@${serverUrl}`;
216
205
  const enrollmentMessage = generateEnrollmentMessage(clockInToken);
217
- await opencode.session.promptAsync({
218
- sessionID: opencodeSessionId,
219
- parts: [{ type: "text", text: enrollmentMessage }],
220
- ...(trimmedAgent ? { agent: trimmedAgent } : {}),
221
- });
206
+ await agenticCodingServer.sendMessage(opencodeSessionId, enrollmentMessage, trimmedAgent ?? undefined);
222
207
  }
223
208
  catch (err) {
224
209
  console.warn("Warning: could not send first message to session:", err);
@@ -260,29 +245,28 @@ export function createRouter(sql, opencode, serverUrl, scheduler, memoryManager)
260
245
  }
261
246
  const row = rows[0];
262
247
  try {
263
- const { data: messages } = await opencode.session.messages({ sessionID: row.session_id, limit });
264
- const result = (messages ?? [])
248
+ const messages = await agenticCodingServer.getMessages(row.session_id, limit);
249
+ const result = messages
265
250
  .slice(-limit)
266
251
  .map((m) => ({
267
- role: m.info.role,
252
+ role: m.role,
268
253
  parts: m.parts.map((p) => {
269
- const part = p;
270
- if (part.type === "text") {
271
- return { type: "text", text: part.text ?? "" };
254
+ if (p.type === "text") {
255
+ return { type: "text", text: p.text ?? "" };
272
256
  }
273
- else if (part.type === "tool") {
257
+ else if (p.type === "tool") {
274
258
  // Extract tool name, input, and output from the tool state
275
- const state = part.state;
259
+ const state = p.state;
276
260
  return {
277
261
  type: "tool",
278
- tool: part.tool,
262
+ tool: p.tool,
279
263
  input: state?.input,
280
264
  output: state?.output,
281
- data: part,
265
+ data: p,
282
266
  };
283
267
  }
284
268
  else {
285
- return { type: part.type, data: part };
269
+ return { type: p.type, data: p };
286
270
  }
287
271
  }),
288
272
  }))
@@ -310,10 +294,7 @@ export function createRouter(sql, opencode, serverUrl, scheduler, memoryManager)
310
294
  }
311
295
  const row = rows[0];
312
296
  try {
313
- await opencode.session.promptAsync({
314
- sessionID: row.session_id,
315
- parts: [{ type: "text", text: text.trim() }],
316
- });
297
+ await agenticCodingServer.sendMessage(row.session_id, text.trim());
317
298
  res.json({ ok: true });
318
299
  }
319
300
  catch (err) {
@@ -332,28 +313,21 @@ export function createRouter(sql, opencode, serverUrl, scheduler, memoryManager)
332
313
  }
333
314
  const session = rows[0];
334
315
  try {
335
- const { data: messages } = await opencode.session.messages({ sessionID: session.session_id });
336
- if (!messages || messages.length === 0) {
316
+ const messages = await agenticCodingServer.getMessages(session.session_id);
317
+ if (messages.length === 0) {
337
318
  res.status(400).json({ error: "Session has no messages to revert to" });
338
319
  return;
339
320
  }
340
321
  const firstMessage = messages[0];
341
- if (!firstMessage || !firstMessage.info || !firstMessage.info.id) {
322
+ if (!firstMessage.id) {
342
323
  res.status(500).json({ error: "Failed to get first message ID" });
343
324
  return;
344
325
  }
345
- // Abort any in-progress generation before reverting, to avoid "session is busy" errors.
346
- // Swallow errors — the session may not be busy, in which case abort is a no-op.
347
- await opencode.session.abort({ sessionID: session.session_id }).catch(() => { });
348
- await opencode.session.revert({ sessionID: session.session_id, messageID: firstMessage.info.id });
326
+ await agenticCodingServer.revertSession(session.session_id, firstMessage.id);
349
327
  const clockInToken = `${session.agent_code}@${serverUrl}`;
350
328
  const enrollmentMessage = generateEnrollmentMessage(clockInToken);
351
- await opencode.session.promptAsync({
352
- sessionID: session.session_id,
353
- parts: [{ type: "text", text: enrollmentMessage }],
354
- ...(session.agent ? { agent: session.agent } : {}),
355
- });
356
- res.json({ ok: true, messageID: firstMessage.info.id });
329
+ await agenticCodingServer.sendMessage(session.session_id, enrollmentMessage, session.agent ?? undefined);
330
+ res.json({ ok: true, messageID: firstMessage.id });
357
331
  }
358
332
  catch (err) {
359
333
  console.error("POST /sessions/:name/revert-to-start error:", err);
@@ -367,26 +341,20 @@ export function createRouter(sql, opencode, serverUrl, scheduler, memoryManager)
367
341
  const results = [];
368
342
  for (const session of allSessions) {
369
343
  try {
370
- const { data: messages } = await opencode.session.messages({ sessionID: session.session_id });
371
- if (!messages || messages.length === 0) {
344
+ const messages = await agenticCodingServer.getMessages(session.session_id);
345
+ if (messages.length === 0) {
372
346
  results.push({ name: session.name, ok: false, error: "No messages" });
373
347
  continue;
374
348
  }
375
349
  const firstMessage = messages[0];
376
- if (!firstMessage?.info?.id) {
350
+ if (!firstMessage.id) {
377
351
  results.push({ name: session.name, ok: false, error: "Failed to get first message ID" });
378
352
  continue;
379
353
  }
380
- // Abort any in-progress generation before reverting
381
- await opencode.session.abort({ sessionID: session.session_id }).catch(() => { });
382
- await opencode.session.revert({ sessionID: session.session_id, messageID: firstMessage.info.id });
354
+ await agenticCodingServer.revertSession(session.session_id, firstMessage.id);
383
355
  const clockInToken = `${session.agent_code}@${serverUrl}`;
384
356
  const enrollmentMessage = generateEnrollmentMessage(clockInToken);
385
- await opencode.session.promptAsync({
386
- sessionID: session.session_id,
387
- parts: [{ type: "text", text: enrollmentMessage }],
388
- ...(session.agent ? { agent: session.agent } : {}),
389
- });
357
+ await agenticCodingServer.sendMessage(session.session_id, enrollmentMessage, session.agent ?? undefined);
390
358
  results.push({ name: session.name, ok: true });
391
359
  }
392
360
  catch (err) {
@@ -408,7 +376,7 @@ export function createRouter(sql, opencode, serverUrl, scheduler, memoryManager)
408
376
  }
409
377
  const row = rows[0];
410
378
  try {
411
- await opencode.session.delete({ sessionID: row.session_id });
379
+ await agenticCodingServer.deleteSession(row.session_id);
412
380
  }
413
381
  catch (err) {
414
382
  console.error("OpenCode session.delete error:", err);
@@ -544,10 +512,7 @@ export function createRouter(sql, opencode, serverUrl, scheduler, memoryManager)
544
512
  const sessionId = sessionMap.get(recipient);
545
513
  const injectText = `[Message from "${trimmedFrom}"]: ${trimmedBody}${MAIL_INJECTION_BLURB}`;
546
514
  try {
547
- await opencode.session.promptAsync({
548
- sessionID: sessionId,
549
- parts: [{ type: "text", text: injectText }],
550
- });
515
+ await agenticCodingServer.sendMessage(sessionId, injectText);
551
516
  await sql `UPDATE messages SET injected = TRUE WHERE id = ${msgId}`;
552
517
  injected = true;
553
518
  }
@@ -956,7 +921,7 @@ export function createRouter(sql, opencode, serverUrl, scheduler, memoryManager)
956
921
  });
957
922
  return router;
958
923
  }
959
- export function createWorkerRouter(sql, opencode, serverUrl, memoryManager) {
924
+ export function createWorkerRouter(sql, agenticCodingServer, serverUrl, memoryManager) {
960
925
  const router = Router();
961
926
  router.get("/worker/clock-in", async (req, res) => {
962
927
  const { code } = req.query;
@@ -1108,10 +1073,7 @@ export function createWorkerRouter(sql, opencode, serverUrl, memoryManager) {
1108
1073
  const recipientSessionId = sessionMap.get(recipient);
1109
1074
  const injectText = `[Message from "${session.name}"]: ${trimmedBody}${MAIL_INJECTION_BLURB}`;
1110
1075
  try {
1111
- await opencode.session.promptAsync({
1112
- sessionID: recipientSessionId,
1113
- parts: [{ type: "text", text: injectText }],
1114
- });
1076
+ await agenticCodingServer.sendMessage(recipientSessionId, injectText);
1115
1077
  await sql `UPDATE messages SET injected = TRUE WHERE id = ${msgId}`;
1116
1078
  injected = true;
1117
1079
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agent-office",
3
- "version": "0.0.12",
4
- "description": "Manage OpenCode sessions with named aliases",
3
+ "version": "0.0.14",
4
+ "description": "An office for your AI agents",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "Richard Anaya",
@@ -34,6 +34,7 @@
34
34
  "dependencies": {
35
35
  "@inkjs/ui": "^2.0.0",
36
36
  "@opencode-ai/sdk": "^1.2.10",
37
+ "agent-office": "^0.0.12",
37
38
  "commander": "^14.0.0",
38
39
  "croner": "^10.0.1",
39
40
  "dotenv": "^17.0.0",
@@ -1,7 +0,0 @@
1
- import { createOpencodeClient as _createOpencodeClient } from "@opencode-ai/sdk/v2/client";
2
- export type OpencodeClient = ReturnType<typeof _createOpencodeClient>;
3
- export declare function createOpencodeClient(baseURL: string): OpencodeClient;
4
- export interface OpencodeSession {
5
- id: string;
6
- title?: string;
7
- }
@@ -1,5 +0,0 @@
1
- import { createOpencodeClient as _createOpencodeClient } from "@opencode-ai/sdk/v2/client";
2
- import { cwd } from "process";
3
- export function createOpencodeClient(baseURL) {
4
- return _createOpencodeClient({ baseUrl: baseURL, directory: cwd() });
5
- }