agentp 0.8.0 → 0.9.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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Three binaries, all registered
4
4
 
5
- - `bin/agentp` (Node.js) — registered in `package.json` `"bin"` — pipes stdin → opencode TUI
5
+ - `bin/agentp` (Node.js) — registered in `package.json` `"bin"` — pipes stdin → opencode session
6
6
  - `bin/ocmux` (Node.js) — registered in `package.json` `"bin"` — manages opencode tmux sessions
7
7
  - `bin/tgagentp` (Node.js) — registered in `package.json` `"bin"` — bridges Telegram bot ↔ opencode TUI
8
8
 
@@ -37,11 +37,14 @@ Extracted from `bin/agentp`; used by both `agentp` and `tgagentp`:
37
37
  - `listenForFinalAnswer(server, onText?, cancelRef?)` — SSE event stream listener; `cancelRef` allows true server-side abort via `req.destroy()`
38
38
  - `listSessions(server, directory?)` — `GET /session`; returns parsed JSON array; optional `?directory=` filter
39
39
  - `createSession(server, title?)` — `POST /session`; returns the created `Session` object; optional `title`
40
- - `updateSession(server, sessionId, title)` — `PATCH /session/:id`; returns the updated `Session` object
41
- - `sendToSession(server, sessionId, text, agent?)` — `POST /session/:id/message`; returns concatenated text parts from response; optional `agent` to handle the message
40
+ - `updateSession(server, sessionId, title, agent?)` — `PATCH /session/:id`; returns the updated `Session` object; optional `agent` to set the session's agent
41
+ - `sendToSession(server, sessionId, text, agent?, cancelRef?)` — `POST /session/:id/message`; returns concatenated text parts from response; optional `agent`, optional `cancelRef` for aborting the request
42
+ - `sendToSessionAsync(server, sessionId, text, agent?)` — `POST /session/:id/prompt_async`; returns 204; non-blocking, answer delivered via SSE
43
+ - `respondToPermission(server, sessionId, permissionId, response)` — `POST /session/:id/permissions/:id`
42
44
  - `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)
43
45
  - `listAgents(server)` — `GET /agent`; returns parsed JSON array of agent objects
44
46
  - `listProviders(server)` — `GET /provider`; returns parsed JSON array of provider objects
47
+ - `listenForSessionEvents(server, sessionId, callbacks, cancelRef?)` — SSE listener for session events (text parts, permissions, thinking, session.idle)
45
48
 
46
49
  ## Shared `lib/ocmux.js`
47
50
 
@@ -67,17 +70,28 @@ These endpoints are consumed but not documented elsewhere in the repo:
67
70
  - `GET /event` (SSE stream)
68
71
  - `GET /session` — list sessions (optional `?directory=` filter)
69
72
  - `POST /session` — create a new session (optional `{ title? }` body)
70
- - `PATCH /session/:id` — update session properties (`{ title }`)
73
+ - `PATCH /session/:id` — update session properties (`{ title }`, optional `{ agent }`)
71
74
  - `POST /session/:id/select` — tell the TUI to navigate to a session (only with TUI attached)
72
75
  - `POST /session/:id/message` — send message to session (returns parts synchronously; optional `agent` field)
76
+ - `POST /session/:id/prompt_async` — send message to session (returns 204; answer via SSE)
73
77
  - `GET /agent` — list available agents
74
78
  - `GET /provider` — list connected providers/models
75
79
 
80
+ ## Agentp gateway (tgagentp ↔ agentp interop)
81
+
82
+ - tgagentp starts a tiny HTTP server (POST /send) on `127.0.0.1`, port from `TGAGENTP_PORT` (default 0 = random).
83
+ - Port is written to `/tmp/tgagentp-port` for agentp discovery.
84
+ - Validates `Authorization` header against `OPENCODE_SERVER_PASSWORD`.
85
+ - agentp `--tg` reads the port file and POSTs `{ text, server }` to the gateway.
86
+ - Messages for the active server are delivered immediately; others are queued per-server.
87
+ - Debounced Telegram notification (configurable via `TGAGENTP_DEBOUNCE_MS`, default 5000ms).
88
+ - Queued messages are flushed on `/servers switch`.
89
+
76
90
  ## OpenCode tmux session model (ocmux)
77
91
 
78
92
  - Single tmux session named `"Opencode"`
79
93
  - Windows named by full project directory path
80
- - Server pane runs `opencode serve --port 0` (via send-keys → interactive shell for env var support), TUI pane runs `opencode attach --continue`
94
+ - Server pane runs `opencode serve --port 0` (via send-keys → interactive shell for env var support), TUI pane runs `opencode attach --continue`; `ocmux serve --print-logs` adds `--print-logs` to the serve command
81
95
  - Log file per project: `/tmp/opencode-serve-<hashDir(dir)>.log` — captured via `tee`, read by ocmux polling loop
82
96
  - Per-project `.ocmux.json` with upward directory search (git-like)
83
97
  - Auto-restarts dead TUI panes on switch (respawn-pane or split-window)
@@ -89,10 +103,10 @@ These endpoints are consumed but not documented elsewhere in the repo:
89
103
  |---|---|---|
90
104
  | `npm link` | local dev install |
91
105
  | `npm install -g .` | alternative local install |
92
- | `agentp [--qa] [port]` | pipe stdin → opencode TUI, stream answer to stdout |
106
+ | `agentp [--qa] [--tg|--no-tg] [port]` | pipe stdin → opencode session, stream answer to stdout (uses session API); `--tg` forwards answer to Telegram |
93
107
  | `tgagentp [port]` | bridge Telegram bot ↔ opencode TUI (needs `TELEGRAM_BOT_TOKEN`) |
94
108
  | `tgagentp --dev` | enable `/shutdown` command for remote restart |
95
- | `ocmux serve [dir]` | start opencode serve in a tmux window (primary verb) |
109
+ | `ocmux serve [--print-logs] [dir]` | start opencode serve in a tmux window (primary verb) |
96
110
  | `ocmux new [dir]` | alias for `serve` (backwards compat, to be removed in 1.0) |
97
111
  | `ocmux kill [dir]` | kill server + tmux window |
98
112
  | `ocmux list [-l]` | list running servers |
@@ -102,7 +116,7 @@ These endpoints are consumed but not documented elsewhere in the repo:
102
116
 
103
117
  ### Pending
104
118
 
105
- - `--print-logs` flag for `ocmux serve`
119
+ - `agentp --getLast n` — retrieve last n answers from session history
106
120
 
107
121
  ### In Progress
108
122
 
@@ -110,12 +124,20 @@ These endpoints are consumed but not documented elsewhere in the repo:
110
124
 
111
125
  ### Done
112
126
 
127
+ - agentp `--qa` full context forwarding to Telegram (rulers, prompt, answer) — 0.9.0
128
+ - agentp resilience: 5s HTTP timeout, pre-send gate check, post-send warning (not hard error) for `--tg`; auto mode silently degrades — 0.9.0
129
+ - tgagentp: `lockedChatId` set at startup to fix race condition with agentp gateway — 0.9.0
130
+ - tgagentp: server error handler + try-catch in gateway handler (no crash on socket errors) — 0.9.0
131
+ - agentp `--tg`/`--no-tg` — gateway forwards answer to Telegram via tgagentp HTTP server; per-server queue with debounced notifications; auto-flush on server switch
132
+ - agentp session API migration — uses `POST /session/:id/message` instead of TUI endpoints
133
+ - ocmux `--print-logs` flag — pass-through to `opencode serve --print-logs` (prints server logs to stderr in the server pane)
134
+ - /agents lists only primary agents with ▶ active marker
135
+ - /agents switch calls `updateSession(agent)` to persist agent on session + `selectSession` to refresh TUI
113
136
  - Logging rebalance: stdout for info, stderr for errors/debug/trace, `--verbose` flag, `2>/dev/null` default usage
114
137
  - `/shutdown force` — refuses shutdown when busy unless `force` flag is given; goodbye message includes state
115
138
  - `/think [on|off|switch]` — toggle real-time forwarding of model thinking messages to the chat; `--think` CLI flag to start enabled
116
139
  - Event-driven permission handling (`POST /session/:id/prompt_async` + SSE listener + `/allow`, `/reject`, `/always` commands) — included in v0.7.0
117
140
  - TUI navigation on `/sessions switch` and `/sessions new` — `POST /tui/select-session` now works in opencode 1.15.13 (was: tried second, never reached)
118
- - /agents switch <name> — switch active agent on the active session
119
141
  - /sessions new [name] — create a new session, TUI follows via /session/:id/select
120
142
  - /sessions rename <name> — rename the current active session
121
143
  - Merge /session into /sessions (keep /session as alias)
package/README.md CHANGED
@@ -53,9 +53,13 @@ agentp [options] [url]
53
53
  Options:
54
54
 
55
55
  - `--qa`: print the original prompt and answer with labels (useful when used as a filter)
56
+ - `--tg`: forward the answer to Telegram via tgagentp gateway (error if unreachable)
57
+ - `--no-tg`: do not forward to Telegram
56
58
  - `--version`: show version
57
59
  - `--help`: show help message
58
60
 
61
+ By default, `--qa` implies `--tg`; standalone mode implies `--no-tg`.
62
+
59
63
  Arguments:
60
64
 
61
65
  - `url`: OpenCode TUI server URL or port number (defaults to `4096`). Examples: `4096`, `http://localhost:4096`, `http://192.168.1.50:4096`
@@ -111,6 +115,15 @@ From Vim/Neovim, send the current visual selection and keep the prompt with the
111
115
  :'<,'>!agentp --qa
112
116
  ```
113
117
 
118
+ From Vim/Neovim, forward the answer to your Telegram:
119
+
120
+ ```vim
121
+ :'<,'>!agentp --qa --tg
122
+ ```
123
+
124
+ (Works as long as `tgagentp` is running. The answer appears both in the editor
125
+ and in your Telegram chat.)
126
+
114
127
  ## ocmux
115
128
 
116
129
  Manage OpenCode server + TUI in tmux for project directories.
@@ -123,9 +136,10 @@ Without arguments, searches upward from `<directory>` (default: `$PWD`) for `.oc
123
136
 
124
137
  Subcommands:
125
138
 
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.
139
+ - **`serve [--git|--GIT] [--print-logs] [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.
127
140
  - `--git` resolves `dir` to the nearest parent with a `.git` entry (file or dir); errors if none is found.
128
141
  - `--GIT` resolves `dir` to the nearest parent with a `.git` directory only; errors if none is found.
142
+ - `--print-logs` passes `--print-logs` to `opencode serve`, which prints server logs to stderr in the server tmux pane.
129
143
  - **`kill [dir]`** — Kill the server found upward from `dir`. Removes its tmux window and state file.
130
144
  - **`list`** — List all running servers with their directories, URLs, and status.
131
145
 
@@ -153,11 +167,15 @@ Notes:
153
167
 
154
168
  ## How agentp Works
155
169
 
156
- 1. Clears the current TUI prompt input.
157
- 2. Appends each line from stdin to the TUI prompt.
158
- 3. Submits the prompt.
159
- 4. Listens to the event stream and prints assistant text parts.
160
- 5. Stops when the session reaches idle state.
170
+ 1. Reads all stdin into a single prompt string.
171
+ 2. Finds the most recently updated session, or creates one named `agentp`.
172
+ 3. Sends the prompt via the session API (`POST /session/:id/message`).
173
+ 4. Prints the assistant answer to stdout.
174
+ 5. With `--tg` (or by default when `--qa` is given), forwards the answer to Telegram
175
+ via the agentp gateway. The answer appears in your Telegram chat as if tgagentp
176
+ itself had processed the request.
177
+
178
+ The session API ensures the request is processed even when no TUI is attached, and returns the full answer in a single HTTP response.
161
179
 
162
180
  Operational hint:
163
181
 
@@ -183,16 +201,20 @@ Non-text Telegram updates (photos, stickers, etc.) are silently ignored.
183
201
 
184
202
  | Command | Action |
185
203
  |---|---|
186
- | `/help [topic]` | Show general help or help for a topic (`servers`, `sessions`, `agents`, `models`) |
204
+ | `/help [topic]` | Show general help or help for a topic (`servers`, `sessions`, `agents`, `models`, `allow`, `think`) |
187
205
  | `/servers` | List all running ocmux-served projects with URL + status (✅ idle / ⏳ busy) |
188
206
  | `/servers switch <name>` | Switch active server; matches by full path, basename, or substring |
189
207
  | `/sessions` | List recent sessions for the current server (max 50, with date headings) |
190
208
  | `/sessions switch <number\|name>` | Switch active session by position or partial name match |
191
- | `/agents` | List available agents (name, mode, model, description) |
209
+ | `/agents` | List primary agents (▶ marker for the active one) |
210
+ | `/agents switch <name>` | Switch active agent — persists on session and refreshes TUI |
192
211
  | `/models` | List connected providers with model counts, context limits, and costs |
193
212
  | `/status` | Show current server path, URL, busy status, and active session |
194
213
  | `/cancel` | Cancel the current AI response for the active server |
195
214
  | `/think [on\|off\|switch]` | Toggle forwarding of model thinking messages to the chat |
215
+ | `/allow` | Approve a permission request once |
216
+ | `/reject` | Deny a permission request |
217
+ | `/always` | Approve and remember for the session |
196
218
  | `/shutdown [force]` | (requires `--dev`) Stop tgagentp; refuses if busy unless `force` is given |
197
219
 
198
220
  ### Per-server state
@@ -217,6 +239,17 @@ tgagentp
217
239
  - `--think`: start with thinking messages enabled (default: off)
218
240
  - `--dev`: enable `/shutdown` command for remote restart (run via `while true; do tgagentp --dev; done`)
219
241
 
242
+ ### Agentp gateway
243
+
244
+ tgagentp starts a tiny HTTP server on `127.0.0.1` that accepts `POST /send` requests from `agentp --tg`. This enables cross-tool interoperability: answers obtained through `agentp` (from scripts, editors, or pipes) are forwarded to your Telegram chat.
245
+
246
+ - Port is randomly assigned by default; overridable via `TGAGENTP_PORT`.
247
+ - Port is written to `/tmp/tgagentp-port` for agentp discovery.
248
+ - Authentication reuses `OPENCODE_SERVER_PASSWORD`.
249
+ - Messages for the active server are delivered immediately.
250
+ - Messages for non-active servers are queued and delivered on `/servers switch`.
251
+ - Debounced Telegram notification on queue (configurable via `TGAGENTP_DEBOUNCE_MS`).
252
+
220
253
  ### Logging
221
254
 
222
255
  Informational messages (startup, discovery, session switches) go to **stdout**. Errors, warnings, and trace/debug messages go to **stderr**. With `--verbose`, detailed
@@ -243,9 +276,11 @@ A startup greeting is sent to the last known chat on boot — includes /status-s
243
276
  | Variable | Description |
244
277
  |---|---|
245
278
  | `TELEGRAM_BOT_TOKEN` | **Required.** Telegram bot token from @BotFather. |
246
- | `OPENCODE_SERVER_PASSWORD` | Optional. OpenCode server HTTP Basic Auth password. |
279
+ | `OPENCODE_SERVER_PASSWORD` | Optional. OpenCode server HTTP Basic Auth password (also used for agentp gateway auth). |
247
280
  | `OPENCODE_SERVER_USERNAME` | Optional. OpenCode server username (default: `opencode`). |
248
281
  | `TGAGENTP_ALLOWED_CHAT_IDS` | Optional. Comma-separated Telegram chat IDs that are allowed to use the bot. |
282
+ | `TGAGENTP_PORT` | Optional. Agentp gateway listen port (default: `0` = random). |
283
+ | `TGAGENTP_DEBOUNCE_MS` | Optional. Debounce interval for queued-message notifications (default: `5000`). |
249
284
 
250
285
  ## License
251
286
 
package/bin/agentp CHANGED
@@ -1,39 +1,73 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ const http = require('http');
4
+ const fs = require('fs');
3
5
  const readline = require('readline');
4
6
  const { version } = require('../package.json');
5
7
  const {
6
- clearPrompt,
7
- appendPrompt,
8
- submitPrompt,
9
- listenForFinalAnswer,
8
+ sendToSession,
9
+ listSessions,
10
+ createSession,
10
11
  } = require('../lib/opencode');
11
12
 
12
- async function sendPrompt(server) {
13
- let promptText = '';
13
+ const TGAGENTP_PORT_FILE = '/tmp/tgagentp-port';
14
14
 
15
- await clearPrompt(server);
15
+ function notifyAgentpGateway(port, server, text) {
16
+ return new Promise((resolve, reject) => {
17
+ const body = JSON.stringify({ text, server });
18
+ const options = {
19
+ hostname: '127.0.0.1',
20
+ port,
21
+ path: '/send',
22
+ method: 'POST',
23
+ headers: {
24
+ 'Content-Type': 'application/json',
25
+ 'Content-Length': Buffer.byteLength(body),
26
+ },
27
+ };
28
+ const password = process.env.OPENCODE_SERVER_PASSWORD;
29
+ if (password) {
30
+ const username = process.env.OPENCODE_SERVER_USERNAME || 'opencode';
31
+ options.headers['Authorization'] = 'Basic ' +
32
+ Buffer.from(`${username}:${password}`).toString('base64');
33
+ }
34
+ const req = http.request(options, (res) => {
35
+ let data = '';
36
+ res.on('data', (chunk) => { data += chunk; });
37
+ res.on('end', () => {
38
+ if (res.statusCode === 200) resolve();
39
+ else reject(new Error(`status ${res.statusCode}: ${data}`));
40
+ });
41
+ });
42
+ req.setTimeout(5000, () => {
43
+ req.destroy();
44
+ reject(new Error('Connection timed out'));
45
+ });
46
+ req.on('error', reject);
47
+ req.write(body);
48
+ req.end();
49
+ });
50
+ }
16
51
 
52
+ async function readAllStdin() {
53
+ let text = '';
17
54
  const rl = readline.createInterface({ input: process.stdin });
18
-
19
55
  for await (const line of rl) {
20
- const promptLine = line + '\n';
21
- promptText += promptLine;
22
- await appendPrompt(server, promptLine);
56
+ text += line + '\n';
23
57
  }
24
-
25
58
  rl.close();
26
- await submitPrompt(server);
27
- return promptText;
59
+ return text;
28
60
  }
29
61
 
30
- const HUMAN_ROW = '👤:  ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄\n'
31
- const AGENT_ROW = '🤖:  ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄\n'
32
- const FINAL_ROW = ' ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄\n';
62
+ const HUMAN_ROW = '👤: —————————————————\n'
63
+ const AGENT_ROW = '🤖: —————————————————\n'
64
+ const FINAL_ROW = ' —————————————————\n';
33
65
 
34
66
  async function main() {
35
67
  let serverBase = 'http://localhost:4096';
36
68
  let qaMode = false;
69
+ // tgMode: false = disabled, 'auto' = auto-detect, true = explicit --tg
70
+ let tgMode = null;
37
71
 
38
72
  const args = process.argv.slice(2);
39
73
  for (let i = 0; i < args.length; i++) {
@@ -43,18 +77,23 @@ async function main() {
43
77
  } else if (args[i] === '--help') {
44
78
  console.error('Usage: agentp [options] [url]');
45
79
  console.error('');
46
- console.error('Pipe prompt text to an OpenCode TUI session and stream the assistant response to stdout.');
80
+ console.error('Pipe prompt text to an OpenCode session and get the assistant response on stdout.');
47
81
  console.error('');
48
82
  console.error('Options:');
49
83
  console.error(' --version Show version');
50
84
  console.error(' --qa Print the original prompt and answer with labels (useful as a filter)');
85
+ console.error(' --tg Forward the answer to Telegram via agentp gateway (error if unavailable)');
86
+ console.error(' --no-tg Do not forward to Telegram');
51
87
  console.error(' --help Show this help message');
52
88
  console.error('');
89
+ console.error('By default, --qa auto-detects tgagentp; standalone mode implies --no-tg.');
90
+ console.error('--tg: error if tgagentp is unavailable; auto mode: silently degrade.');
91
+ console.error('');
53
92
  console.error('Arguments:');
54
- console.error(' url OpenCode TUI server URL or port number (default: 4096)');
93
+ console.error(' url OpenCode server URL or port number (default: 4096)');
55
94
  console.error(' Examples: 4096, http://localhost:4096, http://192.168.1.50:4096');
56
95
  console.error('');
57
- console.error('See also: ocmux -- manage OpenCode tmux sessions (ocmux --help)');
96
+ console.error('See also: tgagentp -- Telegram bridge (tgagentp --help), ocmux -- manage OpenCode tmux sessions (ocmux --help)');
58
97
  console.error('');
59
98
  console.error('Examples:');
60
99
  console.error(' printf "Summarize this file" | agentp');
@@ -64,6 +103,10 @@ async function main() {
64
103
  process.exit(0);
65
104
  } else if (args[i] === '--qa') {
66
105
  qaMode = true;
106
+ } else if (args[i] === '--tg') {
107
+ tgMode = true;
108
+ } else if (args[i] === '--no-tg') {
109
+ tgMode = false;
67
110
  } else if (args[i].includes('://')) {
68
111
  serverBase = args[i].replace(/\/+$/, '');
69
112
  } else if (/^\d+$/.test(args[i])) {
@@ -71,20 +114,91 @@ async function main() {
71
114
  }
72
115
  }
73
116
 
74
- const promptText = await sendPrompt(serverBase);
117
+ // Resolve default: --qa → auto-detect, standalone → disabled
118
+ if (tgMode === null) {
119
+ tgMode = qaMode ? 'auto' : false;
120
+ }
121
+
122
+ // Pre-send: probe tgagentp gateway if enabled
123
+ let tgPort = null;
124
+ if (tgMode) {
125
+ try {
126
+ tgPort = fs.readFileSync(TGAGENTP_PORT_FILE, 'utf8').trim();
127
+ if (!tgPort || !/^\d+$/.test(tgPort)) tgPort = null;
128
+ } catch {
129
+ tgPort = null;
130
+ }
131
+ if (!tgPort && tgMode === true) {
132
+ console.error('Error: tgagentp gateway not found. Is tgagentp running?');
133
+ process.exit(1);
134
+ }
135
+ // auto mode: tgPort stays null → silently skip tgagentp
136
+ }
137
+
138
+ const promptText = await readAllStdin();
75
139
 
76
- if (qaMode) {
77
- process.stdout.write(HUMAN_ROW);
78
- process.stdout.write(promptText);
79
- process.stdout.write(AGENT_ROW);
140
+ // Find most recent session or create one
141
+ let sessions;
142
+ try {
143
+ sessions = await listSessions(serverBase);
144
+ } catch (err) {
145
+ console.error('Error connecting to server:', err.message);
146
+ process.exit(1);
80
147
  }
81
148
 
82
- await listenForFinalAnswer(serverBase, (chunk) => process.stdout.write(chunk));
83
- process.stdout.write('\n');
149
+ let sessionId = null;
150
+ if (sessions && sessions.length > 0) {
151
+ const sorted = sessions
152
+ .filter(s => s.time && s.time.updated)
153
+ .sort((a, b) => b.time.updated - a.time.updated);
154
+ if (sorted.length > 0) sessionId = sorted[0].id;
155
+ }
156
+
157
+ if (!sessionId) {
158
+ try {
159
+ const session = await createSession(serverBase, 'agentp');
160
+ sessionId = session.id;
161
+ } catch (err) {
162
+ console.error('Error creating session:', err.message);
163
+ process.exit(1);
164
+ }
165
+ }
166
+
167
+ let answer;
168
+ try {
169
+ answer = await sendToSession(serverBase, sessionId, promptText);
170
+ } catch (err) {
171
+ console.error('Error sending prompt:', err.message);
172
+ process.exit(1);
173
+ }
174
+
175
+ // Build full --qa output and send to stdout
176
+ let tgText = answer;
84
177
  if (qaMode) {
85
- process.stdout.write(FINAL_ROW);
178
+ tgText = HUMAN_ROW + promptText + AGENT_ROW + answer;
179
+ if (!answer.endsWith('\n')) tgText += '\n';
180
+ tgText += FINAL_ROW;
181
+ process.stdout.write(tgText);
182
+ } else {
183
+ process.stdout.write(answer);
184
+ if (!answer.endsWith('\n')) process.stdout.write('\n');
185
+ }
186
+
187
+ // Post-send: notify tgagentp if we have a port
188
+ let tgError = null;
189
+ if (tgPort) {
190
+ try {
191
+ await notifyAgentpGateway(tgPort, serverBase, tgText);
192
+ } catch (err) {
193
+ tgError = err;
194
+ }
86
195
  }
87
196
 
197
+ // Warnings at the end (stderr)
198
+ if (tgError && tgMode === true) {
199
+ process.stderr.write('\n');
200
+ process.stderr.write(`⚠️ Telegram notification failed: ${tgError.message}\n`);
201
+ }
88
202
  if (!process.env.OPENCODE_SERVER_PASSWORD) {
89
203
  process.stderr.write('\n');
90
204
  process.stderr.write('🔓 ⚠️ Server is not password-protected! Set OPENCODE_SERVER_PASSWORD for better security.\n');
package/bin/ocmux CHANGED
@@ -192,6 +192,7 @@ function printHelp() {
192
192
  console.error(' --git Use the nearest parent with a .git entry (file or dir).');
193
193
  console.error(' --GIT Use the nearest parent with a .git directory only.');
194
194
  console.error(' Refuses to create above an existing .ocmux.json found while searching.');
195
+ console.error( ' --print-logs Pass --print-logs to opencode serve (prints server logs to stderr).');
195
196
  console.error(' kill Kill the server found upward from <directory>.');
196
197
  console.error(' Removes its tmux window and state file.');
197
198
  console.error(' list List all running servers with their directories and URLs.');
@@ -201,6 +202,7 @@ function printHelp() {
201
202
  console.error('');
202
203
  console.error('Options:');
203
204
  console.error(' -l Long output: append " → <dir>" after the URL (serve/default).');
205
+ console.error(' --print-logs Pass --print-logs to opencode serve (prints server logs to stderr).');
204
206
  console.error(' --version Show version');
205
207
  console.error(' -h Show this help');
206
208
  console.error('');
@@ -290,7 +292,7 @@ function cmdList() {
290
292
  // Subcommand: new
291
293
  // ---------------------------------------------------------------------------
292
294
 
293
- function cmdNew(targetDir, longMode) {
295
+ function cmdNew(targetDir, longMode, printLogs) {
294
296
  const sf = path.join(targetDir, '.ocmux.json');
295
297
  if (fs.existsSync(sf)) {
296
298
  console.error(`Error: a server already exists in ${targetDir}.`);
@@ -311,14 +313,14 @@ function cmdNew(targetDir, longMode) {
311
313
  console.error('Cancelled.');
312
314
  process.exit(0);
313
315
  }
314
- doNew(targetDir, longMode);
316
+ doNew(targetDir, longMode, printLogs);
315
317
  });
316
318
  } else {
317
- doNew(targetDir, longMode);
319
+ doNew(targetDir, longMode, printLogs);
318
320
  }
319
321
  }
320
322
 
321
- function doNew(targetDir, longMode) {
323
+ function doNew(targetDir, longMode, printLogs) {
322
324
  ensureSession();
323
325
  const lf = logfileFor(targetDir);
324
326
 
@@ -343,8 +345,9 @@ function doNew(targetDir, longMode) {
343
345
  try { fs.truncateSync(lf); } catch { /* may not exist yet */ }
344
346
 
345
347
  // Send the command to the interactive shell (like typing it manually)
348
+ const serveFlags = printLogs ? '--port 0 --print-logs' : '--port 0';
346
349
  tmux(['send-keys', '-t', `${SESSION}:${winIdx}.0`,
347
- `opencode serve --port 0 2>&1 | tee '${lf}'`, 'Enter']);
350
+ `opencode serve ${serveFlags} 2>&1 | tee '${lf}'`, 'Enter']);
348
351
 
349
352
  // Wait up to 10s for the URL in the logfile.
350
353
  // Don't break on first match — keep polling so url ends up holding the
@@ -432,12 +435,15 @@ function cmdKill(statefile) {
432
435
  function main() {
433
436
  let longMode = false;
434
437
  let gitMode = null;
438
+ let printLogs = false;
435
439
  const positional = [];
436
440
  let dirIsForced = false;
437
441
 
438
442
  for (const arg of process.argv.slice(2)) {
439
443
  if (arg === '-l') {
440
444
  longMode = true;
445
+ } else if (arg === '--print-logs') {
446
+ printLogs = true;
441
447
  } else if (arg === '--git') {
442
448
  if (gitMode && gitMode !== 'nearest') {
443
449
  console.error("Error: cannot combine '--git' with '--GIT'.");
@@ -506,6 +512,10 @@ function main() {
506
512
  console.error("Error: '--git' and '--GIT' are only valid with 'serve'.");
507
513
  process.exit(1);
508
514
  }
515
+ if (printLogs) {
516
+ console.error("Error: '--print-logs' is only valid with 'serve'.");
517
+ process.exit(1);
518
+ }
509
519
  if (targetDir !== process.cwd()) {
510
520
  console.error("Error: 'list' does not accept a directory path.");
511
521
  process.exit(1);
@@ -516,6 +526,10 @@ function main() {
516
526
  console.error("Error: '--git' and '--GIT' are only valid with 'serve'.");
517
527
  process.exit(1);
518
528
  }
529
+ if (printLogs) {
530
+ console.error("Error: '--print-logs' is only valid with 'serve'.");
531
+ process.exit(1);
532
+ }
519
533
  const sf = findParentStatefile(targetDir);
520
534
  if (!sf) {
521
535
  console.error(`Error: no opencode server found in ${targetDir} or any of its parents.`);
@@ -528,12 +542,16 @@ function main() {
528
542
  const flagLabel = gitMode === 'root' ? '--GIT' : '--git';
529
543
  targetDir = resolveGitDir(targetDir, gitMode === 'root', flagLabel);
530
544
  }
531
- cmdNew(targetDir, longMode);
545
+ cmdNew(targetDir, longMode, printLogs);
532
546
  } else {
533
547
  if (gitMode) {
534
548
  console.error("Error: '--git' and '--GIT' are only valid with 'serve'.");
535
549
  process.exit(1);
536
550
  }
551
+ if (printLogs) {
552
+ console.error("Error: '--print-logs' is only valid with 'serve'.");
553
+ process.exit(1);
554
+ }
537
555
  // Default mode
538
556
  const sf = findParentStatefile(targetDir);
539
557
  if (!sf) {