agentp 0.4.0 → 0.5.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/AGENTS.md +100 -8
- package/CHANGELOG.md +2 -0
- package/README.md +67 -2
- package/bin/agentp +12 -150
- package/bin/ocmux +14 -81
- package/bin/tgagentp +899 -0
- package/lib/ocmux.js +156 -0
- package/lib/opencode.js +270 -0
- package/package.json +3 -2
package/AGENTS.md
CHANGED
|
@@ -1,23 +1,56 @@
|
|
|
1
1
|
# agentp — agent notes
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## Three binaries, all registered
|
|
4
4
|
|
|
5
5
|
- `bin/agentp` (Node.js) — registered in `package.json` `"bin"` — pipes stdin → opencode TUI
|
|
6
6
|
- `bin/ocmux` (Node.js) — registered in `package.json` `"bin"` — manages opencode tmux sessions
|
|
7
|
+
- `bin/tgagentp` (Node.js) — registered in `package.json` `"bin"` — bridges Telegram bot ↔ opencode TUI
|
|
7
8
|
|
|
8
9
|
## Package facts
|
|
9
10
|
|
|
10
|
-
- **Zero npm dependencies.**
|
|
11
|
+
- **Zero npm dependencies.** All scripts use only Node.js stdlib (`http`, `https`, `readline`, `url`, `child_process`, `fs`, `path`, `crypto`, `os`).
|
|
11
12
|
- **No tests, no CI, no lint, no formatter.** `npm test` is a stub that exits 1.
|
|
12
13
|
- **No lockfile.** Versions resolve at install time.
|
|
13
14
|
- **`"type": "commonjs"`**, requires Node >= 18.
|
|
14
|
-
- **Linux and macOS compatible.**
|
|
15
|
+
- **Linux and macOS compatible.** All tools use portable Node.js APIs only.
|
|
15
16
|
|
|
16
17
|
## `.ocmux.json`
|
|
17
18
|
|
|
18
19
|
Runtime state file created by `bin/ocmux` (contains URL, log path, and window index). **Not in `.gitignore`** — avoid committing it.
|
|
19
20
|
|
|
20
|
-
##
|
|
21
|
+
## Shared `lib/opencode.js`
|
|
22
|
+
|
|
23
|
+
Extracted from `bin/agentp`; used by both `agentp` and `tgagentp`:
|
|
24
|
+
|
|
25
|
+
- `getAuthHeaders()` — reads `OPENCODE_SERVER_PASSWORD` / `OPENCODE_SERVER_USERNAME`
|
|
26
|
+
- `makeRequest(options, data)` — thin `http.request` wrapper returning a Promise
|
|
27
|
+
- `buildJsonRequest(url, method, body)` — builds request options for JSON POST/GET (no Content-Type/Length for bodyless requests)
|
|
28
|
+
- `clearPrompt(server)`, `appendPrompt(server, text)`, `submitPrompt(server)` — TUI prompt endpoints
|
|
29
|
+
- `sendText(server, text)` — convenience: clear + append + submit in one call
|
|
30
|
+
- `listenForFinalAnswer(server, onText?, cancelRef?)` — SSE event stream listener; `cancelRef` allows true server-side abort via `req.destroy()`
|
|
31
|
+
- `listSessions(server, directory?)` — `GET /session`; returns parsed JSON array; optional `?directory=` filter
|
|
32
|
+
- `createSession(server, title?)` — `POST /session`; returns the created `Session` object; optional `title`
|
|
33
|
+
- `updateSession(server, sessionId, title)` — `PATCH /session/:id`; returns the updated `Session` object
|
|
34
|
+
- `sendToSession(server, sessionId, text, agent?)` — `POST /session/:id/message`; returns concatenated text parts from response; optional `agent` to handle the message
|
|
35
|
+
- `selectSession(server, sessionId)` — `POST /session/:id/select`; tells the TUI to navigate to the given session (silently ignores 404 if the endpoint isn't available in older opencode versions)
|
|
36
|
+
- `listAgents(server)` — `GET /agent`; returns parsed JSON array of agent objects
|
|
37
|
+
- `listProviders(server)` — `GET /provider`; returns parsed JSON array of provider objects
|
|
38
|
+
|
|
39
|
+
## Shared `lib/ocmux.js`
|
|
40
|
+
|
|
41
|
+
Shared ocmux session discovery; used by both `bin/ocmux` and `bin/tgagentp`:
|
|
42
|
+
|
|
43
|
+
- `readState(file)` — parses `.ocmux.json`, returns `null` on error
|
|
44
|
+
- `statefileFor(dir)` — `path.join(dir, '.ocmux.json')`
|
|
45
|
+
- `tuiPaneId(windowIndex)` — returns pane ID if TUI process (`node`) is running, `null` if dead
|
|
46
|
+
- `windowByDir(dir)` — returns window index by name match
|
|
47
|
+
- `windowNameByIndex(idx)` — reverse: name from index
|
|
48
|
+
- `activeWindowIndex()` — returns index of currently selected tmux window
|
|
49
|
+
- `paneCount(windowIndex)` — number of panes in a window
|
|
50
|
+
- `listServers()` — scans all tmux windows for `.ocmux.json` state files; returns `{ url, dir, index, status }` array
|
|
51
|
+
- `activateServer(dir, index, url)` — pin window name, restart dead TUI pane, select window, zoom pane
|
|
52
|
+
|
|
53
|
+
## OpenCode HTTP protocol
|
|
21
54
|
|
|
22
55
|
These endpoints are consumed but not documented elsewhere in the repo:
|
|
23
56
|
|
|
@@ -25,24 +58,83 @@ These endpoints are consumed but not documented elsewhere in the repo:
|
|
|
25
58
|
- `POST /tui/append-prompt`
|
|
26
59
|
- `POST /tui/submit-prompt`
|
|
27
60
|
- `GET /event` (SSE stream)
|
|
61
|
+
- `GET /session` — list sessions (optional `?directory=` filter)
|
|
62
|
+
- `POST /session` — create a new session (optional `{ title? }` body)
|
|
63
|
+
- `PATCH /session/:id` — update session properties (`{ title }`)
|
|
64
|
+
- `POST /session/:id/select` — tell the TUI to navigate to a session (only with TUI attached)
|
|
65
|
+
- `POST /session/:id/message` — send message to session (returns parts synchronously; optional `agent` field)
|
|
66
|
+
- `GET /agent` — list available agents
|
|
67
|
+
- `GET /provider` — list connected providers/models
|
|
28
68
|
|
|
29
69
|
## OpenCode tmux session model (ocmux)
|
|
30
70
|
|
|
31
71
|
- Single tmux session named `"Opencode"`
|
|
32
72
|
- Windows named by full project directory path
|
|
33
|
-
- Server pane runs `opencode serve --port 0
|
|
73
|
+
- Server pane runs `opencode serve --port 0` (via send-keys → interactive shell for env var support), TUI pane runs `opencode attach --continue`
|
|
74
|
+
- Log file per project: `/tmp/opencode-serve-<hashDir(dir)>.log` — captured via `tee`, read by ocmux polling loop
|
|
34
75
|
- Per-project `.ocmux.json` with upward directory search (git-like)
|
|
35
|
-
- Auto-restarts dead TUI panes on switch
|
|
76
|
+
- Auto-restarts dead TUI panes on switch (respawn-pane or split-window)
|
|
36
77
|
- Auto-zooms TUI pane on switch/create
|
|
37
78
|
|
|
38
79
|
## Commands
|
|
39
80
|
|
|
40
81
|
| Command | What |
|
|
41
|
-
|
|
82
|
+
|---|---|---|
|
|
42
83
|
| `npm link` | local dev install |
|
|
43
84
|
| `npm install -g .` | alternative local install |
|
|
44
85
|
| `agentp [--qa] [port]` | pipe stdin → opencode TUI, stream answer to stdout |
|
|
45
|
-
| `
|
|
86
|
+
| `tgagentp [port]` | bridge Telegram bot ↔ opencode TUI (needs `TELEGRAM_BOT_TOKEN`) |
|
|
87
|
+
| `tgagentp --dev` | enable `/shutdown` command for remote restart |
|
|
88
|
+
| `ocmux serve [dir]` | start opencode serve in a tmux window (primary verb) |
|
|
89
|
+
| `ocmux new [dir]` | alias for `serve` (backwards compat, to be removed in 1.0) |
|
|
46
90
|
| `ocmux kill [dir]` | kill server + tmux window |
|
|
47
91
|
| `ocmux list [-l]` | list running servers |
|
|
48
92
|
| `ocmux [dir]` | switch to existing opencode tmux window |
|
|
93
|
+
|
|
94
|
+
## TODO
|
|
95
|
+
|
|
96
|
+
### Pending
|
|
97
|
+
|
|
98
|
+
*(none)*
|
|
99
|
+
|
|
100
|
+
### Done
|
|
101
|
+
|
|
102
|
+
- /agents switch <name> — switch active agent on the active session
|
|
103
|
+
- /sessions new [name] — create a new session, TUI follows via /session/:id/select
|
|
104
|
+
- /sessions rename <name> — rename the current active session
|
|
105
|
+
- Merge /session into /sessions (keep /session as alias)
|
|
106
|
+
- Case-insensitive matching for all switch subcommands (/servers, /sessions, /agents)
|
|
107
|
+
- Fix: /sessions filter includes sessions with directory (TUI sessions) + active session exception
|
|
108
|
+
- Fix: /sessions now shows the active session even without agent set (exception in filter)
|
|
109
|
+
- Fix: /sessions switch and /sessions new call selectSession (TUI navigation doesn't work — see With Issues)
|
|
110
|
+
- Fix: active session highlighted with ▶ bullet in session list
|
|
111
|
+
- Message splitting (>4096 chars, breaks at newlines)
|
|
112
|
+
- lib/ocmux.js shared session discovery, refactor ocmux + tgagentp
|
|
113
|
+
- /cancel with true server-side SSE abort via cancelRef
|
|
114
|
+
- /shutdown (--dev mode) with Telegram offset acknowledgment
|
|
115
|
+
|
|
116
|
+
### With Issues
|
|
117
|
+
|
|
118
|
+
- `/sessions switch` and `/sessions new` — `selectSession` tries `POST /session/:id/select` and `POST /tui/select-session` but the TUI pane doesn't navigate to the session. Message routing (via `activeSessionId` + `sendToSession`) works correctly in the background. This is a known limitation — the TUI must be navigated manually to see the conversation in the UI. The underlying opencode API may lack the needed TUI navigation endpoints.
|
|
119
|
+
|
|
120
|
+
### Verified
|
|
121
|
+
|
|
122
|
+
- Password auth support (OPENCODE_SERVER_PASSWORD) for agentp, ocmux, tgagentp
|
|
123
|
+
- agentp HTTP Basic Auth headers on all requests (POST and SSE)
|
|
124
|
+
- agentp clear 401 error reporting
|
|
125
|
+
- ocmux interactive shells (send-keys) for env var propagation
|
|
126
|
+
- tgagentp Telegram bridge bot with long-polling
|
|
127
|
+
- Markdown→HTML converter (**bold**, *italic*, `code`, code blocks, headings, tables)
|
|
128
|
+
- Chat allowlist (TGAGENTP_ALLOWED_CHAT_IDS)
|
|
129
|
+
- Slash command system (/help, /servers, /sessions, /session, /agents, /agents switch, /models, /status, /cancel, /shutdown)
|
|
130
|
+
- Per-server state tracking (busy/idle, pendingResponse, activeSessionId, activeAgentName, taskId)
|
|
131
|
+
- Background async processing (non-commands fire without await)
|
|
132
|
+
- /servers switch activates tmux window (pin, restart TUI, select, zoom)
|
|
133
|
+
- Startup greeting with /status-style info persisted via temp file
|
|
134
|
+
- Fix port mismatch bug: truncate stale log file before send-keys in doNew
|
|
135
|
+
- Update README with full tgagentp features (slash commands, --dev, env vars)
|
|
136
|
+
- Update AGENTS.md with lib/ocmux.js, missing API endpoints, current architecture
|
|
137
|
+
- Single-user lock: first chat to connect owns the session
|
|
138
|
+
- Fix startup greeting markdown not being converted to HTML
|
|
139
|
+
|
|
140
|
+
*(Keep this TODO up to date: move items between categories as their status changes.)*
|
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,8 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
7
|
- Rename package and CLI command from `aprompt` to `agentp`.
|
|
8
|
+
- Extract shared OpenCode client logic into `lib/opencode.js`.
|
|
9
|
+
- Add `tgagentp` — bridge a Telegram bot chat with an OpenCode TUI session.
|
|
8
10
|
|
|
9
11
|
## [0.1.0] - 2026-05-08
|
|
10
12
|
|
package/README.md
CHANGED
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
[](https://www.npmjs.com/package/agentp)
|
|
6
6
|
[](https://www.npmjs.com/package/agentp)
|
|
7
7
|
|
|
8
|
-
This package provides
|
|
8
|
+
This package provides three CLI tools:
|
|
9
9
|
|
|
10
10
|
- **`agentp`** — pipes prompt text into a running OpenCode TUI session and streams the assistant final answer back to stdout
|
|
11
11
|
- **`ocmux`** — manages OpenCode server + TUI sessions in tmux (create, switch, kill, list)
|
|
12
|
+
- **`tgagentp`** — bridges a Telegram bot chat with all running OpenCode TUI sessions (receives messages from Telegram, routes them to the active server, sends answers back). Supports slash commands for multi-server management, session switching, agent/model listing, and remote restart.
|
|
12
13
|
|
|
13
14
|
It is designed for prompt-driven workflows where you want to do things like:
|
|
14
15
|
|
|
@@ -122,7 +123,7 @@ Without arguments, searches upward from `<directory>` (default: `$PWD`) for `.oc
|
|
|
122
123
|
|
|
123
124
|
Subcommands:
|
|
124
125
|
|
|
125
|
-
- **`serve [--git|--GIT] [dir]`** — Create a server in `dir` (default: `$PWD`) and attach a TUI pane. Aliased as `new` for backwards compatibility. Errors if one already exists there. Warns if a parent directory already has a server.
|
|
126
|
+
- **`serve [--git|--GIT] [dir]`** — Create a server in `dir` (default: `$PWD`) and attach a TUI pane. Aliased as `new` for backwards compatibility (to be removed in 1.0). Errors if one already exists there. Warns if a parent directory already has a server.
|
|
126
127
|
- `--git` resolves `dir` to the nearest parent with a `.git` entry (file or dir); errors if none is found.
|
|
127
128
|
- `--GIT` resolves `dir` to the nearest parent with a `.git` directory only; errors if none is found.
|
|
128
129
|
- **`kill [dir]`** — Kill the server found upward from `dir`. Removes its tmux window and state file.
|
|
@@ -162,6 +163,70 @@ Operational hint:
|
|
|
162
163
|
|
|
163
164
|
- You can keep a separate `opencode attach` view open to see the full run context while `agentp` is used from shell scripts or editor buffers.
|
|
164
165
|
|
|
166
|
+
## tgagentp
|
|
167
|
+
|
|
168
|
+
Bridge a Telegram bot chat with OpenCode TUI sessions managed by `ocmux`.
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
tgagentp [options]
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Keeps running indefinitely. On each text message from Telegram it:
|
|
175
|
+
|
|
176
|
+
1. Routes non-command messages to the current active server's TUI (same protocol as `agentp`).
|
|
177
|
+
2. Waits for the assistant to finish (background async — commands remain responsive).
|
|
178
|
+
3. Sends the full answer back to the same Telegram chat.
|
|
179
|
+
|
|
180
|
+
Non-text Telegram updates (photos, stickers, etc.) are silently ignored.
|
|
181
|
+
|
|
182
|
+
### Slash commands
|
|
183
|
+
|
|
184
|
+
| Command | Action |
|
|
185
|
+
|---|---|
|
|
186
|
+
| `/help [topic]` | Show general help or help for a topic (`servers`, `sessions`, `agents`, `models`) |
|
|
187
|
+
| `/servers` | List all running ocmux-served projects with URL + status (✅ idle / ⏳ busy) |
|
|
188
|
+
| `/servers switch <name>` | Switch active server; matches by full path, basename, or substring |
|
|
189
|
+
| `/sessions` | List recent sessions for the current server (max 50, with date headings) |
|
|
190
|
+
| `/sessions switch <number\|name>` | Switch active session by position or partial name match |
|
|
191
|
+
| `/agents` | List available agents (name, mode, model, description) |
|
|
192
|
+
| `/models` | List connected providers with model counts, context limits, and costs |
|
|
193
|
+
| `/status` | Show current server path, URL, busy status, and active session |
|
|
194
|
+
| `/cancel` | Cancel the current AI response for the active server |
|
|
195
|
+
| `/shutdown` | (requires `--dev`) Stop tgagentp gracefully; acknowledge offset and exit |
|
|
196
|
+
|
|
197
|
+
### Per-server state
|
|
198
|
+
|
|
199
|
+
`tgagentp` tracks state independently per server URL: busy/idle status, pending response, active session ID, and cancellation token. Switching servers while one is busy stores the answer as pending — switching back delivers it.
|
|
200
|
+
|
|
201
|
+
### Setup
|
|
202
|
+
|
|
203
|
+
1. Create a bot with [@BotFather](https://t.me/BotFather) on Telegram and copy the token.
|
|
204
|
+
2. Export the token and start `tgagentp`:
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
export TELEGRAM_BOT_TOKEN="your-bot-token-here"
|
|
208
|
+
tgagentp
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Options
|
|
212
|
+
|
|
213
|
+
- `--version`: show version
|
|
214
|
+
- `--help`: show help message
|
|
215
|
+
- `--dev`: enable `/shutdown` command for remote restart (run via `while true; do tgagentp --dev; done`)
|
|
216
|
+
|
|
217
|
+
### Telemetry
|
|
218
|
+
|
|
219
|
+
A startup greeting is sent to the last known chat on boot — includes /status-style server info. The chat ID is persisted at `/tmp/tgagentp-startup-chat`.
|
|
220
|
+
|
|
221
|
+
### Environment variables
|
|
222
|
+
|
|
223
|
+
| Variable | Description |
|
|
224
|
+
|---|---|
|
|
225
|
+
| `TELEGRAM_BOT_TOKEN` | **Required.** Telegram bot token from @BotFather. |
|
|
226
|
+
| `OPENCODE_SERVER_PASSWORD` | Optional. OpenCode server HTTP Basic Auth password. |
|
|
227
|
+
| `OPENCODE_SERVER_USERNAME` | Optional. OpenCode server username (default: `opencode`). |
|
|
228
|
+
| `TGAGENTP_ALLOWED_CHAT_IDS` | Optional. Comma-separated Telegram chat IDs that are allowed to use the bot. |
|
|
229
|
+
|
|
165
230
|
## License
|
|
166
231
|
|
|
167
232
|
MIT
|
package/bin/agentp
CHANGED
|
@@ -1,73 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
const { URL } = require('url');
|
|
3
|
+
const readline = require('readline');
|
|
5
4
|
const { version } = require('../package.json');
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
return { 'Authorization': `Basic ${encoded}` };
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function makeRequest(options, data) {
|
|
16
|
-
return new Promise((resolve, reject) => {
|
|
17
|
-
const req = http.request(options, (res) => {
|
|
18
|
-
let body = '';
|
|
19
|
-
res.on('data', (chunk) => { body += chunk; });
|
|
20
|
-
res.on('end', () => {
|
|
21
|
-
if (res.statusCode === 401) {
|
|
22
|
-
reject(new Error('OpenCode server authentication failed. Set OPENCODE_SERVER_PASSWORD to match the server password.'));
|
|
23
|
-
} else {
|
|
24
|
-
resolve({ status: res.statusCode, body });
|
|
25
|
-
}
|
|
26
|
-
});
|
|
27
|
-
});
|
|
28
|
-
req.on('error', reject);
|
|
29
|
-
if (data) req.write(data);
|
|
30
|
-
req.end();
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function buildJsonRequest(url, method, body) {
|
|
35
|
-
const parsed = new URL(url);
|
|
36
|
-
return {
|
|
37
|
-
hostname: parsed.hostname,
|
|
38
|
-
port: parsed.port,
|
|
39
|
-
path: parsed.pathname,
|
|
40
|
-
method: method,
|
|
41
|
-
headers: {
|
|
42
|
-
'Content-Type': 'application/json',
|
|
43
|
-
'Content-Length': Buffer.byteLength(body),
|
|
44
|
-
...getAuthHeaders()
|
|
45
|
-
}
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
async function clearPrompt(server) {
|
|
50
|
-
const url = `${server}/tui/clear-prompt`;
|
|
51
|
-
await makeRequest(buildJsonRequest(url, 'POST', '{}'), '{}');
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
async function appendPrompt(server, text) {
|
|
55
|
-
const url = `${server}/tui/append-prompt`;
|
|
56
|
-
const body = JSON.stringify({ text });
|
|
57
|
-
await makeRequest(buildJsonRequest(url, 'POST', body), body);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
async function submitPrompt(server) {
|
|
61
|
-
const url = `${server}/tui/submit-prompt`;
|
|
62
|
-
await makeRequest(buildJsonRequest(url, 'POST', '{}'), '{}');
|
|
63
|
-
}
|
|
5
|
+
const {
|
|
6
|
+
clearPrompt,
|
|
7
|
+
appendPrompt,
|
|
8
|
+
submitPrompt,
|
|
9
|
+
listenForFinalAnswer,
|
|
10
|
+
} = require('../lib/opencode');
|
|
64
11
|
|
|
65
12
|
async function sendPrompt(server) {
|
|
66
13
|
let promptText = '';
|
|
67
14
|
|
|
68
15
|
await clearPrompt(server);
|
|
69
16
|
|
|
70
|
-
const readline = require('readline');
|
|
71
17
|
const rl = readline.createInterface({ input: process.stdin });
|
|
72
18
|
|
|
73
19
|
for await (const line of rl) {
|
|
@@ -85,94 +31,6 @@ const HUMAN_ROW = '👤:  ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄
|
|
|
85
31
|
const AGENT_ROW = '🤖:  ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄\n'
|
|
86
32
|
const FINAL_ROW = ' ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄\n';
|
|
87
33
|
|
|
88
|
-
function listenForFinalAnswer(server, showFinalRow) {
|
|
89
|
-
return new Promise((resolve, reject) => {
|
|
90
|
-
const url = `${server}/event`;
|
|
91
|
-
const parsed = new URL(url);
|
|
92
|
-
|
|
93
|
-
const req = http.request({
|
|
94
|
-
hostname: parsed.hostname,
|
|
95
|
-
port: parsed.port,
|
|
96
|
-
path: parsed.pathname,
|
|
97
|
-
method: 'GET',
|
|
98
|
-
headers: { 'Accept': 'text/event-stream', ...getAuthHeaders() }
|
|
99
|
-
}, (res) => {
|
|
100
|
-
if (res.statusCode === 401) {
|
|
101
|
-
res.resume();
|
|
102
|
-
reject(new Error('OpenCode server authentication failed. Set OPENCODE_SERVER_PASSWORD to match the server password.'));
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
let buffer = '';
|
|
106
|
-
const userMessageIDs = new Set();
|
|
107
|
-
|
|
108
|
-
res.on('data', (chunk) => {
|
|
109
|
-
buffer += chunk.toString();
|
|
110
|
-
|
|
111
|
-
// Split by double newline (SSE event separator)
|
|
112
|
-
while (buffer.includes('\n\n')) {
|
|
113
|
-
const eventEnd = buffer.indexOf('\n\n');
|
|
114
|
-
const eventData = buffer.slice(0, eventEnd);
|
|
115
|
-
buffer = buffer.slice(eventEnd + 2);
|
|
116
|
-
|
|
117
|
-
// Extract data lines
|
|
118
|
-
const lines = eventData.split('\n');
|
|
119
|
-
|
|
120
|
-
let jsonStr = '';
|
|
121
|
-
|
|
122
|
-
for (const line of lines) {
|
|
123
|
-
if (line.startsWith('data: ')) {
|
|
124
|
-
jsonStr += line.slice(6);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
if (!jsonStr.trim()) continue;
|
|
129
|
-
|
|
130
|
-
try {
|
|
131
|
-
const event = JSON.parse(jsonStr);
|
|
132
|
-
|
|
133
|
-
// Track user message IDs
|
|
134
|
-
if (event.type === 'message.updated' && event.properties?.info?.role === 'user') {
|
|
135
|
-
userMessageIDs.add(event.properties.info.id);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// Check for session.idle to detect completion
|
|
139
|
-
if (event.type === 'session.idle') {
|
|
140
|
-
process.stdout.write('\n');
|
|
141
|
-
if (showFinalRow) {
|
|
142
|
-
process.stdout.write(`${FINAL_ROW}`);
|
|
143
|
-
}
|
|
144
|
-
req.destroy();
|
|
145
|
-
res.destroy();
|
|
146
|
-
resolve();
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const part = event.properties?.part;
|
|
151
|
-
if (!part) continue;
|
|
152
|
-
|
|
153
|
-
// Filter out user input by checking messageID
|
|
154
|
-
if (part.messageID && userMessageIDs.has(part.messageID)) {
|
|
155
|
-
continue;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
if (part.type === 'text' && part.text != null && part.text !== '') {
|
|
159
|
-
process.stdout.write(part.text);
|
|
160
|
-
}
|
|
161
|
-
} catch (e) {
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
res.on('end', () => {
|
|
167
|
-
resolve();
|
|
168
|
-
});
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
req.on('error', reject);
|
|
172
|
-
req.end();
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
|
|
176
34
|
async function main() {
|
|
177
35
|
let serverBase = 'http://localhost:4096';
|
|
178
36
|
let qaMode = false;
|
|
@@ -221,7 +79,11 @@ async function main() {
|
|
|
221
79
|
process.stdout.write(AGENT_ROW);
|
|
222
80
|
}
|
|
223
81
|
|
|
224
|
-
await listenForFinalAnswer(serverBase,
|
|
82
|
+
await listenForFinalAnswer(serverBase, (chunk) => process.stdout.write(chunk));
|
|
83
|
+
process.stdout.write('\n');
|
|
84
|
+
if (qaMode) {
|
|
85
|
+
process.stdout.write(FINAL_ROW);
|
|
86
|
+
}
|
|
225
87
|
|
|
226
88
|
if (!process.env.OPENCODE_SERVER_PASSWORD) {
|
|
227
89
|
process.stderr.write('\n');
|
package/bin/ocmux
CHANGED
|
@@ -6,6 +6,16 @@ const crypto = require('crypto');
|
|
|
6
6
|
const readline = require('readline');
|
|
7
7
|
const { spawnSync, execSync } = require('child_process');
|
|
8
8
|
const os = require('os');
|
|
9
|
+
const {
|
|
10
|
+
readState,
|
|
11
|
+
statefileFor,
|
|
12
|
+
tuiPaneId,
|
|
13
|
+
windowByDir,
|
|
14
|
+
windowNameByIndex,
|
|
15
|
+
activeWindowIndex,
|
|
16
|
+
paneCount,
|
|
17
|
+
listServers,
|
|
18
|
+
} = require('../lib/ocmux');
|
|
9
19
|
|
|
10
20
|
// ---------------------------------------------------------------------------
|
|
11
21
|
// Constants
|
|
@@ -43,18 +53,6 @@ function logfileFor(dir) {
|
|
|
43
53
|
return `/tmp/opencode-serve-${hashDir(dir)}.log`;
|
|
44
54
|
}
|
|
45
55
|
|
|
46
|
-
function statefileFor(dir) {
|
|
47
|
-
return path.join(dir, '.ocmux.json');
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function readState(file) {
|
|
51
|
-
try {
|
|
52
|
-
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
53
|
-
} catch {
|
|
54
|
-
return null;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
56
|
function findParentStatefile(startDir) {
|
|
59
57
|
let dir = path.resolve(startDir);
|
|
60
58
|
while (true) {
|
|
@@ -112,52 +110,6 @@ function resolveGitDir(startDir, requireDir, flagLabel) {
|
|
|
112
110
|
return result.dir;
|
|
113
111
|
}
|
|
114
112
|
|
|
115
|
-
function windowByDir(dir) {
|
|
116
|
-
const r = tmux(['list-windows', '-t', SESSION, '-F', '#{window_index}\t#{window_name}']);
|
|
117
|
-
if (r.status !== 0) return null;
|
|
118
|
-
for (const line of r.stdout.trim().split('\n').filter(Boolean)) {
|
|
119
|
-
const [idx, name] = line.split('\t');
|
|
120
|
-
if (name === dir) return parseInt(idx, 10);
|
|
121
|
-
}
|
|
122
|
-
return null;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function windowNameByIndex(idx) {
|
|
126
|
-
const r = tmux(['list-windows', '-t', SESSION, '-F', '#{window_index}\t#{window_name}']);
|
|
127
|
-
if (r.status !== 0) return null;
|
|
128
|
-
for (const line of r.stdout.trim().split('\n').filter(Boolean)) {
|
|
129
|
-
const [wi, name] = line.split('\t');
|
|
130
|
-
if (parseInt(wi, 10) === idx) return name;
|
|
131
|
-
}
|
|
132
|
-
return null;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function activeWindowIndex() {
|
|
136
|
-
const r = tmux(['list-windows', '-t', SESSION, '-F', '#{window_index} #{window_active}']);
|
|
137
|
-
if (r.status !== 0) return null;
|
|
138
|
-
for (const line of r.stdout.trim().split('\n').filter(Boolean)) {
|
|
139
|
-
const [idx, active] = line.split(' ');
|
|
140
|
-
if (active === '1') return parseInt(idx, 10);
|
|
141
|
-
}
|
|
142
|
-
return null;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function tuiPaneId(windowIndex) {
|
|
146
|
-
const r = tmux(['list-panes', '-t', `${SESSION}:${windowIndex}`, '-F', '#{pane_id}\t#{pane_current_command}']);
|
|
147
|
-
if (r.status !== 0) return null;
|
|
148
|
-
for (const line of r.stdout.trim().split('\n').filter(Boolean)) {
|
|
149
|
-
const [paneId, cmd] = line.split('\t');
|
|
150
|
-
if (cmd === 'node') return paneId;
|
|
151
|
-
}
|
|
152
|
-
return null;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function paneCount(windowIndex) {
|
|
156
|
-
const r = tmux(['list-panes', '-t', `${SESSION}:${windowIndex}`, '-F', '#{pane_id}']);
|
|
157
|
-
if (r.status !== 0) return 0;
|
|
158
|
-
return r.stdout.trim().split('\n').filter(Boolean).length;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
113
|
function findWindowsByBasename(name) {
|
|
162
114
|
const r = tmux(['list-windows', '-t', SESSION, '-F', '#{window_name}']);
|
|
163
115
|
if (r.status !== 0) return [];
|
|
@@ -316,29 +268,7 @@ function cmdSwitch(statefile, longMode) {
|
|
|
316
268
|
// ---------------------------------------------------------------------------
|
|
317
269
|
|
|
318
270
|
function cmdList() {
|
|
319
|
-
const
|
|
320
|
-
if (sessionR.status !== 0) {
|
|
321
|
-
console.log('No opencode servers running.');
|
|
322
|
-
return;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
const listR = tmux(['list-windows', '-t', SESSION, '-F', '#{window_index}\t#{window_name}']);
|
|
326
|
-
if (listR.status !== 0 || !listR.stdout.trim()) {
|
|
327
|
-
console.log('No opencode servers running.');
|
|
328
|
-
return;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
const rows = [];
|
|
332
|
-
for (const line of listR.stdout.trim().split('\n').filter(Boolean)) {
|
|
333
|
-
const [idx, dir] = line.split('\t');
|
|
334
|
-
const sf = path.join(dir, '.ocmux.json');
|
|
335
|
-
if (!fs.existsSync(sf)) continue;
|
|
336
|
-
const state = readState(sf);
|
|
337
|
-
if (!state || !state.url) continue;
|
|
338
|
-
const tui = tuiPaneId(parseInt(idx, 10));
|
|
339
|
-
rows.push({ url: state.url, dir, status: tui ? 'alive' : 'dead' });
|
|
340
|
-
}
|
|
341
|
-
|
|
271
|
+
const rows = listServers();
|
|
342
272
|
if (rows.length === 0) {
|
|
343
273
|
console.log('No opencode servers running.');
|
|
344
274
|
return;
|
|
@@ -409,6 +339,9 @@ function doNew(targetDir, longMode) {
|
|
|
409
339
|
|
|
410
340
|
pinWindowName(winIdx);
|
|
411
341
|
|
|
342
|
+
// Truncate log file so stale URL from previous run can't be matched by polling
|
|
343
|
+
try { fs.truncateSync(lf); } catch { /* may not exist yet */ }
|
|
344
|
+
|
|
412
345
|
// Send the command to the interactive shell (like typing it manually)
|
|
413
346
|
tmux(['send-keys', '-t', `${SESSION}:${winIdx}.0`,
|
|
414
347
|
`opencode serve --port 0 2>&1 | tee '${lf}'`, 'Enter']);
|