agent-message 0.1.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/README.md +227 -0
- package/npm/bin/agent-messenger.mjs +386 -0
- package/npm/runtime/agent_gateway.mjs +175 -0
- package/npm/runtime/bin/agent-messenger-cli-darwin-amd64 +0 -0
- package/npm/runtime/bin/agent-messenger-cli-darwin-arm64 +0 -0
- package/npm/runtime/bin/agent-messenger-server-darwin-amd64 +0 -0
- package/npm/runtime/bin/agent-messenger-server-darwin-arm64 +0 -0
- package/npm/runtime/web-dist/apple-touch-icon.png +0 -0
- package/npm/runtime/web-dist/assets/index-BgmjffS2.js +182 -0
- package/npm/runtime/web-dist/assets/index-D_RPU5JN.css +1 -0
- package/npm/runtime/web-dist/assets/workbox-window.prod.es5-vqzQaGvo.js +2 -0
- package/npm/runtime/web-dist/favicon.svg +9 -0
- package/npm/runtime/web-dist/index.html +21 -0
- package/npm/runtime/web-dist/manifest.webmanifest +1 -0
- package/npm/runtime/web-dist/masked-icon.svg +9 -0
- package/npm/runtime/web-dist/pwa-192x192.png +0 -0
- package/npm/runtime/web-dist/pwa-512x512.png +0 -0
- package/npm/runtime/web-dist/pwa-artwork.svg +9 -0
- package/npm/runtime/web-dist/sw.js +1 -0
- package/npm/runtime/web-dist/workbox-8c29f6e4.js +1 -0
- package/package.json +33 -0
package/README.md
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# Agent Messenger
|
|
2
|
+
|
|
3
|
+
Agent Messenger is a direct-message stack with three clients:
|
|
4
|
+
- HTTP/SSE server (`server/`)
|
|
5
|
+
- Web app (`web/`)
|
|
6
|
+
- CLI (`cli/`)
|
|
7
|
+
|
|
8
|
+
This README covers a Phase 7 quickstart for local development and local production-like testing.
|
|
9
|
+
|
|
10
|
+
## Prerequisites
|
|
11
|
+
|
|
12
|
+
- Go `1.26+`
|
|
13
|
+
- Node.js `18+` and npm
|
|
14
|
+
- Docker + Docker Compose (for PostgreSQL compose flow)
|
|
15
|
+
|
|
16
|
+
## Server Quickstart
|
|
17
|
+
|
|
18
|
+
### Option A: Local server with SQLite (default)
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
cd server
|
|
22
|
+
go run .
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Default server settings:
|
|
26
|
+
- `SERVER_ADDR=:8080`
|
|
27
|
+
- `DB_DRIVER=sqlite`
|
|
28
|
+
- `SQLITE_DSN=./agent_messenger.sqlite`
|
|
29
|
+
- `UPLOAD_DIR=./uploads`
|
|
30
|
+
- `CORS_ALLOWED_ORIGINS=*`
|
|
31
|
+
|
|
32
|
+
Example override:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
cd server
|
|
36
|
+
DB_DRIVER=sqlite SQLITE_DSN=./dev.sqlite UPLOAD_DIR=./uploads go run .
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Option B: Local production-like stack (Server + PostgreSQL)
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
docker compose up --build
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
This starts:
|
|
46
|
+
- `postgres` on `localhost:5432`
|
|
47
|
+
- `server` on `localhost:8080` with:
|
|
48
|
+
- `DB_DRIVER=postgres`
|
|
49
|
+
- `POSTGRES_DSN=postgres://agent:agent@postgres:5432/agent_messenger?sslmode=disable`
|
|
50
|
+
|
|
51
|
+
To stop and remove containers:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
docker compose down
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
To also remove persisted DB/uploads volumes:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
docker compose down -v
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Web Quickstart
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
cd web
|
|
67
|
+
npm ci
|
|
68
|
+
npm run dev
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
In local dev, Vite proxies `/api/...` and `/static/uploads/...` to `http://localhost:8080`, so you usually do not need `VITE_API_BASE_URL`.
|
|
72
|
+
If your API is on a different origin, set `VITE_API_BASE_URL`:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
cd web
|
|
76
|
+
VITE_API_BASE_URL=http://localhost:8080 npm run dev
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
When `VITE_API_BASE_URL` is set, requests become cross-origin and the server must allow that origin via `CORS_ALLOWED_ORIGINS`.
|
|
80
|
+
|
|
81
|
+
Build check:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
cd web
|
|
85
|
+
npm run build
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Local Bundle Commands
|
|
89
|
+
|
|
90
|
+
From the project root, you can start the SQLite-backed API server and the production-like local web gateway together:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
./dev-up
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
This will:
|
|
97
|
+
- build `web/dist`
|
|
98
|
+
- build the Go server binary into `~/.agent-messenger/bin`
|
|
99
|
+
- start the API on `127.0.0.1:18080`
|
|
100
|
+
- start the local web gateway on `127.0.0.1:8788`
|
|
101
|
+
|
|
102
|
+
To stop both processes:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
./dev-stop
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
If you also want to start or stop the named tunnel that serves `https://agent.namjaeyoun.com`, use:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
./dev-up --with-tunnel
|
|
112
|
+
./dev-stop --with-tunnel
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## npm Distribution (macOS)
|
|
116
|
+
|
|
117
|
+
The repo now includes a publishable npm package surface for macOS (`arm64` and `x64`). The npm command keeps the existing CLI behavior and also adds local stack lifecycle commands:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
agent-messenger start
|
|
121
|
+
agent-messenger status
|
|
122
|
+
agent-messenger stop
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Default npm launcher ports:
|
|
126
|
+
- API: `127.0.0.1:8080`
|
|
127
|
+
- Web: `127.0.0.1:8788`
|
|
128
|
+
|
|
129
|
+
You can override runtime location and ports when needed:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
agent-messenger start --runtime-dir /tmp/agent-messenger --api-port 28080 --web-port 28788
|
|
133
|
+
agent-messenger status --runtime-dir /tmp/agent-messenger --api-port 28080 --web-port 28788
|
|
134
|
+
agent-messenger stop --runtime-dir /tmp/agent-messenger
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
When publishing, `npm pack` / `npm publish` will run the package `prepack` hook, which:
|
|
138
|
+
- builds `web/dist`
|
|
139
|
+
- bundles `deploy/agent_gateway.mjs`
|
|
140
|
+
- cross-compiles macOS `arm64` and `x64` binaries for the Go CLI and API server into `npm/runtime/`
|
|
141
|
+
|
|
142
|
+
You can run the same packaging step manually from the repo root:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
npm run prepare:npm-bundle
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
PWA install:
|
|
149
|
+
|
|
150
|
+
- Open the deployed web app in Safari on iPhone.
|
|
151
|
+
- Use `Share -> Add to Home Screen`.
|
|
152
|
+
- The app now ships with a web app manifest, service worker, and Apple touch icon so it can be installed like a standalone app.
|
|
153
|
+
|
|
154
|
+
## Claude Code Skill
|
|
155
|
+
|
|
156
|
+
Install the agent-messenger CLI skill to give Claude Code full knowledge of this project's CLI commands, flags, and json_render component catalog:
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
npx skills add https://github.com/siisee11/agent-messenger --skill agent-messenger-cli
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## CLI Quickstart
|
|
163
|
+
|
|
164
|
+
Run from `cli/` with optional `--server-url` override.
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
cd cli
|
|
168
|
+
go run . --server-url http://localhost:8080 register alice 1234
|
|
169
|
+
go run . --server-url http://localhost:8080 login alice 1234
|
|
170
|
+
go run . profile list
|
|
171
|
+
go run . profile switch alice
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Common commands:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
# Conversations
|
|
178
|
+
go run . --server-url http://localhost:8080 ls
|
|
179
|
+
go run . --server-url http://localhost:8080 open bob
|
|
180
|
+
|
|
181
|
+
# Messaging
|
|
182
|
+
go run . --server-url http://localhost:8080 send bob "hello"
|
|
183
|
+
go run . --server-url http://localhost:8080 read bob --n 20
|
|
184
|
+
go run . --server-url http://localhost:8080 edit 1 "edited text"
|
|
185
|
+
go run . --server-url http://localhost:8080 delete 1
|
|
186
|
+
|
|
187
|
+
# Reactions
|
|
188
|
+
go run . --server-url http://localhost:8080 react 1 👍
|
|
189
|
+
go run . --server-url http://localhost:8080 unreact 1 👍
|
|
190
|
+
|
|
191
|
+
# Realtime watch
|
|
192
|
+
go run . --server-url http://localhost:8080 watch bob
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
CLI config is stored at `~/.agent-messenger/config` by default.
|
|
196
|
+
Each successful `login` or `register` also saves a named profile, and `go run . profile switch <username>` swaps the active account locally.
|
|
197
|
+
|
|
198
|
+
## Validation and Constraints (Phase 7)
|
|
199
|
+
|
|
200
|
+
- Username identity fields: `3-32` chars, allowed `[A-Za-z0-9._-]`
|
|
201
|
+
- PIN: `4-6` numeric digits
|
|
202
|
+
- Uploads:
|
|
203
|
+
- max file size: `20 MB`
|
|
204
|
+
- unsupported file types are rejected
|
|
205
|
+
|
|
206
|
+
## Dev Checks
|
|
207
|
+
|
|
208
|
+
Server:
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
cd server
|
|
212
|
+
go test ./...
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
CLI:
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
cd cli
|
|
219
|
+
go test ./...
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Web:
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
cd web
|
|
226
|
+
npm run build
|
|
227
|
+
```
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawn, spawnSync } from 'node:child_process'
|
|
4
|
+
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
5
|
+
import { access, constants } from 'node:fs/promises'
|
|
6
|
+
import os from 'node:os'
|
|
7
|
+
import { dirname, join, resolve } from 'node:path'
|
|
8
|
+
import process from 'node:process'
|
|
9
|
+
import { fileURLToPath } from 'node:url'
|
|
10
|
+
|
|
11
|
+
const DEFAULT_API_HOST = '127.0.0.1'
|
|
12
|
+
const DEFAULT_API_PORT = 8080
|
|
13
|
+
const DEFAULT_WEB_HOST = '127.0.0.1'
|
|
14
|
+
const DEFAULT_WEB_PORT = 8788
|
|
15
|
+
const STARTUP_ATTEMPTS = 40
|
|
16
|
+
const STARTUP_DELAY_MS = 500
|
|
17
|
+
const PROCESS_STOP_DELAY_MS = 1000
|
|
18
|
+
|
|
19
|
+
const scriptDir = dirname(fileURLToPath(import.meta.url))
|
|
20
|
+
const packageRoot = resolve(scriptDir, '..', '..')
|
|
21
|
+
const bundleRoot = resolve(packageRoot, 'npm', 'runtime')
|
|
22
|
+
const bundleBinDir = join(bundleRoot, 'bin')
|
|
23
|
+
const bundleGatewayPath = join(bundleRoot, 'agent_gateway.mjs')
|
|
24
|
+
const bundleWebDistDir = join(bundleRoot, 'web-dist')
|
|
25
|
+
|
|
26
|
+
const lifecycleCommands = new Set(['start', 'stop', 'status'])
|
|
27
|
+
|
|
28
|
+
async function main() {
|
|
29
|
+
const [command, ...rest] = process.argv.slice(2)
|
|
30
|
+
if (!command) {
|
|
31
|
+
printRootUsage()
|
|
32
|
+
process.exit(1)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (command === '--help' || command === '-h' || command === 'help') {
|
|
36
|
+
printRootUsage()
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (lifecycleCommands.has(command)) {
|
|
41
|
+
const options = parseLifecycleOptions(rest)
|
|
42
|
+
await ensureBundleReady()
|
|
43
|
+
|
|
44
|
+
if (command === 'start') {
|
|
45
|
+
await startStack(options)
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
if (command === 'stop') {
|
|
49
|
+
await stopStack(options, { quiet: false })
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
await printStatus(options)
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
await ensureBundleReady()
|
|
57
|
+
delegateToBundledCli([command, ...rest])
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function printRootUsage() {
|
|
61
|
+
console.error(`Usage:
|
|
62
|
+
agent-messenger start [--runtime-dir <dir>] [--api-host <host>] [--api-port <port>] [--web-host <host>] [--web-port <port>]
|
|
63
|
+
agent-messenger stop [--runtime-dir <dir>]
|
|
64
|
+
agent-messenger status [--runtime-dir <dir>] [--api-host <host>] [--api-port <port>] [--web-host <host>] [--web-port <port>]
|
|
65
|
+
agent-messenger <existing-cli-command> [...args]`)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function parseLifecycleOptions(args) {
|
|
69
|
+
const options = {
|
|
70
|
+
runtimeDir: join(os.homedir(), '.agent-messenger'),
|
|
71
|
+
apiHost: DEFAULT_API_HOST,
|
|
72
|
+
apiPort: DEFAULT_API_PORT,
|
|
73
|
+
webHost: DEFAULT_WEB_HOST,
|
|
74
|
+
webPort: DEFAULT_WEB_PORT,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
78
|
+
const arg = args[index]
|
|
79
|
+
if (arg === '--help' || arg === '-h') {
|
|
80
|
+
printRootUsage()
|
|
81
|
+
process.exit(0)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (arg === '--runtime-dir') {
|
|
85
|
+
options.runtimeDir = requireOptionValue(args, ++index, arg)
|
|
86
|
+
continue
|
|
87
|
+
}
|
|
88
|
+
if (arg === '--api-host') {
|
|
89
|
+
options.apiHost = requireOptionValue(args, ++index, arg)
|
|
90
|
+
continue
|
|
91
|
+
}
|
|
92
|
+
if (arg === '--web-host') {
|
|
93
|
+
options.webHost = requireOptionValue(args, ++index, arg)
|
|
94
|
+
continue
|
|
95
|
+
}
|
|
96
|
+
if (arg === '--api-port') {
|
|
97
|
+
options.apiPort = parsePort(requireOptionValue(args, ++index, arg), arg)
|
|
98
|
+
continue
|
|
99
|
+
}
|
|
100
|
+
if (arg === '--web-port') {
|
|
101
|
+
options.webPort = parsePort(requireOptionValue(args, ++index, arg), arg)
|
|
102
|
+
continue
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
throw new Error(`unknown option: ${arg}`)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return options
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function requireOptionValue(args, index, flag) {
|
|
112
|
+
const value = args[index]
|
|
113
|
+
if (!value || value.startsWith('-')) {
|
|
114
|
+
throw new Error(`missing value for ${flag}`)
|
|
115
|
+
}
|
|
116
|
+
return value
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function parsePort(value, flag) {
|
|
120
|
+
const parsed = Number(value)
|
|
121
|
+
if (!Number.isInteger(parsed) || parsed <= 0 || parsed > 65535) {
|
|
122
|
+
throw new Error(`invalid port for ${flag}: ${value}`)
|
|
123
|
+
}
|
|
124
|
+
return parsed
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function ensureBundleReady() {
|
|
128
|
+
const requiredPaths = [
|
|
129
|
+
bundleGatewayPath,
|
|
130
|
+
bundleWebDistDir,
|
|
131
|
+
resolveBinaryPath('agent-messenger-server'),
|
|
132
|
+
resolveBinaryPath('agent-messenger-cli'),
|
|
133
|
+
]
|
|
134
|
+
|
|
135
|
+
for (const target of requiredPaths) {
|
|
136
|
+
if (!existsSync(target)) {
|
|
137
|
+
throw new Error(
|
|
138
|
+
`npm bundle is incomplete: missing ${target}. Run "npm run prepare:npm-bundle" before packing or publishing.`,
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
await access(resolveBinaryPath('agent-messenger-server'), constants.X_OK)
|
|
144
|
+
await access(resolveBinaryPath('agent-messenger-cli'), constants.X_OK)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function resolveBinaryPath(baseName) {
|
|
148
|
+
if (process.platform !== 'darwin') {
|
|
149
|
+
throw new Error(`unsupported platform: ${process.platform}. This package currently supports macOS only.`)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
let archSuffix
|
|
153
|
+
if (process.arch === 'arm64') {
|
|
154
|
+
archSuffix = 'darwin-arm64'
|
|
155
|
+
} else if (process.arch === 'x64') {
|
|
156
|
+
archSuffix = 'darwin-amd64'
|
|
157
|
+
} else {
|
|
158
|
+
throw new Error(`unsupported architecture: ${process.arch}. Expected arm64 or x64 on macOS.`)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return join(bundleBinDir, `${baseName}-${archSuffix}`)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function runtimePaths(runtimeDir) {
|
|
165
|
+
return {
|
|
166
|
+
runtimeDir,
|
|
167
|
+
logDir: join(runtimeDir, 'logs'),
|
|
168
|
+
uploadDir: join(runtimeDir, 'uploads'),
|
|
169
|
+
serverLog: join(runtimeDir, 'logs', 'server.log'),
|
|
170
|
+
gatewayLog: join(runtimeDir, 'logs', 'gateway.log'),
|
|
171
|
+
serverPidfile: join(runtimeDir, 'server.pid'),
|
|
172
|
+
gatewayPidfile: join(runtimeDir, 'gateway.pid'),
|
|
173
|
+
sqlitePath: join(runtimeDir, 'agent_messenger.sqlite'),
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function startStack(options) {
|
|
178
|
+
const paths = runtimePaths(options.runtimeDir)
|
|
179
|
+
mkdirSync(paths.runtimeDir, { recursive: true })
|
|
180
|
+
mkdirSync(paths.logDir, { recursive: true })
|
|
181
|
+
mkdirSync(paths.uploadDir, { recursive: true })
|
|
182
|
+
|
|
183
|
+
await stopStack(options, { quiet: true })
|
|
184
|
+
|
|
185
|
+
writeFileSync(paths.serverLog, '')
|
|
186
|
+
writeFileSync(paths.gatewayLog, '')
|
|
187
|
+
|
|
188
|
+
const serverBinary = resolveBinaryPath('agent-messenger-server')
|
|
189
|
+
const gatewayChild = { pid: null }
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
const serverChild = spawnDetached(serverBinary, [], {
|
|
193
|
+
...process.env,
|
|
194
|
+
SERVER_ADDR: `${options.apiHost}:${options.apiPort}`,
|
|
195
|
+
DB_DRIVER: 'sqlite',
|
|
196
|
+
SQLITE_DSN: paths.sqlitePath,
|
|
197
|
+
UPLOAD_DIR: paths.uploadDir,
|
|
198
|
+
CORS_ALLOWED_ORIGINS: '*',
|
|
199
|
+
}, paths.serverLog)
|
|
200
|
+
writeFileSync(paths.serverPidfile, `${serverChild.pid}\n`)
|
|
201
|
+
|
|
202
|
+
await waitForHttp(`http://${options.apiHost}:${options.apiPort}/healthz`, 'API server')
|
|
203
|
+
|
|
204
|
+
gatewayChild.pid = spawnDetached(
|
|
205
|
+
process.execPath,
|
|
206
|
+
[bundleGatewayPath],
|
|
207
|
+
{
|
|
208
|
+
...process.env,
|
|
209
|
+
AGENT_GATEWAY_HOST: options.webHost,
|
|
210
|
+
AGENT_GATEWAY_PORT: String(options.webPort),
|
|
211
|
+
AGENT_API_ORIGIN: `http://${options.apiHost}:${options.apiPort}`,
|
|
212
|
+
AGENT_WEB_DIST: bundleWebDistDir,
|
|
213
|
+
},
|
|
214
|
+
paths.gatewayLog,
|
|
215
|
+
).pid
|
|
216
|
+
writeFileSync(paths.gatewayPidfile, `${gatewayChild.pid}\n`)
|
|
217
|
+
|
|
218
|
+
await waitForHttp(`http://${options.webHost}:${options.webPort}`, 'web gateway')
|
|
219
|
+
} catch (error) {
|
|
220
|
+
await stopStack(options, { quiet: true })
|
|
221
|
+
throw error
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
console.log('Agent Messenger is up.')
|
|
225
|
+
console.log(`API: http://${options.apiHost}:${options.apiPort}`)
|
|
226
|
+
console.log(`Web: http://${options.webHost}:${options.webPort}`)
|
|
227
|
+
console.log(`Logs: ${paths.serverLog} ${paths.gatewayLog}`)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function stopStack(options, { quiet }) {
|
|
231
|
+
const paths = runtimePaths(options.runtimeDir)
|
|
232
|
+
const stoppedServer = await killFromPidfile(paths.serverPidfile)
|
|
233
|
+
const stoppedGateway = await killFromPidfile(paths.gatewayPidfile)
|
|
234
|
+
|
|
235
|
+
if (!quiet) {
|
|
236
|
+
if (stoppedServer || stoppedGateway) {
|
|
237
|
+
console.log('Agent Messenger is stopped.')
|
|
238
|
+
} else {
|
|
239
|
+
console.log('Agent Messenger is not running.')
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function printStatus(options) {
|
|
245
|
+
const paths = runtimePaths(options.runtimeDir)
|
|
246
|
+
const serverPid = readPidfile(paths.serverPidfile)
|
|
247
|
+
const gatewayPid = readPidfile(paths.gatewayPidfile)
|
|
248
|
+
const serverRunning = serverPid !== null && isPidAlive(serverPid)
|
|
249
|
+
const gatewayRunning = gatewayPid !== null && isPidAlive(gatewayPid)
|
|
250
|
+
const apiHealthy = serverRunning ? await isHttpReady(`http://${options.apiHost}:${options.apiPort}/healthz`) : false
|
|
251
|
+
const webHealthy = gatewayRunning ? await isHttpReady(`http://${options.webHost}:${options.webPort}`) : false
|
|
252
|
+
|
|
253
|
+
console.log(`API server: ${serverRunning ? 'running' : 'stopped'}${apiHealthy ? ' (healthy)' : ''}`)
|
|
254
|
+
console.log(`Web gateway: ${gatewayRunning ? 'running' : 'stopped'}${webHealthy ? ' (healthy)' : ''}`)
|
|
255
|
+
console.log(`Runtime dir: ${paths.runtimeDir}`)
|
|
256
|
+
console.log(`API URL: http://${options.apiHost}:${options.apiPort}`)
|
|
257
|
+
console.log(`Web URL: http://${options.webHost}:${options.webPort}`)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function delegateToBundledCli(args) {
|
|
261
|
+
const cliBinary = resolveBinaryPath('agent-messenger-cli')
|
|
262
|
+
const result = spawnSync(cliBinary, args, {
|
|
263
|
+
stdio: 'inherit',
|
|
264
|
+
env: process.env,
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
if (result.error) {
|
|
268
|
+
throw result.error
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (result.signal) {
|
|
272
|
+
process.kill(process.pid, result.signal)
|
|
273
|
+
return
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
process.exit(result.status ?? 1)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function spawnDetached(command, args, env, logFile) {
|
|
280
|
+
const stdoutFd = openSync(logFile, 'a')
|
|
281
|
+
const stderrFd = openSync(logFile, 'a')
|
|
282
|
+
|
|
283
|
+
try {
|
|
284
|
+
const child = spawn(command, args, {
|
|
285
|
+
detached: true,
|
|
286
|
+
stdio: ['ignore', stdoutFd, stderrFd],
|
|
287
|
+
env,
|
|
288
|
+
})
|
|
289
|
+
child.unref()
|
|
290
|
+
|
|
291
|
+
if (!Number.isInteger(child.pid) || child.pid <= 0) {
|
|
292
|
+
throw new Error(`failed to launch background process: ${command}`)
|
|
293
|
+
}
|
|
294
|
+
return { pid: child.pid }
|
|
295
|
+
} finally {
|
|
296
|
+
closeSync(stdoutFd)
|
|
297
|
+
closeSync(stderrFd)
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function readPidfile(pidfile) {
|
|
302
|
+
if (!existsSync(pidfile)) {
|
|
303
|
+
return null
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const raw = readFileSync(pidfile, 'utf8').trim()
|
|
307
|
+
const pid = Number(raw)
|
|
308
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
309
|
+
rmSync(pidfile, { force: true })
|
|
310
|
+
return null
|
|
311
|
+
}
|
|
312
|
+
return pid
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function isPidAlive(pid) {
|
|
316
|
+
try {
|
|
317
|
+
process.kill(pid, 0)
|
|
318
|
+
return true
|
|
319
|
+
} catch (error) {
|
|
320
|
+
if (error && typeof error === 'object' && 'code' in error && error.code === 'ESRCH') {
|
|
321
|
+
return false
|
|
322
|
+
}
|
|
323
|
+
return true
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async function killFromPidfile(pidfile) {
|
|
328
|
+
const pid = readPidfile(pidfile)
|
|
329
|
+
if (pid === null) {
|
|
330
|
+
return false
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (isPidAlive(pid)) {
|
|
334
|
+
safeKill(pid, 'SIGTERM')
|
|
335
|
+
await sleep(PROCESS_STOP_DELAY_MS)
|
|
336
|
+
if (isPidAlive(pid)) {
|
|
337
|
+
safeKill(pid, 'SIGKILL')
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
rmSync(pidfile, { force: true })
|
|
342
|
+
return true
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async function waitForHttp(url, label) {
|
|
346
|
+
for (let attempt = 0; attempt < STARTUP_ATTEMPTS; attempt += 1) {
|
|
347
|
+
if (await isHttpReady(url)) {
|
|
348
|
+
return
|
|
349
|
+
}
|
|
350
|
+
await sleep(STARTUP_DELAY_MS)
|
|
351
|
+
}
|
|
352
|
+
throw new Error(`${label} did not become ready: ${url}`)
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async function isHttpReady(url) {
|
|
356
|
+
try {
|
|
357
|
+
const controller = new AbortController()
|
|
358
|
+
const timeout = setTimeout(() => controller.abort(), 1000)
|
|
359
|
+
const response = await fetch(url, { signal: controller.signal })
|
|
360
|
+
clearTimeout(timeout)
|
|
361
|
+
return response.ok
|
|
362
|
+
} catch {
|
|
363
|
+
return false
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function sleep(ms) {
|
|
368
|
+
return new Promise((resolvePromise) => {
|
|
369
|
+
setTimeout(resolvePromise, ms)
|
|
370
|
+
})
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function safeKill(pid, signal) {
|
|
374
|
+
try {
|
|
375
|
+
process.kill(pid, signal)
|
|
376
|
+
} catch (error) {
|
|
377
|
+
if (!(error && typeof error === 'object' && 'code' in error && error.code === 'ESRCH')) {
|
|
378
|
+
throw error
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
main().catch((error) => {
|
|
384
|
+
console.error(error instanceof Error ? error.message : String(error))
|
|
385
|
+
process.exit(1)
|
|
386
|
+
})
|