agent-office 0.0.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.
- package/LICENSE +21 -0
- package/README.md +170 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +41 -0
- package/dist/commands/manage.d.ts +5 -0
- package/dist/commands/manage.js +20 -0
- package/dist/commands/serve.d.ts +9 -0
- package/dist/commands/serve.js +54 -0
- package/dist/commands/worker.d.ts +1 -0
- package/dist/commands/worker.js +50 -0
- package/dist/db/index.d.ts +10 -0
- package/dist/db/index.js +9 -0
- package/dist/db/migrate.d.ts +2 -0
- package/dist/db/migrate.js +45 -0
- package/dist/lib/opencode.d.ts +7 -0
- package/dist/lib/opencode.js +4 -0
- package/dist/manage/app.d.ts +6 -0
- package/dist/manage/app.js +102 -0
- package/dist/manage/components/AgentCode.d.ts +8 -0
- package/dist/manage/components/AgentCode.js +73 -0
- package/dist/manage/components/CreateSession.d.ts +7 -0
- package/dist/manage/components/CreateSession.js +37 -0
- package/dist/manage/components/DeleteSession.d.ts +7 -0
- package/dist/manage/components/DeleteSession.js +55 -0
- package/dist/manage/components/InjectText.d.ts +8 -0
- package/dist/manage/components/InjectText.js +51 -0
- package/dist/manage/components/SessionList.d.ts +8 -0
- package/dist/manage/components/SessionList.js +52 -0
- package/dist/manage/components/TailMessages.d.ts +8 -0
- package/dist/manage/components/TailMessages.js +77 -0
- package/dist/manage/hooks/useApi.d.ts +33 -0
- package/dist/manage/hooks/useApi.js +82 -0
- package/dist/server/index.d.ts +3 -0
- package/dist/server/index.js +22 -0
- package/dist/server/routes.d.ts +5 -0
- package/dist/server/routes.js +228 -0
- package/package.json +50 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Richard Anaya
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# agent-office
|
|
2
|
+
|
|
3
|
+
Manage [OpenCode](https://opencode.ai) sessions with named aliases, agent codes, and a full-screen terminal UI.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
`agent-office` is a CLI tool with two main roles:
|
|
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.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install -g agent-office
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Or run without installing:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npx agent-office serve --help
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Requirements
|
|
26
|
+
|
|
27
|
+
- Node.js >= 18
|
|
28
|
+
- A running [OpenCode](https://opencode.ai) server (`opencode serve`)
|
|
29
|
+
- A PostgreSQL database
|
|
30
|
+
|
|
31
|
+
## Quick Start
|
|
32
|
+
|
|
33
|
+
### 1. Start the server
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
agent-office serve \
|
|
37
|
+
--database-url "postgresql://user:pass@localhost:5432/mydb" \
|
|
38
|
+
--opencode-url "http://localhost:4096" \
|
|
39
|
+
--host 127.0.0.1 \
|
|
40
|
+
--port 7654 \
|
|
41
|
+
--password mysecret
|
|
42
|
+
```
|
|
43
|
+
|
|
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
|
+
```
|
|
51
|
+
|
|
52
|
+
### 2. Open the manager
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
agent-office manage http://localhost:7654 --password mysecret
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### 3. Clock in as a worker
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
agent-office worker clock-in <agent_code>@http://localhost:7654
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Environment Variables
|
|
65
|
+
|
|
66
|
+
Copy `.env.example` to `.env` for local development:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
cp .env.example .env
|
|
70
|
+
```
|
|
71
|
+
|
|
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`) |
|
|
77
|
+
|
|
78
|
+
## Commands
|
|
79
|
+
|
|
80
|
+
### `agent-office serve`
|
|
81
|
+
|
|
82
|
+
Starts the HTTP server. Runs database migrations automatically on startup.
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
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)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### `agent-office manage <url>`
|
|
94
|
+
|
|
95
|
+
Opens the full-screen terminal UI. All operations go through the server — `manage` never touches the database or OpenCode directly.
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
Arguments:
|
|
99
|
+
url URL of the agent-office server
|
|
100
|
+
|
|
101
|
+
Options:
|
|
102
|
+
--password <password> REQUIRED. API password (env: AGENT_OFFICE_PASSWORD)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
#### TUI screens
|
|
106
|
+
|
|
107
|
+
| Screen | Description |
|
|
108
|
+
|---|---|
|
|
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 |
|
|
115
|
+
|
|
116
|
+
### `agent-office worker clock-in <token>`
|
|
117
|
+
|
|
118
|
+
Clock in as a named agent. No password required — authentication is via the agent code.
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
agent-office worker clock-in <agent_code>@<server-url>
|
|
122
|
+
|
|
123
|
+
# Example
|
|
124
|
+
agent-office worker clock-in 550e8400-e29b-41d4-a716-446655440000@http://localhost:7654
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
On success, prints:
|
|
128
|
+
|
|
129
|
+
```
|
|
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.
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## REST API
|
|
134
|
+
|
|
135
|
+
All endpoints except `/worker/clock-in` require `Authorization: Bearer <password>`.
|
|
136
|
+
|
|
137
|
+
| Method | Path | Description |
|
|
138
|
+
|---|---|---|
|
|
139
|
+
| `GET` | `/health` | Health check |
|
|
140
|
+
| `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) |
|
|
147
|
+
|
|
148
|
+
## Development
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
# Install dependencies
|
|
152
|
+
npm install
|
|
153
|
+
|
|
154
|
+
# Copy env
|
|
155
|
+
cp .env.example .env
|
|
156
|
+
# Edit .env with your values
|
|
157
|
+
|
|
158
|
+
# Run server
|
|
159
|
+
npm run dev:serve -- --password secret
|
|
160
|
+
|
|
161
|
+
# Run manager (in another terminal)
|
|
162
|
+
npm run dev:manage -- http://localhost:7654 --password secret
|
|
163
|
+
|
|
164
|
+
# Build
|
|
165
|
+
npm run build
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## License
|
|
169
|
+
|
|
170
|
+
MIT © Richard Anaya
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import "dotenv/config";
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
const program = new Command();
|
|
5
|
+
program
|
|
6
|
+
.name("agent-office")
|
|
7
|
+
.description("Manage OpenCode sessions with named aliases")
|
|
8
|
+
.version("0.1.0");
|
|
9
|
+
program
|
|
10
|
+
.command("serve")
|
|
11
|
+
.description("Start the agent-office HTTP server")
|
|
12
|
+
.option("--database-url <url>", "PostgreSQL connection string", process.env.DATABASE_URL)
|
|
13
|
+
.option("--opencode-url <url>", "OpenCode server URL", process.env.OPENCODE_URL ?? "http://localhost:4096")
|
|
14
|
+
.option("--host <host>", "Host to bind to", "127.0.0.1")
|
|
15
|
+
.option("--port <port>", "Port to serve on", "7654")
|
|
16
|
+
.option("--password <password>", "REQUIRED. API password", process.env.AGENT_OFFICE_PASSWORD)
|
|
17
|
+
.action(async (options) => {
|
|
18
|
+
const { serve } = await import("./commands/serve.js");
|
|
19
|
+
await serve(options);
|
|
20
|
+
});
|
|
21
|
+
program
|
|
22
|
+
.command("manage")
|
|
23
|
+
.description("Launch the interactive TUI to manage sessions")
|
|
24
|
+
.argument("<url>", "URL of the agent-office server (e.g. http://localhost:7654)")
|
|
25
|
+
.option("--password <password>", "REQUIRED. API password", process.env.AGENT_OFFICE_PASSWORD)
|
|
26
|
+
.action(async (url, options) => {
|
|
27
|
+
const { manage } = await import("./commands/manage.js");
|
|
28
|
+
await manage(url, options);
|
|
29
|
+
});
|
|
30
|
+
const workerCmd = program
|
|
31
|
+
.command("worker")
|
|
32
|
+
.description("Worker agent commands");
|
|
33
|
+
workerCmd
|
|
34
|
+
.command("clock-in")
|
|
35
|
+
.description("Clock in as a worker agent")
|
|
36
|
+
.argument("<token>", "Agent token in the format <agent_code>@<server-url>")
|
|
37
|
+
.action(async (token) => {
|
|
38
|
+
const { clockIn } = await import("./commands/worker.js");
|
|
39
|
+
await clockIn(token);
|
|
40
|
+
});
|
|
41
|
+
program.parse();
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { render } from "ink";
|
|
3
|
+
import { App } from "../manage/app.js";
|
|
4
|
+
export async function manage(url, options) {
|
|
5
|
+
const password = options.password;
|
|
6
|
+
if (!password) {
|
|
7
|
+
console.error("Error: --password is required (or set AGENT_OFFICE_PASSWORD)");
|
|
8
|
+
process.exit(1);
|
|
9
|
+
}
|
|
10
|
+
// Validate the URL minimally
|
|
11
|
+
try {
|
|
12
|
+
new URL(url);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
console.error(`Error: invalid server URL "${url}"`);
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
const { waitUntilExit } = render(_jsx(App, { serverUrl: url, password: password }));
|
|
19
|
+
await waitUntilExit();
|
|
20
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { createDb } from "../db/index.js";
|
|
2
|
+
import { runMigrations } from "../db/migrate.js";
|
|
3
|
+
import { createOpencodeClient } from "../lib/opencode.js";
|
|
4
|
+
import { createApp } from "../server/index.js";
|
|
5
|
+
export async function serve(options) {
|
|
6
|
+
const password = options.password;
|
|
7
|
+
if (!password) {
|
|
8
|
+
console.error("Error: --password is required (or set AGENT_OFFICE_PASSWORD)");
|
|
9
|
+
process.exit(1);
|
|
10
|
+
}
|
|
11
|
+
const databaseUrl = options.databaseUrl;
|
|
12
|
+
if (!databaseUrl) {
|
|
13
|
+
console.error("Error: --database-url is required (or set DATABASE_URL)");
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
const port = parseInt(options.port, 10);
|
|
17
|
+
if (isNaN(port) || port < 1 || port > 65535) {
|
|
18
|
+
console.error(`Error: invalid port "${options.port}"`);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
// Connect to DB
|
|
22
|
+
console.log("Connecting to database...");
|
|
23
|
+
const sql = createDb(databaseUrl);
|
|
24
|
+
// Run migrations
|
|
25
|
+
console.log("Running migrations...");
|
|
26
|
+
try {
|
|
27
|
+
await runMigrations(sql);
|
|
28
|
+
console.log("Migrations complete.");
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
console.error("Migration failed:", err);
|
|
32
|
+
await sql.end();
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
// Init OpenCode client
|
|
36
|
+
const opencode = createOpencodeClient(options.opencodeUrl);
|
|
37
|
+
// Create Express app
|
|
38
|
+
const app = createApp(sql, opencode, password);
|
|
39
|
+
// Start server
|
|
40
|
+
const server = app.listen(port, options.host, () => {
|
|
41
|
+
console.log(`agent-office server listening on http://${options.host}:${port}`);
|
|
42
|
+
});
|
|
43
|
+
// Graceful shutdown
|
|
44
|
+
const shutdown = async () => {
|
|
45
|
+
console.log("\nShutting down...");
|
|
46
|
+
server.close(async () => {
|
|
47
|
+
await sql.end();
|
|
48
|
+
console.log("Goodbye.");
|
|
49
|
+
process.exit(0);
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
process.on("SIGINT", shutdown);
|
|
53
|
+
process.on("SIGTERM", shutdown);
|
|
54
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function clockIn(token: string): Promise<void>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// UUID v4 pattern
|
|
2
|
+
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)
|
|
5
|
+
const atIndex = token.indexOf("@");
|
|
6
|
+
if (atIndex === -1) {
|
|
7
|
+
console.error("Error: token must be in the format <agent_code>@<server-url>");
|
|
8
|
+
console.error("Example: 550e8400-e29b-41d4-a716-446655440000@http://localhost:7654");
|
|
9
|
+
process.exit(1);
|
|
10
|
+
}
|
|
11
|
+
const agentCode = token.slice(0, atIndex);
|
|
12
|
+
const serverUrl = token.slice(atIndex + 1);
|
|
13
|
+
if (!UUID_RE.test(agentCode)) {
|
|
14
|
+
console.error(`Error: "${agentCode}" is not a valid UUID agent code`);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
let parsedUrl;
|
|
18
|
+
try {
|
|
19
|
+
parsedUrl = new URL(serverUrl);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
console.error(`Error: "${serverUrl}" is not a valid URL`);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
const clockInUrl = `${parsedUrl.origin}/worker/clock-in?code=${encodeURIComponent(agentCode)}`;
|
|
26
|
+
let res;
|
|
27
|
+
try {
|
|
28
|
+
res = await fetch(clockInUrl);
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
console.error(`Error: could not reach ${parsedUrl.origin}`);
|
|
32
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
let body;
|
|
36
|
+
try {
|
|
37
|
+
body = await res.json();
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
console.error(`Error: invalid response from server`);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
if (!res.ok) {
|
|
44
|
+
const msg = body.error ?? `HTTP ${res.status}`;
|
|
45
|
+
console.error(`Error: ${msg}`);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
const { message } = body;
|
|
49
|
+
console.log(message);
|
|
50
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import postgres from "postgres";
|
|
2
|
+
export type Sql = ReturnType<typeof postgres>;
|
|
3
|
+
export declare function createDb(databaseUrl: string): Sql;
|
|
4
|
+
export interface SessionRow {
|
|
5
|
+
id: number;
|
|
6
|
+
name: string;
|
|
7
|
+
session_id: string;
|
|
8
|
+
agent_code: string;
|
|
9
|
+
created_at: Date;
|
|
10
|
+
}
|
package/dist/db/index.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
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
|
+
export async function runMigrations(sql) {
|
|
25
|
+
await sql `
|
|
26
|
+
CREATE TABLE IF NOT EXISTS _migrations (
|
|
27
|
+
version INTEGER PRIMARY KEY,
|
|
28
|
+
name VARCHAR(255) NOT NULL,
|
|
29
|
+
applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
30
|
+
)
|
|
31
|
+
`;
|
|
32
|
+
const applied = await sql `
|
|
33
|
+
SELECT version FROM _migrations ORDER BY version
|
|
34
|
+
`;
|
|
35
|
+
const appliedVersions = new Set(applied.map((r) => r.version));
|
|
36
|
+
for (const migration of MIGRATIONS) {
|
|
37
|
+
if (appliedVersions.has(migration.version))
|
|
38
|
+
continue;
|
|
39
|
+
console.log(` Applying migration ${migration.version}: ${migration.name}`);
|
|
40
|
+
await sql.begin(async (tx) => {
|
|
41
|
+
await tx.unsafe(migration.sql);
|
|
42
|
+
await tx.unsafe(`INSERT INTO _migrations (version, name) VALUES ($1, $2)`, [migration.version, migration.name]);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect } from "react";
|
|
3
|
+
import { Box, Text, useApp, useStdout, useInput } from "ink";
|
|
4
|
+
import { Select, Spinner } from "@inkjs/ui";
|
|
5
|
+
import { useApi } from "./hooks/useApi.js";
|
|
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";
|
|
12
|
+
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" },
|
|
19
|
+
{ label: "Quit", value: "quit" },
|
|
20
|
+
];
|
|
21
|
+
const SUB_SCREENS = ["list", "create", "delete", "tail", "inject", "agent-code"];
|
|
22
|
+
const FOOTER_HINTS = {
|
|
23
|
+
connecting: "",
|
|
24
|
+
"auth-error": "",
|
|
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",
|
|
32
|
+
};
|
|
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 })] }));
|
|
35
|
+
}
|
|
36
|
+
function Footer({ hint }) {
|
|
37
|
+
return (_jsx(Box, { borderStyle: "single", borderColor: "gray", paddingX: 1, children: _jsx(Text, { dimColor: true, children: hint }) }));
|
|
38
|
+
}
|
|
39
|
+
export function App({ serverUrl, password }) {
|
|
40
|
+
const { exit } = useApp();
|
|
41
|
+
const { stdout } = useStdout();
|
|
42
|
+
const { checkHealth } = useApi(serverUrl, password);
|
|
43
|
+
const [screen, setScreen] = useState("connecting");
|
|
44
|
+
const [termHeight, setTermHeight] = useState(stdout?.rows ?? 24);
|
|
45
|
+
const [termWidth, setTermWidth] = useState(stdout?.columns ?? 80);
|
|
46
|
+
// Track terminal size
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
const update = () => {
|
|
49
|
+
setTermHeight(stdout?.rows ?? 24);
|
|
50
|
+
setTermWidth(stdout?.columns ?? 80);
|
|
51
|
+
};
|
|
52
|
+
stdout?.on("resize", update);
|
|
53
|
+
return () => { stdout?.off("resize", update); };
|
|
54
|
+
}, [stdout]);
|
|
55
|
+
// Health check on mount
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
checkHealth().then((ok) => {
|
|
58
|
+
setScreen(ok ? "menu" : "auth-error");
|
|
59
|
+
});
|
|
60
|
+
}, []);
|
|
61
|
+
useInput((input, key) => {
|
|
62
|
+
if (key.escape && SUB_SCREENS.includes(screen)) {
|
|
63
|
+
setScreen("menu");
|
|
64
|
+
}
|
|
65
|
+
if (input === "q" && screen === "menu") {
|
|
66
|
+
exit();
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
const goBack = () => setScreen("menu");
|
|
70
|
+
const handleMenuSelect = (value) => {
|
|
71
|
+
if (value === "quit") {
|
|
72
|
+
exit();
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
setScreen(value);
|
|
76
|
+
};
|
|
77
|
+
// content area = terminal height minus header (3 rows) and footer (3 rows)
|
|
78
|
+
const contentHeight = Math.max(4, termHeight - 6);
|
|
79
|
+
const renderContent = () => {
|
|
80
|
+
switch (screen) {
|
|
81
|
+
case "connecting":
|
|
82
|
+
return (_jsx(Box, { height: contentHeight, alignItems: "center", justifyContent: "center", children: _jsx(Spinner, { label: `Connecting to ${serverUrl}...` }) }));
|
|
83
|
+
case "auth-error":
|
|
84
|
+
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
|
+
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 })] }));
|
|
87
|
+
case "list":
|
|
88
|
+
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 }) }));
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
return (_jsxs(Box, { flexDirection: "column", width: termWidth, children: [_jsx(Header, { serverUrl: serverUrl }), renderContent(), screen !== "connecting" && (_jsx(Footer, { hint: FOOTER_HINTS[screen] }))] }));
|
|
102
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
interface AgentCodeProps {
|
|
2
|
+
serverUrl: string;
|
|
3
|
+
password: string;
|
|
4
|
+
onBack: () => void;
|
|
5
|
+
contentHeight: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function AgentCode({ serverUrl, password, onBack, contentHeight }: AgentCodeProps): import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { Box, Text, useInput } from "ink";
|
|
4
|
+
import { Select, Spinner, ConfirmInput } from "@inkjs/ui";
|
|
5
|
+
import { useApi, useAsyncState } from "../hooks/useApi.js";
|
|
6
|
+
const MASKED = "••••••••-••••-••••-••••-••••••••••••";
|
|
7
|
+
export function AgentCode({ serverUrl, password, onBack, contentHeight }) {
|
|
8
|
+
const { listSessions, regenerateCode } = useApi(serverUrl, password);
|
|
9
|
+
const { run: runList } = useAsyncState();
|
|
10
|
+
const [sessions, setSessions] = useState([]);
|
|
11
|
+
const [stage, setStage] = useState("loading-sessions");
|
|
12
|
+
const [selected, setSelected] = useState(null);
|
|
13
|
+
const [revealed, setRevealed] = useState(false);
|
|
14
|
+
const [error, setError] = useState(null);
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
runList(listSessions).then((rows) => {
|
|
17
|
+
setSessions(rows ?? []);
|
|
18
|
+
setStage("select");
|
|
19
|
+
});
|
|
20
|
+
}, []);
|
|
21
|
+
// r = reveal/hide, g = trigger regenerate, when viewing
|
|
22
|
+
useInput((input) => {
|
|
23
|
+
if (stage !== "view")
|
|
24
|
+
return;
|
|
25
|
+
if (input === "r")
|
|
26
|
+
setRevealed((v) => !v);
|
|
27
|
+
if (input === "g")
|
|
28
|
+
setStage("confirm-regen");
|
|
29
|
+
});
|
|
30
|
+
const handleSelect = (name) => {
|
|
31
|
+
const session = sessions.find((s) => s.name === name) ?? null;
|
|
32
|
+
setSelected(session);
|
|
33
|
+
setRevealed(false);
|
|
34
|
+
setStage("view");
|
|
35
|
+
};
|
|
36
|
+
const handleConfirmRegen = async (confirmed) => {
|
|
37
|
+
if (!confirmed || !selected) {
|
|
38
|
+
setStage("view");
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
setStage("regenerating");
|
|
42
|
+
try {
|
|
43
|
+
const updated = await regenerateCode(selected.name);
|
|
44
|
+
setSelected(updated);
|
|
45
|
+
setRevealed(true); // auto-reveal the new code
|
|
46
|
+
setStage("view");
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
50
|
+
setStage("error");
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
if (stage === "loading-sessions") {
|
|
54
|
+
return (_jsx(Box, { height: contentHeight, alignItems: "center", justifyContent: "center", children: _jsx(Spinner, { label: "Loading sessions..." }) }));
|
|
55
|
+
}
|
|
56
|
+
if (stage === "regenerating") {
|
|
57
|
+
return (_jsx(Box, { height: contentHeight, alignItems: "center", justifyContent: "center", children: _jsx(Spinner, { label: "Generating new agent code..." }) }));
|
|
58
|
+
}
|
|
59
|
+
if (stage === "error") {
|
|
60
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Text, { bold: true, children: "Agent Code" }), _jsxs(Text, { color: "red", children: ["Error: ", error] }), _jsx(Text, { dimColor: true, children: "Press Esc to go back" })] }));
|
|
61
|
+
}
|
|
62
|
+
if (stage === "select") {
|
|
63
|
+
if (sessions.length === 0) {
|
|
64
|
+
return (_jsx(Box, { height: contentHeight, alignItems: "center", justifyContent: "center", children: _jsx(Text, { dimColor: true, children: "No sessions yet. Create one first." }) }));
|
|
65
|
+
}
|
|
66
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Text, { bold: true, children: "Agent Code" }), _jsx(Text, { dimColor: true, children: "Select a session to view its agent code:" }), _jsx(Select, { options: sessions.map((s) => ({ label: s.name, value: s.name })), onChange: handleSelect })] }));
|
|
67
|
+
}
|
|
68
|
+
if (stage === "confirm-regen" && selected) {
|
|
69
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Text, { bold: true, children: "Agent Code" }), _jsxs(Text, { children: ["Regenerate code for ", _jsx(Text, { color: "cyan", bold: true, children: selected.name }), "?", " ", _jsx(Text, { dimColor: true, children: "The old code will stop working immediately." })] }), _jsx(ConfirmInput, { defaultChoice: "cancel", onConfirm: () => void handleConfirmRegen(true), onCancel: () => void handleConfirmRegen(false) })] }));
|
|
70
|
+
}
|
|
71
|
+
// stage === "view"
|
|
72
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Box, { gap: 2, children: [_jsx(Text, { bold: true, children: "Agent Code" }), _jsx(Text, { color: "cyan", children: selected?.name })] }), _jsxs(Box, { gap: 2, marginY: 1, children: [_jsx(Text, { bold: true, children: "Code:" }), revealed ? (_jsx(Text, { color: "yellow", children: selected?.agent_code })) : (_jsx(Text, { dimColor: true, children: MASKED }))] }), _jsxs(Box, { gap: 2, children: [_jsx(Text, { dimColor: true, children: "Clock-in format:" }), _jsxs(Text, { dimColor: true, children: [revealed ? selected?.agent_code : MASKED, "@<server-url>"] })] }), _jsxs(Box, { flexDirection: "column", marginTop: 1, gap: 0, children: [_jsxs(Text, { dimColor: true, children: ["r ", revealed ? "hide" : "reveal", " code"] }), _jsx(Text, { dimColor: true, children: "g regenerate code" }), _jsx(Text, { dimColor: true, children: "Esc back to menu" })] })] }));
|
|
73
|
+
}
|